package xtc.lang; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.File; /** * A class file post-processor to rewrite a class file for the remapped line * number table or to append a SMAP attribute to the class file. This * post-processor is a utility for the source-to-source transformation tools * such as Jeannie. This post-processor provides the following two modes. * * First, stratify mode with -stratify command line flag rewrites the * "LineNumberTable" attribute for each method in the class file, and modify the * "SourceFile" attribute. This has an advantage of working well with both the * current java VM and debugger. However, this does not work if the number of * the orignal source files is more than one. * * Second, flatten mode with -flatten command line flag appends an SMAP to the * end of the class file as "SourceDebugExtension." This is a general and * powerful way to provide remapping information for the source-to-source * transformation. We found that this SMAP works well with the current java * debuggers (SUN jdb 1.6 and eclipse 3.2 Java debugger). However, JVMs in the * SUN JDK 1.6 and IBM J9 1.5.0 do not use SMAP when they dump stack trace. * * By default, this remapper operates in the flatten mode. The first solution * with the SMAP will be the right way in the long run as JVM supports better * stack dump messgage with SMAP. * * @author Byeongcheol Lee * */ public class ClassfileSourceRemapper { /** * Class file attribute names related to the line number table remapping. For * further information, look at the Java virtual machine specification. * http://java.sun.com/docs/books/jvms/second_edition/html/ClassFile.doc.html */ private static final String ANAME_SMAP = "SourceDebugExtension"; private static final String ANAME_LINENUMBERTABLE = "LineNumberTable"; private static final String ANAME_CODE = "Code"; private static final String ANAME_SOURCEFILE= "SourceFile"; /** * Constant pool tags for java class file. For further information, look at * the Java virtual machine specification. * http://java.sun.com/docs/books/jvms/second_edition/html/ClassFile.doc.html */ public static final int CP_Class = 7; public static final int CP_Fieldref = 9; public static final int CP_Methodref = 10; public static final int CP_InterfaceMethodref = 11; public static final int CP_String = 8; public static final int CP_Integer = 3; public static final int CP_Float = 4; public static final int CP_Long = 5; public static final int CP_Double = 6; public static final int CP_NameAndType = 12; public static final int CP_Utf8 = 1; /** * A line number entry in the "LineNumberTable" attribute. */ private static class LineNumberTableEntry { int start_pc; int line_number; LineNumberTableEntry(int pc, int line) { assert (pc <= 65535) & (line <= 65535); start_pc = pc; line_number =line; } int getStartPC() {return start_pc;} int getLineNumber() {return line_number;} } /** * A command line usage. * * @param umsg An additional message to explain what is wrong. */ private static void usage(String umsg) { String msg = "usage: ClassfileSourceRemapper {-stratify|-flatten} [java source file] [class file]"; System.err.println(msg + "\n" + umsg); System.exit(-1); } /** * Run the class file remapping process by taking a user command line. * * @param args The command line arguments. */ public static void main(String[] args) { //default option vlaue String javaSource = null; String classFile = null; boolean bStaratify = false; boolean bFlatten = true; for(int i=0;i <args.length;i++) { String arg = args[i]; if (arg.equals("-stratify")) { bFlatten = false; bStaratify = true; } else if (arg.equals("-flatten")) { bFlatten = true; bStaratify = false; } else if (javaSource == null ) { if (new File(arg).canRead()) { javaSource = arg; } else { usage("can not read " + arg); } } else if (classFile == null) { if (new File(arg).canRead()) { classFile = arg; } else { usage("can not write " + arg); } } } //validate options if (bStaratify && bFlatten) {usage("specify only one of the -stratify and -flatten");} if (javaSource== null) {usage("specify [java source file]");} if (classFile == null) {usage("spcify [class file]");} try { //get source-to-source map SourceMapExtractor smap = new SourceMapExtractor(javaSource); smap.genSMAP(); //validate source-to-source map if (smap.getNumberOfInputSourceFiles() < 1) { System.err.println("no source-to-source mapping found in the " + javaSource); System.exit(-1); } boolean bUseJSR45 = bStaratify; if (!bUseJSR45) { if (smap.getNumberOfInputSourceFiles() >= 2 ) { System.err.println("more than one input source files in the " + javaSource); System.err.println("This tool can not process class file with -flatten option"); System.err.println("Please consider using -stratify option"); System.exit(-1); } } //update class file for line number remapping.. ClassfileSourceRemapper lineRemapper = new ClassfileSourceRemapper(smap, classFile, classFile, bUseJSR45); lineRemapper.doRemapping(); } catch (IOException e) { System.err.println("failed in remapping line number information"); e.printStackTrace(); } } /** * A flag to rewrite line number table in the class file. */ private final boolean bRemapLineNumberTable; /** * A flag to add SMAP table to the class file. */ private final boolean bInsertSMAPTable; /** * An input class file name. */ private final String inputClassFile; /** * An output class file name. */ private final String outputClassFile; /** * A source-to-source remapping information. */ private final SourceMapExtractor smap; /** * An input stream for class file. */ private DataInputStream is; /** * An output stream for the post processed class file. */ private DataOutputStream os; /** * An in-memory stream to hold the content of postprocessed class file. */ private ByteArrayOutputStream bos; /** * A constant pool index of the injected UTF8 string for the input source file * name in the output class file. "sourcefile_index" field of the "SourceFile" * class attribute will be redirected to reference this new source file name. */ private int constantPoolEntryIndexForInputSourceFileName = -1; /** * A constant pool index of the injected UTF8 string for SMAP attribute name * in the output class file. This class file post-process will append to the * end of class file a "SourceDebugExtension" class attribute, and * "attribute_name_index" field of the debug class attribute will point to * this new UTF8 constant pool entry. */ private int constantPoolEntryIndexForSMAP = -1; /** * A constant pool entries only for UTF8 string. For an constrant pool index * number, i, UTF8ConstantPoolEntries[i] is non-null if the constant poll * entry is UTF8 string. Otherwise, it's null. */ private String[] UTF8ConstantPoolEntries; /** * @param smap A Source-to-source mapping. * @param inputClassFile An input class file. * @param outputClassFile An output class file. * @param bUseJSR45 Wheather or not to use JSR45 SMAP for remapping. */ public ClassfileSourceRemapper(SourceMapExtractor smap, String inputClassFile, String outputClassFile, boolean bUseJSR45) { this.smap = smap; this.inputClassFile = inputClassFile; this.outputClassFile = outputClassFile; if (bUseJSR45) { bRemapLineNumberTable = false; bInsertSMAPTable = true; } else { bRemapLineNumberTable = true; bInsertSMAPTable = false; } } /** * Define remapping of a line number table for a method. * * @param old An old line number table. * @param mname A method name. * @param mdesc A method signagure. * @return A new byte code to source line mapping. */ private LineNumberTableEntry[] adjustLineNumberTable( LineNumberTableEntry[] old, String mname, String mdesc) { assert (old != null); LineNumberTableEntry[] newtable = new LineNumberTableEntry[old.length]; for (int i = 0; i < newtable.length;i++) { LineNumberTableEntry oldEntry = old[i]; int pc = oldEntry.getStartPC(); int javaLine = oldEntry.getLineNumber(); int jniLine = smap.getSingleSourceLine(javaLine); newtable[i] = new LineNumberTableEntry(pc, jniLine); } return newtable; } /** * Update the line number mapping in the class file. */ public void doRemapping() throws IOException { assert (smap != null) && (is == null) && (os == null) && (bos == null); //read input class file and generate modified class file in the //memory try { is = new DataInputStream(new FileInputStream(inputClassFile)); bos = new ByteArrayOutputStream(); os = new DataOutputStream(bos); processClass(); is.close(); } catch(IOException e) { System.err.println("error while reading:" + inputClassFile); throw e; } //flush memory to the output class file try { byte cbytes[] = bos.toByteArray(); bos.close(); os.close(); assert (cbytes != null) && (cbytes.length > 0); FileOutputStream fos = new FileOutputStream(outputClassFile); fos.write(cbytes); fos.close(); } catch(IOException e) { System.err.println("error while writing to: " + outputClassFile); throw e; } is = null; os = null; bos = null; constantPoolEntryIndexForInputSourceFileName = -1; constantPoolEntryIndexForSMAP = -1; UTF8ConstantPoolEntries = null; } /** * Begin the line number remapping process. */ private void processClass() throws IOException { int magic = processInt(); assert magic == 0xcafebabe; processUnsignedShort(); //minor processUnsignedShort(); //major int numCP = is.readUnsignedShort(); assert numCP >= 1; UTF8ConstantPoolEntries = new String[numCP]; os.writeShort(numCP + 1); for(int i = 1; i < numCP; i++) { int tag = processCP(i); //Long, and double take two constant pool entry if (tag == CP_Double || tag == CP_Long ) { i++; } } int next_cp_index = numCP; if (bRemapLineNumberTable) { //inject UTF8 JNI source file name final String jniSourceFile = smap.getSingleSourceFileName(); final byte[] utf8InputSourceFileName = jniSourceFile.getBytes("UTF8"); constantPoolEntryIndexForInputSourceFileName = next_cp_index++; os.writeByte(CP_Utf8); os.writeShort(utf8InputSourceFileName.length); os.write(utf8InputSourceFileName); } if (bInsertSMAPTable) { constantPoolEntryIndexForSMAP = next_cp_index++; byte[] smap_section_name = ANAME_SMAP.getBytes("UTF8"); os.writeByte(1); os.writeShort(smap_section_name.length); os.write(smap_section_name); } int flag = processShort(); //flags int this_class = processUnsignedShort(); //this_class int super_class = processUnsignedShort(); //super_class //interfaces int numInterfaces = processUnsignedShort(); if (numInterfaces > 0) { processBytes(numInterfaces * 2); } //fields int numFields = processUnsignedShort(); for(int i = 0; i < numFields; i++) { //access flags(u2), name_index(u2), descriptor_index(u2) processBytes(6); //attributes_count(u2) int numFieldAttributes = processUnsignedShort(); for(int j = 0; j< numFieldAttributes; j++) { processAttribute(); } } //methods int numMethods = processUnsignedShort(); for(int i = 0; i < numMethods; i++) { processMethodInfo(i); } //class attributes int numClassAttributes = is.readUnsignedShort(); if (bInsertSMAPTable) { os.writeShort(numClassAttributes + 1); } else { os.writeShort(numClassAttributes); } for(int i = 0; i < numClassAttributes; i++) { int name_index = processUnsignedShort(); String attr_name = UTF8ConstantPoolEntries[name_index]; assert (attr_name != null); if (bRemapLineNumberTable && attr_name.equals(ANAME_SOURCEFILE)) { processSourceFile(); } else { processBytes(processInt()); } } if (bInsertSMAPTable) { //append SMAP attribute byte[] smap_content = smap.toStringInSMAPFormat().getBytes("UTF8"); os.writeShort(constantPoolEntryIndexForSMAP); os.writeInt(smap_content.length); os.write(smap_content); } assert is.read() == -1; return; } /** * Handle a constant pool entyr at an index. * * @param index A constant pool index number. * @return A tag value for the constant pool entry. */ private int processCP(int index) throws IOException { byte tag = processByte(); switch(tag) { case CP_Class: processBytes(2); break; case CP_Fieldref: case CP_Methodref: case CP_InterfaceMethodref: processBytes(2); processBytes(2); break; case CP_String: processBytes(2); break; case CP_Integer: case CP_Float: processBytes(4); break; case CP_Long: case CP_Double: processBytes(8); break; case CP_NameAndType: processBytes(4); break; case CP_Utf8: { int len = processUnsignedShort(); final byte[] utf8string = processBytes(len); UTF8ConstantPoolEntries[index] = new String(utf8string, "UTF8"); break; } default: assert false; break; } return tag; } /** * Process i's method info. * * @param i An index number of the current method info to be processed. */ private void processMethodInfo(int i) throws IOException { // access flags(u2) int flag = processUnsignedShort(); //name_index(u2) int name_index = processUnsignedShort(); String mname = UTF8ConstantPoolEntries[name_index]; //descriptor_index(u2) int desc_index = processUnsignedShort(); String mdesc = UTF8ConstantPoolEntries[desc_index]; //attributes_count(u2) int numMethodAttributes = processUnsignedShort(); if (bRemapLineNumberTable) { for(int j = 0; j < numMethodAttributes; j++) { int attr_name_index = processUnsignedShort(); String attr_name = UTF8ConstantPoolEntries[attr_name_index]; if (attr_name.equals(ANAME_CODE)) { processMethodCodeAttribute(attr_name_index, mname, mdesc); } else { processBytes(processInt()); } } } else { for(int j = 0; j < numMethodAttributes; j++) { processAttribute(); } } } /** * Handle a line number table for each method. * * @param mname A method name. * @param mdesc A method descriptor. */ private void processLineNumberTable(String mname, String mdesc) throws IOException { //read old table int ilen = is.readInt(); int inum_table_entries = is.readUnsignedShort(); LineNumberTableEntry[] oldTable = new LineNumberTableEntry[inum_table_entries]; for(int i =0; i < inum_table_entries;i++){ int pc = is.readUnsignedShort(); int line = is.readUnsignedShort(); oldTable[i] = new LineNumberTableEntry(pc, line); } //write new table LineNumberTableEntry[] newTable = adjustLineNumberTable(oldTable, mname, mdesc); assert (newTable.length == oldTable.length); int onum_table_entries = newTable.length; int oAttributelen = 2 + onum_table_entries * (2+2); os.writeInt(oAttributelen); os.writeShort(onum_table_entries); for(int i =0; i < onum_table_entries;i++) { int pc = newTable[i].getStartPC(); int line = newTable[i].getLineNumber(); os.writeShort(pc); os.writeShort(line); } } /** * Handle SourceFile attribute. */ private void processSourceFile() throws IOException { //read old attribute int ilen = is.readInt(); int iSourceFileIndex = is.readUnsignedShort(); String oldSourceFileName = UTF8ConstantPoolEntries[iSourceFileIndex]; assert (ilen == 2) && constantPoolEntryIndexForInputSourceFileName >= 1; //write new attribute os.writeInt(2); os.writeShort(constantPoolEntryIndexForInputSourceFileName); } /** * Handle a method code attribute. * * @param attr_name_index A constant pool index for UTF8 attribute index. * @param mname A method name. * @param mdesc A method signature. */ private void processMethodCodeAttribute(int attr_name_index, String mname, String mdesc) throws IOException { assert UTF8ConstantPoolEntries[attr_name_index].equals(ANAME_CODE); //u4 attribute_length int codeAttrLen = processInt(); //u2 max_stack int max_stack = processUnsignedShort(); //u2 max_locals int max_locals = processUnsignedShort(); //u4 code_length;u1 code[code_length]; int code_length = processInt(); processBytes(code_length); //u2 exception_table_length; exception_table[exception_table_length] int ex_length = processUnsignedShort(); processBytes(8 * ex_length); int nested_attr_count = processUnsignedShort(); for(int i = 0;i < nested_attr_count;i++) { int name_index = processUnsignedShort(); String attr_name = UTF8ConstantPoolEntries[name_index]; if (attr_name.equals(ANAME_LINENUMBERTABLE)) { processLineNumberTable(mname, mdesc); } else { processBytes(processInt()); } } } /** * Skip the current attribute section. */ private void processAttribute() throws IOException { processBytes(2); int i = processInt(); processBytes(i); } /** * Skip specified number of bytes. * * @param i A number of bytes to skip. * @return A byte array. */ private byte[] processBytes(int i) throws IOException { byte buf[] = new byte[i]; int j = is.read(buf); assert j == i; os.write(buf); return buf; } /** * Skip 4 bytes for intger value. * * @return An integer value. */ private int processInt() throws IOException { int i = is.readInt(); os.writeInt(i); return i; } /** * Skip two bytes for the signed short. * * @return A signed short value. */ private short processShort() throws IOException { short word0 = is.readShort(); os.writeShort(word0); return word0; } /** * Skip 2 bytes for unsigned shour value. * * @return An unsighed short value. */ private int processUnsignedShort() throws IOException { int i = is.readUnsignedShort(); os.writeShort(i); return i; } /** * Skip 1 byte. * * @return A byte value. */ private byte processByte() throws IOException { byte byte0 = is.readByte(); os.writeByte(byte0); return byte0; } }