package org.unsynchronized;
import java.io.*;
import java.util.*;
import java.util.regex.*;
/**
* The main user-facing class for the jdeserialize library. Also the implementation of
* the command-line tool.<br/>
* <br/>
* Library:<br/>
* <br/>
* The jdeserialize class parses the stream (method run()). From there, call the
* getContent() method to get an itemized list of all items written to the stream,
* or getHandleMaps() to get a list of all handle->content maps generated during parsing.
* The objects are generally instances that implement the interface "content"; see the
* documentation of various implementors to get more information about the inner
* representations.<br/>
* <br/>
* To enable debugging on stdout, use the enableDebug() or disableDebug() options. <br/>
* <br/>
* <br/>
* Command-line tool: <br/>
* <br/>
* The tool reads in a set of files and generates configurable output on stdout. The
* primary output consists of three separate stages. The first stage is a textual
* description of every piece of content in the stream, in the order it was written.
* There is generally a one-to-one mapping between ObjectOutputStream.writeXXX() calls and
* items printed in the first stage. The first stage may be suppressed with the
* -nocontent command-line option. <br/>
* <br/>
* The second stage is a list of every class declaration serialized in the file. These
* are formatted as normal Java language class declarations. Several options are
* available to govern this stage, including -filter, -showarrays, -noclasses, and
* -fixnames. <br/>
* <br/>
* The third stage is a dump of every instance embedded inside the stream, including
* textual descriptions of field values. This is useful for casual viewing of class data.
* To suppress this stage, use -noinstances. <br/>
* <br/>
* You can also get debugging information generated during the parse phase by supplying
* -debug.
* <br/>
* The data from block data objects can be extracted with the -blockdata <file> option.
* Additionally, a manifest describing the size of each individual block can be generated
* with the -blockdatamanifest <file> option.
* <br/>
* References: <br/>
* - Java Object Serialization Specification ch. 6 (Object Serialization Stream
* Protocol): <br/>
* http://download.oracle.com/javase/6/docs/platform/serialization/spec/protocol.html <br/>
* - "Modified UTF-8 Strings" within the JNI specification:
* http://download.oracle.com/javase/1.5.0/docs/guide/jni/spec/types.html#wp16542 <br/>
* - "Inner Classes Specification" within the JDK 1.1.8 docs:
* http://java.sun.com/products/archive/jdk/1.1/ <br/>
* - "Java Language Specification", third edition, particularly section 3:
* http://java.sun.com/docs/books/jls/third_edition/html/j3TOC.html <br/>
*
* @see Content
*/
public class JDeserialize {
public static final long serialVersionUID = 78790714646095L;
public static final String INDENT = " ";
public static final int CODEWIDTH = 90;
public static final String linesep = System.getProperty("line.separator");
public static final String[] keywords = new String[] {
"abstract", "continue", "for", "new", "switch", "assert", "default", "if",
"package", "synchronized", "boolean", "do", "goto", "private", "this",
"break", "double", "implements", "protected", "throw", "byte", "else",
"import", "public", "throws", "case", "enum", "instanceof", "return",
"transient", "catch", "extends", "int", "short", "try", "char", "final",
"interface", "static", "void", "class", "finally", "long", "strictfp",
"volatile", "const", "float", "native", "super", "while" };
public static HashSet<String> keywordSet;
private String filename;
private HashMap<Integer,Content> handles = new HashMap<Integer,Content>();
private ArrayList<Map<Integer,Content>> handlemaps = new ArrayList<Map<Integer,Content>>();
private ArrayList<Content> content;
private int curhandle;
private boolean debugEnabled;
static {
keywordSet = new HashSet<String>();
for(String kw: keywords) {
keywordSet.add(kw);
}
}
/**
* <p>
* Retrieves the list of content objects that were written to the stream. Each item
* generally corresponds to an invocation of an ObjectOutputStream writeXXX() method.
* A notable exception is the class exceptionstate, which represents an embedded
* exception that was caught during serialization.
* </p>
*
* <p>
* See the various implementors of content to get information about what data is
* available.
* </p>
*
* <p>
* Entries in the list may be null, because it's perfectly legitimate to write a null
* reference to the stream.
* </p>
*
* @return a list of content objects
* @see Content
* @see ExceptionState
*/
public List<Content> getContent() {
return content;
}
/**
* <p>
* Return a list of Maps containing every object with a handle. The keys are integers
* -- the handles themselves -- and the values are instances of type content.
* </p>
*
* <p>
* Although there is only one map active at a given point, a stream may have multiple
* logical maps: when a reset happens (indicated by TC_RESET), the current map is
* cleared.
* </p>
*
* <p>
* See the spec for details on handles.
* </p>
* @return a list of <Integer,content> maps
*/
public List<Map<Integer,Content>> getHandleMaps() {
return handlemaps;
}
/**
* Suitably escapes non-printable-ASCII characters (and doublequotes) for use
* in a Java string literal.
*
* @param str string to escape
* @return an escaped version of the string
*/
public static String unicodeEscape(String str) {
StringBuffer sb = new StringBuffer();
int cplen = str.codePointCount(0, str.length());
for(int i = 0; i < cplen; i++) {
int cp = str.codePointAt(i);
if(cp == '"') {
sb.append("\\\"");
}
if(cp < 0x20 || cp > 0x7f) {
sb.append("\\u" + hexnoprefix(4));
} else {
sb.appendCodePoint(cp);
}
}
return sb.toString();
}
public static String indent(int level) {
StringBuffer sb = new StringBuffer("");
for(int i = 0; i < level; i++) {
sb.append(INDENT);
}
return sb.toString();
}
public void read_Classdata(DataInputStream dis, Instance inst) throws IOException {
ArrayList<ClassDesc> classes = new ArrayList<ClassDesc>();
inst.classdesc.getHierarchy(classes);
Map<ClassDesc, Map<Field, Object>> alldata = new HashMap<ClassDesc, Map<Field, Object>>();
Map<ClassDesc, List<Content>> ann = new HashMap<ClassDesc, List<Content>>();
for(ClassDesc cd: classes) {
Map<Field, Object> values = new HashMap<Field, Object>();
if((cd.descflags & ObjectStreamConstants.SC_SERIALIZABLE) != 0) {
if((cd.descflags & ObjectStreamConstants.SC_EXTERNALIZABLE) != 0) {
throw new IOException("SC_EXTERNALIZABLE & SC_SERIALIZABLE encountered");
}
for(Field f: cd.fields) {
Object o = read_FieldValue(f.type, dis);
values.put(f, o);
}
alldata.put(cd, values);
if((cd.descflags & ObjectStreamConstants.SC_WRITE_METHOD) != 0) {
if((cd.descflags & ObjectStreamConstants.SC_ENUM) != 0) {
throw new IOException("SC_ENUM & SC_WRITE_METHOD encountered!");
}
ann.put(cd, read_classAnnotation(dis));
}
} else if((cd.descflags & ObjectStreamConstants.SC_EXTERNALIZABLE) != 0) {
if((cd.descflags & ObjectStreamConstants.SC_SERIALIZABLE) != 0) {
throw new IOException("SC_SERIALIZABLE & SC_EXTERNALIZABLE encountered");
}
if((cd.descflags & ObjectStreamConstants.SC_BLOCK_DATA) != 0) {
throw new EOFException("hit externalizable with nonzero SC_BLOCK_DATA; can't interpret data");
} else {
ann.put(cd, read_classAnnotation(dis));
}
}
}
inst.annotations = ann;
inst.fielddata = alldata;
}
public Object read_FieldValue(FieldType f, DataInputStream dis) throws IOException {
switch(f) {
case BYTE:
return Byte.valueOf(dis.readByte());
case CHAR:
return Character.valueOf(dis.readChar());
case DOUBLE:
return Double.valueOf(dis.readDouble());
case FLOAT:
return Float.valueOf(dis.readFloat());
case INTEGER:
return Integer.valueOf(dis.readInt());
case LONG:
return Long.valueOf(dis.readLong());
case SHORT:
return Short.valueOf(dis.readShort());
case BOOLEAN:
return Boolean.valueOf(dis.readBoolean());
case OBJECT:
case ARRAY:
byte stc = dis.readByte();
if(f == FieldType.ARRAY && stc != ObjectStreamConstants.TC_ARRAY) {
throw new IOException("array type listed, but typecode is not TC_ARRAY: " + hex(stc));
}
Content c = read_Content(stc, dis, false);
if(c != null && c.isExceptionObject()) {
throw new ReadException(c);
}
return c;
default:
throw new IOException("can't process type: " + f.toString());
}
}
public JDeserialize(String filename) {
this.filename = filename;
}
private int newHandle() {
return curhandle++;
}
public static String resolveJavaType(FieldType type, String classname, boolean convertSlashes, boolean fixname) throws IOException {
if(type == FieldType.ARRAY) {
StringBuffer asb = new StringBuffer("");
for(int i = 0; i < classname.length(); i++) {
char ch = classname.charAt(i);
switch(ch) {
case '[':
asb.append("[]");
continue;
case 'L':
String cn = decodeClassName(classname.substring(i), convertSlashes);
if(fixname) {
cn = fixClassName(cn);
}
return cn + asb.toString();
default:
if(ch < 1 || ch > 127) {
throw new ValidityException("invalid array field type descriptor character: " + classname);
}
FieldType ft = FieldType.get((byte)ch);
if(i != (classname.length()-1)) {
throw new ValidityException("array field type descriptor is too long: " + classname);
}
String ftn = ft.getJavaType();
if(fixname) {
ftn = fixClassName(ftn);
}
return ftn + asb.toString();
}
}
throw new ValidityException("array field type descriptor is too short: " + classname);
} else if(type == FieldType.OBJECT) {
return decodeClassName(classname, convertSlashes);
} else {
return type.getJavaType();
}
}
public List<Content> read_classAnnotation(DataInputStream dis) throws IOException {
List<Content> list = new ArrayList<Content>();
while(true) {
byte tc = dis.readByte();
if(tc == ObjectStreamConstants.TC_ENDBLOCKDATA) {
return list;
}
if(tc == ObjectStreamConstants.TC_RESET) {
reset();
continue;
}
Content c = read_Content(tc, dis, true);
if(c != null && c.isExceptionObject()) {
throw new ReadException(c);
}
list.add(c);
}
}
public static void dump_Instance(int indentlevel, Instance inst, PrintStream ps) {
StringBuffer sb = new StringBuffer();
sb.append("[instance " + hex(inst.handle) + ": " + hex(inst.classdesc.handle) + "/" + inst.classdesc.name);
if(inst.annotations != null && inst.annotations.size() > 0) {
sb.append(linesep).append(" object annotations:").append(linesep);
for(ClassDesc cd: inst.annotations.keySet()) {
sb.append(" ").append(cd.name).append(linesep);
for(Content c: inst.annotations.get(cd)) {
sb.append(" ").append(c.toString()).append(linesep);
}
}
}
if(inst.fielddata != null && inst.fielddata.size() > 0) {
sb.append(linesep).append(" field data:").append(linesep);
for(ClassDesc cd: inst.fielddata.keySet()) {
sb.append(" ").append(hex(cd.handle)).append("/").append(cd.name).append(":").append(linesep);
for(Field f: inst.fielddata.get(cd).keySet()) {
Object o = inst.fielddata.get(cd).get(f);
sb.append(" ").append(f.name).append(": ");
if(o instanceof Content) {
Content c = (Content)o;
int h = c.getHandle();
if(h == inst.handle) {
sb.append("this");
} else {
sb.append("r" + hex(h));
}
sb.append(": ").append(c.toString());
sb.append(linesep);
} else {
sb.append("" + o).append(linesep);
}
}
}
}
sb.append("]");
ps.println(sb);
}
/**
* "Fix" the given name by transforming illegal characters, such that the end result
* is a legal Java identifier that is not a keyword.
* If the string is modified at all, the result will be prepended with "$__".
*
* @param name the name to be transformed
* @return the unmodified string if it is legal, otherwise a legal-identifier version
*/
public static String fixClassName(String name) {
if(name == null) {
return "$__null";
}
if(keywordSet.contains(name)) {
return "$__" + name;
}
StringBuffer sb = new StringBuffer();
int cplen = name.codePointCount(0, name.length());
if(cplen < 1) {
return "$__zerolen";
}
boolean modified = false;
int scp = name.codePointAt(0);
if(!Character.isJavaIdentifierStart(scp)) {
modified = true;
if(!Character.isJavaIdentifierPart(scp) || Character.isIdentifierIgnorable(scp)) {
sb.append("x");
} else {
sb.appendCodePoint(scp);
}
} else {
sb.appendCodePoint(scp);
}
for(int i = 1; i < cplen; i++) {
int cp = name.codePointAt(i);
if(!Character.isJavaIdentifierPart(cp) || Character.isIdentifierIgnorable(cp)) {
modified = true;
sb.append("x");
} else {
sb.appendCodePoint(cp);
}
}
if(modified) {
return "$__" + sb.toString();
} else {
return name;
}
}
public static void dump_ClassDesc(int indentlevel, ClassDesc cd, PrintStream ps, boolean fixname) throws IOException {
String classname = cd.name;
if(fixname) {
classname = fixClassName(classname);
}
if(cd.annotations != null && cd.annotations.size() > 0) {
ps.println(indent(indentlevel) + "// annotations: ");
for(Content c: cd.annotations) {
ps.print(indent(indentlevel) + "// " + indent(1));
ps.println(c.toString());
}
}
if(cd.classtype == ClassDescType.NORMALCLASS) {
if((cd.descflags & ObjectStreamConstants.SC_ENUM) != 0) {
ps.print(indent(indentlevel) + "enum " + classname + " {");
boolean shouldindent = true;
int len = indent(indentlevel+1).length();
for(String econst: cd.enumconstants) {
if(shouldindent) {
ps.println("");
ps.print(indent(indentlevel+1));
shouldindent = false;
}
len += econst.length();
ps.print(econst + ", ");
if(len >= CODEWIDTH) {
len = indent(indentlevel+1).length();
shouldindent = true;
}
}
ps.println("");
ps.println(indent(indentlevel) + "}");
return;
}
ps.print(indent(indentlevel));
if(cd.isStaticMemberClass()) {
ps.print("static ");
}
ps.print("class " + (classname.charAt(0) == '[' ? resolveJavaType(FieldType.ARRAY, cd.name, false, fixname) : classname));
if(cd.superclass != null) {
ps.print(" extends " + cd.superclass.name);
}
ps.print(" implements ");
if((cd.descflags & ObjectStreamConstants.SC_EXTERNALIZABLE) != 0) {
ps.print("java.io.Externalizable");
} else {
ps.print("java.io.Serializable");
}
if(cd.interfaces != null) {
for(String intf: cd.interfaces) {
ps.print(", " + intf);
}
}
ps.println(" {");
for(Field f: cd.fields) {
if(f.isInnerClassReference()) {
continue;
}
ps.print(indent(indentlevel+1) + f.getJavaType());
ps.println(" " + f.name + ";");
}
for(ClassDesc icd: cd.innerclasses) {
dump_ClassDesc(indentlevel+1, icd, ps, fixname);
}
ps.println(indent(indentlevel)+"}");
} else if(cd.classtype == ClassDescType.PROXYCLASS) {
ps.print(indent(indentlevel) + "// proxy class " + hex(cd.handle));
if(cd.superclass != null) {
ps.print(" extends " + cd.superclass.name);
}
ps.println(" implements ");
for(String intf: cd.interfaces) {
ps.println(indent(indentlevel) + "// " + intf + ", ");
}
if((cd.descflags & ObjectStreamConstants.SC_EXTERNALIZABLE) != 0) {
ps.println(indent(indentlevel) + "// java.io.Externalizable");
} else {
ps.println(indent(indentlevel) + "// java.io.Serializable");
}
} else {
throw new ValidityException("encountered invalid classdesc type!");
}
}
public void setHandle(int handle, Content c) throws IOException {
if(handles.containsKey(handle)) {
throw new IOException("trying to reset handle " + hex(handle));
}
handles.put(handle, c);
}
public void reset() {
debug("reset ordered!");
if(handles != null && handles.size() > 0) {
HashMap<Integer,Content> hm = new HashMap<Integer,Content>();
hm.putAll(handles);
handlemaps.add(hm);
}
handles.clear();
curhandle = ObjectStreamConstants.baseWireHandle; // 0x7e0000
}
/**
* Read the content of a thrown exception object. According to the spec, this must be
* an object of type Throwable. Although the Sun JDK always appears to provide enough
* information about the hierarchy to reach all the way back to java.lang.Throwable,
* it's unclear whether this is actually a requirement. From my reading, it's
* possible that some other ObjectOutputStream implementations may leave some gaps in
* the hierarchy, forcing this app to hit the classloader. To avoid this, we merely
* ensure that the written object is indeed an instance; ensuring that the object is
* indeed a Throwable is an exercise left to the user.
*/
public Content read_Exception(DataInputStream dis) throws IOException {
reset();
byte tc = dis.readByte();
if(tc == ObjectStreamConstants.TC_RESET) {
throw new ValidityException("TC_RESET for object while reading exception: what should we do?");
}
Content c = read_Content(tc, dis, false);
if(c == null) {
throw new ValidityException("stream signaled for an exception, but exception object was null!");
}
if(!(c instanceof Instance)) {
throw new ValidityException("stream signaled for an exception, but content is not an object!");
}
if(c.isExceptionObject()) {
throw new ReadException(c);
}
c.setIsExceptionObject(true);
reset();
return c;
}
public ClassDesc read_classDesc(DataInputStream dis) throws IOException {
byte tc = dis.readByte();
ClassDesc cd = handle_classDesc(tc, dis, false);
return cd;
}
public ClassDesc read_newClassDesc(DataInputStream dis) throws IOException {
byte tc = dis.readByte();
ClassDesc cd = handle_newClassDesc(tc, dis);
return cd;
}
public Content read_prevObject(DataInputStream dis) throws IOException {
int handle = dis.readInt();
if(!handles.containsKey(Integer.valueOf(handle))) {
throw new ValidityException("can't find an entry for handle " + hex(handle));
}
Content c = handles.get(handle);
debug("prevObject: handle " + hex(c.getHandle()) + " classdesc " + c.toString());
return c;
}
public ClassDesc handle_newClassDesc(byte tc, DataInputStream dis) throws IOException {
return handle_classDesc(tc, dis, true);
}
public ClassDesc handle_classDesc(byte tc, DataInputStream dis, boolean mustBeNew) throws IOException {
if(tc == ObjectStreamConstants.TC_CLASSDESC) {
String name = dis.readUTF();
long serialVersionUID = dis.readLong();
int handle = newHandle();
byte descflags = dis.readByte();
short nfields = dis.readShort();
if(nfields < 0) {
throw new IOException("invalid field count: " + nfields);
}
Field[] fields = new Field[nfields];
for(short s = 0; s < nfields; s++) {
byte ftype = dis.readByte();
if(ftype == 'B' || ftype == 'C' || ftype == 'D'
|| ftype == 'F' || ftype == 'I' || ftype == 'J'
|| ftype == 'S' || ftype == 'Z') {
String fieldname = dis.readUTF();
fields[s] = new Field(FieldType.get(ftype), fieldname);
} else if(ftype == '[' || ftype == 'L') {
String fieldname = dis.readUTF();
byte stc = dis.readByte();
StringObject classname = read_newString(stc, dis);
//String classname = dis.readUTF();
fields[s] = new Field(FieldType.get(ftype), fieldname, classname);
} else {
throw new IOException("invalid field type char: " + hex(ftype));
}
}
ClassDesc cd = new ClassDesc(ClassDescType.NORMALCLASS);
cd.name = name;
cd.serialVersionUID = serialVersionUID;
cd.handle = handle;
cd.descflags = descflags;
cd.fields = fields;
cd.annotations = read_classAnnotation(dis);
cd.superclass = read_classDesc(dis);
setHandle(handle, cd);
debug("read new classdesc: handle " + hex(handle) + " name " + name);
return cd;
} else if(tc == ObjectStreamConstants.TC_NULL) {
if(mustBeNew) {
throw new ValidityException("expected new class description -- got null!");
}
debug("read null classdesc");
return null;
} else if(tc == ObjectStreamConstants.TC_REFERENCE) {
if(mustBeNew) {
throw new ValidityException("expected new class description -- got a reference!");
}
Content c = read_prevObject(dis);
if(!(c instanceof ClassDesc)) {
throw new IOException("referenced object not a class description!");
}
ClassDesc cd = (ClassDesc)c;
return cd;
} else if(tc == ObjectStreamConstants.TC_PROXYCLASSDESC) {
int handle = newHandle();
int icount = dis.readInt();
if(icount < 0) {
throw new IOException("invalid proxy interface count: " + hex(icount));
}
String interfaces[] = new String[icount];
for(int i = 0; i < icount; i++) {
interfaces[i] = dis.readUTF();
}
ClassDesc cd = new ClassDesc(ClassDescType.PROXYCLASS);
cd.handle = handle;
cd.interfaces = interfaces;
cd.annotations = read_classAnnotation(dis);
cd.superclass = read_classDesc(dis);
setHandle(handle, cd);
cd.name = "(proxy class; no name)";
debug("read new proxy classdesc: handle " + hex(handle) + " names [" + Arrays.toString(interfaces) + "]");
return cd;
} else {
throw new ValidityException("expected a valid class description starter got " + hex(tc));
}
}
public ArrayObject read_newArray(DataInputStream dis) throws IOException {
ClassDesc cd = read_classDesc(dis);
int handle = newHandle();
debug("reading new array: handle " + hex(handle) + " classdesc " + cd.toString());
if(cd.name.length() < 2) {
throw new IOException("invalid name in array classdesc: " + cd.name);
}
ArrayCol ac = read_arrayValues(cd.name.substring(1), dis);
return new ArrayObject(handle, cd, ac);
}
public ArrayCol read_arrayValues(String str, DataInputStream dis) throws IOException {
byte b = str.getBytes("UTF-8")[0];
FieldType ft = FieldType.get(b);
int size = dis.readInt();
if(size < 0) {
throw new IOException("invalid array size: " + size);
}
ArrayCol ac = new ArrayCol(ft);
for(int i = 0; i < size; i++) {
ac.add(read_FieldValue(ft, dis));
continue;
}
return ac;
}
public ClassObject read_newClass(DataInputStream dis) throws IOException {
ClassDesc cd = read_classDesc(dis);
int handle = newHandle();
debug("reading new class: handle " + hex(handle) + " classdesc " + cd.toString());
ClassObject c = new ClassObject(handle, cd);
setHandle(handle, c);
return c;
}
public EnumObject read_newEnum(DataInputStream dis) throws IOException {
ClassDesc cd = read_classDesc(dis);
if(cd == null) {
throw new IOException("enum classdesc can't be null!");
}
int handle = newHandle();
debug("reading new enum: handle " + hex(handle) + " classdesc " + cd.toString());
byte tc = dis.readByte();
StringObject so = read_newString(tc, dis);
cd.addEnum(so.value);
setHandle(handle, so);
return new EnumObject(handle, cd, so);
}
public StringObject read_newString(byte tc, DataInputStream dis) throws IOException {
byte[] data;
if(tc == ObjectStreamConstants.TC_REFERENCE) {
Content c = read_prevObject(dis);
if(!(c instanceof StringObject)) {
throw new IOException("got reference for a string, but referenced value was something else!");
}
return (StringObject)c;
}
int handle = newHandle();
if(tc == ObjectStreamConstants.TC_STRING) {
int len = dis.readUnsignedShort();
data = new byte[len];
} else if(tc == ObjectStreamConstants.TC_LONGSTRING) {
long len = dis.readLong();
if(len < 0) {
throw new IOException("invalid long string length: " + len);
}
if(len > 2147483647) {
throw new IOException("long string is too long: " + len);
}
if(len < 65536) {
debugerr("warning: small string length encoded as TC_LONGSTRING: " + len);
}
data = new byte[(int)len];
} else if(tc == ObjectStreamConstants.TC_NULL) {
throw new ValidityException("stream signaled TC_NULL when string type expected!");
} else {
throw new IOException("invalid tc byte in string: " + hex(tc));
}
dis.readFully(data);
debug("reading new string: handle " + hex(handle) + " bufsz " + data.length);
StringObject sobj = new StringObject(handle, data);
setHandle(handle, sobj);
return sobj;
}
public BlockData read_blockdata(byte tc, DataInputStream dis) throws IOException {
int size;
if(tc == ObjectStreamConstants.TC_BLOCKDATA) {
size = dis.readUnsignedByte();
} else if(tc == ObjectStreamConstants.TC_BLOCKDATALONG) {
size = dis.readInt();
} else {
throw new IOException("invalid tc value for blockdata: " + hex(tc));
}
if(size < 0) {
throw new IOException("invalid value for blockdata size: " + size);
}
byte[] b = new byte[size];
dis.readFully(b);
debug("read blockdata of size " + size);
return new BlockData(b);
}
public Instance read_newObject(DataInputStream dis) throws IOException {
ClassDesc cd = read_classDesc(dis);
int handle = newHandle();
debug("reading new object: handle " + hex(handle) + " classdesc " + cd.toString());
Instance i = new Instance();
i.classdesc = cd;
i.handle = handle;
setHandle(handle, i);
read_Classdata(dis, i);
debug("done reading object for handle " + hex(handle));
return i;
}
/**
* <p>
* Read the next object corresponding to the spec grammar rule "content", and return
* an object of type content.
* </p>
*
* <p>
* Usually, there is a 1:1 mapping of content items and returned instances. The
* one case where this isn't true is when an exception is embedded inside another
* object. When this is encountered, only the serialized exception object is
* returned; it's up to the caller to backtrack in order to gather any data from the
* object that was being serialized when the exception was thrown.
* </p>
*
* @param tc the last byte read from the stream; it must be one of the TC_* values
* within ObjectStreamConstants.*
* @param dis the DataInputStream to read from
* @param blockdata whether or not to read TC_BLOCKDATA (this is the difference
* between spec rules "object" and "content").
* @return an object representing the last read item from the stream
* @throws IOException when a validity or I/O error occurs while reading
*/
public Content read_Content(byte tc, DataInputStream dis, boolean blockdata) throws IOException {
try {
switch(tc) {
case ObjectStreamConstants.TC_OBJECT:
return read_newObject(dis);
case ObjectStreamConstants.TC_CLASS:
return read_newClass(dis);
case ObjectStreamConstants.TC_ARRAY:
return read_newArray(dis);
case ObjectStreamConstants.TC_STRING:
case ObjectStreamConstants.TC_LONGSTRING:
return read_newString(tc, dis);
case ObjectStreamConstants.TC_ENUM:
return read_newEnum(dis);
case ObjectStreamConstants.TC_CLASSDESC:
case ObjectStreamConstants.TC_PROXYCLASSDESC:
return handle_newClassDesc(tc, dis);
case ObjectStreamConstants.TC_REFERENCE:
return read_prevObject(dis);
case ObjectStreamConstants.TC_NULL:
return null;
case ObjectStreamConstants.TC_EXCEPTION:
return read_Exception(dis);
case ObjectStreamConstants.TC_BLOCKDATA:
case ObjectStreamConstants.TC_BLOCKDATALONG:
if(blockdata == false) {
throw new IOException("got a blockdata TC_*, but not allowed here: " + hex(tc));
}
return read_blockdata(tc, dis);
default:
throw new IOException("unknown content tc byte in stream: " + hex(tc));
}
} catch (ReadException ere) {
return ere.getExceptionObject();
}
}
/**
* <p>
* Reads in an entire ObjectOutputStream output on the given stream, filing
* this object's content and handle maps with data about the objects in the stream.
* </p>
*
* <p>
* If shouldConnect is true, then jdeserialize will attempt to identify member classes
* by their names according to the details laid out in the Inner Classes
* Specification. If it finds one, it will set the classdesc's flag indicating that
* it is an member class, and it will create a reference in its enclosing class.
* </p>
*
* @param is an open InputStream on a serialized stream of data
* @param shouldConnect true if jdeserialize should attempt to identify and connect
* member classes with their enclosing classes
*
* Also see the <pre>connectMemberClasses</pre> method for more information on the
* member-class-detection algorithm.
*/
public void run(InputStream is, boolean shouldConnect) throws IOException {
LoggerInputStream lis = null;
DataInputStream dis = null;
try {
lis = new LoggerInputStream(is);
dis = new DataInputStream(lis);
short magic = dis.readShort();
if(magic != ObjectStreamConstants.STREAM_MAGIC) {
throw new ValidityException("file magic mismatch! expected " + ObjectStreamConstants.STREAM_MAGIC + ", got " + magic);
}
short streamversion = dis.readShort();
if(streamversion != ObjectStreamConstants.STREAM_VERSION) {
throw new ValidityException("file version mismatch! expected " + ObjectStreamConstants.STREAM_VERSION + ", got " + streamversion);
}
reset();
content = new ArrayList<Content>();
while(true) {
byte tc;
try {
lis.record();
tc = dis.readByte();
if(tc == ObjectStreamConstants.TC_RESET) {
reset();
continue;
}
} catch (EOFException eoe) {
break;
}
Content c = read_Content(tc, dis, true);
System.out.println("read: " + c);
if(c != null && c.isExceptionObject()) {
c = new ExceptionState(c, lis.getRecordedData());
}
content.add(c);
}
} finally {
if(dis != null) {
try {
dis.close();
} catch (Exception ignore) { }
}
if(lis != null) {
try {
lis.close();
} catch (Exception ignore) {}
}
}
for(Content c: handles.values()) {
c.validate();
}
if(shouldConnect) {
connectMemberClasses();
for(Content c: handles.values()) {
c.validate();
}
}
if(handles != null && handles.size() > 0) {
HashMap<Integer,Content> hm = new HashMap<Integer,Content>();
hm.putAll(handles);
handlemaps.add(hm);
}
}
public void dump(Getopt go) throws IOException {
if(go.hasOption("-blockdata") || go.hasOption("-blockdatamanifest")) {
List<String> bout = go.getArguments("-blockdata");
List<String> mout = go.getArguments("-blockdatamanifest");
FileOutputStream bos = null, mos = null;
PrintWriter pw = null;
try {
if(bout != null && bout.size() > 0) {
bos = new FileOutputStream(bout.get(0));
}
if(mout != null && bout.size() > 0) {
mos = new FileOutputStream(mout.get(0));
pw = new PrintWriter(mos);
pw.println("# Each line in this file that doesn't begin with a '#' contains the size of");
pw.println("# an individual blockdata block written to the stream.");
}
for(Content c: content) {
System.out.println(c.toString());
if(c instanceof BlockData) {
BlockData bd = (BlockData)c;
if(mos != null) {
pw.println(bd.buf.length);
}
if(bos != null) {
bos.write(bd.buf);
}
}
}
} finally {
if(bos != null) {
try {
bos.close();
} catch (IOException ignore) { }
}
if(mos != null) {
try {
pw.close();
mos.close();
} catch (IOException ignore) { }
}
}
}
if(!go.hasOption("-nocontent")) {
System.out.println("//// BEGIN stream content output");
for(Content c: content) {
System.out.println(c);
}
System.out.println("//// END stream content output");
System.out.println("");
}
if(!go.hasOption("-noclasses")) {
boolean showarray = go.hasOption("-showarrays");
List<String> fpat = go.getArguments("-filter");
System.out.println("//// BEGIN class declarations"
+ (showarray? "" : " (excluding array classes)")
+ ((fpat != null && fpat.size() > 0)
? " (exclusion filter " + fpat.get(0) + ")"
: ""));
for(Content c: handles.values()) {
if(c instanceof ClassDesc) {
ClassDesc cl = (ClassDesc)c;
if(showarray == false && cl.isArrayClass()) {
continue;
}
// Member classes will be displayed as part of their enclosing
// classes.
if(cl.isStaticMemberClass() || cl.isInnerClass()) {
continue;
}
if(fpat != null && fpat.size() > 0 && cl.name.matches(fpat.get(0))) {
continue;
}
dump_ClassDesc(0, cl, System.out, go.hasOption("-fixnames"));
System.out.println("");
}
}
System.out.println("//// END class declarations");
System.out.println("");
}
if(!go.hasOption("-noinstances")) {
System.out.println("//// BEGIN instance dump");
for(Content c: handles.values()) {
if(c instanceof Instance) {
Instance i = (Instance)c;
dump_Instance(0, i, System.out);
}
}
System.out.println("//// END instance dump");
System.out.println("");
}
}
/**
* <p>
* Connects member classes according to the rules specified by the JDK 1.1 Inner
* Classes Specification.
* </p>
*
* <pre>
* Inner classes:
* for each class C containing an object reference member R named this$N, do:
* if the name of C matches the pattern O$I
* AND the name O matches the name of an existing type T
* AND T is the exact type referred to by R, then:
* don't display the declaration of R in normal dumping,
* consider C to be an inner class of O named I
*
* Static member classes (after):
* for each class C matching the pattern O$I,
* where O is the name of a class in the same package
* AND C is not an inner class according to the above algorithm:
* consider C to be an inner class of O named I
* </pre>
*
* <p>
* This functions fills in the isInnerClass value in classdesc, the
* isInnerClassReference value in field, the isLocalInnerClass value in
* classdesc, and the isStaticMemberClass value in classdesc where necessary.
* </p>
*
* <p>
* A word on static classes: serializing a static member class S doesn't inherently
* require serialization of its parent class P. Unlike inner classes, S doesn't
* retain an instance of P, and therefore P's class description doesn't need to be
* written. In these cases, if parent classes can be found, their static member
* classes will be connected; but if they can't be found, the names will not be
* changed and no ValidityException will be thrown.
* </p>
*
* @throws ValidityException if the found values don't correspond to spec
*/
public void connectMemberClasses() throws IOException {
HashMap<ClassDesc, String> newnames = new HashMap<ClassDesc, String>();
HashMap<String, ClassDesc> classes = new HashMap<String, ClassDesc>();
HashSet<String> classnames = new HashSet<String>();
for(Content c: handles.values()) {
if(!(c instanceof ClassDesc)) {
continue;
}
ClassDesc cd = (ClassDesc)c;
classes.put(cd.name, cd);
classnames.add(cd.name);
}
Pattern fpat = Pattern.compile("^this\\$(\\d+)$");
Pattern clpat = Pattern.compile("^((?:[^\\$]+\\$)*[^\\$]+)\\$([^\\$]+)$");
for(ClassDesc cd: classes.values()) {
if(cd.classtype == ClassDescType.PROXYCLASS) {
continue;
}
for(Field f: cd.fields) {
if(f.type != FieldType.OBJECT) {
continue;
}
Matcher m = fpat.matcher(f.name);
if(!m.matches()) {
continue;
}
boolean islocal = false;
Matcher clmat = clpat.matcher(cd.name);
if(!clmat.matches()) {
throw new ValidityException("inner class enclosing-class reference field exists, but class name doesn't match expected pattern: class " + cd.name + " field " + f.name);
}
String outer = clmat.group(1), inner = clmat.group(2);
ClassDesc outercd = classes.get(outer);
if(outercd == null) {
throw new ValidityException("couldn't connect inner classes: outer class not found for field name " + f.name);
}
if(!outercd.name.equals(f.getJavaType())) {
throw new ValidityException("outer class field type doesn't match field type name: " + f.classname.value + " outer class name " + outercd.name);
}
outercd.addInnerClass(cd);
cd.setIsLocalInnerClass(islocal);
cd.setIsInnerClass(true);
f.setIsInnerClassReference(true);
newnames.put(cd, inner);
}
}
for(ClassDesc cd: classes.values()) {
if(cd.classtype == ClassDescType.PROXYCLASS) {
continue;
}
if(cd.isInnerClass()) {
continue;
}
Matcher clmat = clpat.matcher(cd.name);
if(!clmat.matches()) {
continue;
}
String outer = clmat.group(1), inner = clmat.group(2);
ClassDesc outercd = classes.get(outer);
if(outercd != null) {
outercd.addInnerClass(cd);
cd.setIsStaticMemberClass(true);
newnames.put(cd, inner);
}
}
for(ClassDesc ncd: newnames.keySet()) {
String newname = newnames.get(ncd);
if(classnames.contains(newname)) {
throw new ValidityException("can't rename class from " + ncd.name + " to " + newname + " -- class already exists!");
}
for(ClassDesc cd: classes.values()) {
if(cd.classtype == ClassDescType.PROXYCLASS) {
continue;
}
for(Field f: cd.fields) {
if(f.getJavaType().equals(ncd.name)) {
f.setReferenceTypeName(newname);
}
}
}
if(classnames.remove(ncd.name) == false) {
throw new ValidityException("tried to remove " + ncd.name + " from classnames cache, but couldn't find it!");
}
ncd.name = newname;
if(classnames.add(newname) == false) {
throw new ValidityException("can't rename class to " + newname + " -- class already exists!");
}
}
}
/**
* Decodes a class name according to the field-descriptor format in the jvm spec,
* section 4.3.2.
* @param fdesc name in field-descriptor format (Lfoo/bar/baz;)
* @param convertSlashes true iff slashes should be replaced with periods (true for
* "real" field-descriptor format; false for names in classdesc)
* @return a fully-qualified class name
* @throws ValidityException if the name isn't valid
*/
public static String decodeClassName(String fdesc, boolean convertSlashes) throws ValidityException {
if(fdesc.charAt(0) != 'L' || fdesc.charAt(fdesc.length()-1) != ';' || fdesc.length() < 3) {
throw new ValidityException("invalid name (not in field-descriptor format): " + fdesc);
}
String subs = fdesc.substring(1, fdesc.length()-1);
if(convertSlashes) {
return subs.replace('/', '.');
}
return subs;
}
public static String hexnoprefix(long value) {
return hexnoprefix(value, 2);
}
public static String hexnoprefix(long value, int len) {
if(value < 0) {
value = 256 + value;
}
String s = Long.toString(value, 16);
while(s.length() < len) {
s = "0" + s;
}
return s;
}
public static String hex(long value) {
return "0x" + hexnoprefix(value);
}
public static void debugerr(String message) {
System.err.println(message);
}
public void debug(String message) {
if(debugEnabled) {
System.out.println(message);
}
}
public static void main(String[] args) {
HashMap<String, Integer> options = new HashMap<String, Integer>();
Getopt go = new Getopt();
go.addOption("-help", 0, "Show this list.");
go.addOption("-debug", 0, "Write debug info generated during parsing to stdout.");
go.addOption("-filter", 1, "Exclude classes that match the given String.matches() regex from class output.");
go.addOption("-nocontent", 0, "Don't output descriptions of the content in the stream.");
go.addOption("-noinstances", 0, "Don't output descriptions of every instance.");
go.addOption("-showarrays", 0, "Show array class declarations (e.g. int[]).");
go.addOption("-noconnect", 0, "Don't attempt to connect member classes to their enclosing classes.");
go.addOption("-fixnames", 0, "In class names, replace illegal Java identifier characters with legal ones.");
go.addOption("-noclasses", 0, "Don't output class declarations.");
go.addOption("-blockdata", 1, "Write raw blockdata out to the specified file.");
go.addOption("-blockdatamanifest", 1, "Write blockdata manifest out to the specified file.");
try {
go.parse(args);
} catch (Getopt.OptionParseException ope) {
System.err.println("argument error: " + ope.getMessage());
System.out.println(go.getDescriptionString());
System.exit(1);
}
if(go.hasOption("-help")) {
System.out.println(go.getDescriptionString());
System.exit(1);
}
List<String> fargs = go.getOtherArguments();
if(fargs.size() < 1) {
debugerr("args: [options] file1 [file2 .. fileN]");
System.err.println("");
System.err.println(go.getDescriptionString());
System.exit(1);
}
for(String filename: fargs) {
FileInputStream fis = null;
try {
fis = new FileInputStream(filename);
JDeserialize jd = new JDeserialize(filename);
if(go.hasOption("-debug")) {
jd.debugEnabled = true;
} else {
jd.debugEnabled = false;
}
jd.run(fis, !go.hasOption("-noconnect"));
jd.dump(go);
} catch(EOFException eoe) {
debugerr("EOF error while attempting to decode file " + filename + ": " + eoe.getMessage());
eoe.printStackTrace();
} catch(IOException ioe) {
debugerr("error while attempting to decode file " + filename + ": " + ioe.getMessage());
ioe.printStackTrace();
} finally {
if(fis != null) {
try {
fis.close();
} catch (Exception ignore) { }
}
}
}
}
}