diff --git a/BCV Icon.ico b/BCV Icon.ico new file mode 100644 index 00000000..5ae06e44 Binary files /dev/null and b/BCV Icon.ico differ diff --git a/BytecodeViewer 2.9.0.jar b/BytecodeViewer 2.9.0.jar new file mode 100644 index 00000000..e91f7e58 Binary files /dev/null and b/BytecodeViewer 2.9.0.jar differ diff --git a/BytecodeViewer 2.8.1.jar b/BytecodeViewer.exe similarity index 73% rename from BytecodeViewer 2.8.1.jar rename to BytecodeViewer.exe index 917c0ef0..5e8154bc 100644 Binary files a/BytecodeViewer 2.8.1.jar and b/BytecodeViewer.exe differ diff --git a/README.txt b/README.txt index bf15af16..20942699 100644 --- a/README.txt +++ b/README.txt @@ -49,7 +49,7 @@ Key Features: Java Decompiler - It utilizes FernFlower, Procyon and CFR for decompilation. Bytecode Decompiler - A modified version of CFIDE's. Hex Viewer - Powered by JHexPane. - Each Decompiler/Viewer is toggleable, you can also select what will display on each pane. + Each Decompiler/Editor/Viewer is toggleable, you can also select what will display on each pane. Fully Featured Search System - Search through strings, functions, variables and more! A Plugin System With Built In Plugins - (Show All Strings, Malicious Code Scanner, String Decrypters, etc) Fully Featured Scripting System That Supports Groovy. @@ -292,4 +292,30 @@ Changelog: 02/04/2015 - Added Krakatau Assembly. --- 2.8.1 ---: 02/04/2015 - Fixed UI bug with Krakatau/Krakatau Editable view panes. -02/05/2015 - Added CTRL + F. \ No newline at end of file +02/05/2015 - Added CTRL + F. +--- 2.9.0 ---: +02/11/2015 - Added ZStringArray String Decrypter. (Thanks Righteous) +02/20/2015 - Moved the decompilers/disassemblers around. +02/20/2015 - Fixed a resource leak with Krakatau Decompiler/Disassembler/Assembler. +02/21/2015 - Fixed regex searching if your regex search contained a syntax error. +02/21/2015 - Added the compiler/decompiler instances to the BytecodeViewer API class. +02/21/2015 - Sped up the decompilers, each view pane runs its own decompiler thread. +02/21/2015 - Added Janino compiler, you can now compile the decompiled source code inside of BCV. +02/21/2015 - Added the editable option for almost all of the decompilers/disassemblers. +02/21/2015 - Cached the next/previous icons and added a resources class for all resources. +01/21/2015 - Renamed EZ-Injection as File-Run, however kept the plugin named EZ-Injection. +02/21/2015 - Dropped Groovy support, added .Java plugin compilation instead (now only 10mb). +02/21/2015 - Added support for reading resources, including displaying images, detecting pure ascii files and more. +02/21/2015 - Fixed an issue with loading an already selected node in the file navigation pane. +02/22/2015 - Added an error console to the Java compiler +02/22/2015 - Ensured the spawned Python/Krakatau processes are killed when closing BCV. +02/22/2015 - Made it more beginner friendly. +02/22/2015 - Fixed? The file navigation search. +02/22/2015 - Added a shit ton more comments to non-api related classes. +02/23/2015 - Added APK resources. +02/23/2015 - MORE ANDROID LOVE! Added APKTool.jar's decode. (Takes a while so it's a setting, also pumped the jar back to 16MB) +02/23/2015 - Added close all but this tab menu. +02/23/2015 - Not really code related, but added _install.bat and _uninstall.bat for the exe version of BCV. +02/23/2015 - Back to ASM5, packed dex2jar in its own obfuscated jar. +02/23/2015 - Added the annotations back to the Bytecode Decompiler. (Once again, thanks Bibl) +02/23/2015 - It once again works with Java 8 Jars. \ No newline at end of file diff --git a/_install BCV.bat b/_install BCV.bat new file mode 100644 index 00000000..123a2700 --- /dev/null +++ b/_install BCV.bat @@ -0,0 +1,14 @@ +@echo off +assoc .class=BCV +assoc .apk=BCV +assoc .dex=BCV +ftype BCV="%CD%\BytecodeViewer.exe" "%%1" +echo. +echo. +echo Installed, .class, .apk and .dex will be associated with BytecodeViwer.exe +echo. +echo Note, if you move BytecodeViewer.exe +echo you'll need to re-run this program in the same directory as it. +echo. +echo. +pause \ No newline at end of file diff --git a/_uninstall BCV.bat b/_uninstall BCV.bat new file mode 100644 index 00000000..c14674ec --- /dev/null +++ b/_uninstall BCV.bat @@ -0,0 +1,10 @@ +@echo off +assoc .class= +assoc .apk= +assoc .dex= +echo. +echo. +echo Uninstalled, .class, .apk and .dex will no longer be associated. +echo. +echo. +pause \ No newline at end of file diff --git a/jar2exe_config.j2e b/jar2exe_config.j2e new file mode 100644 index 00000000..9e9d4538 --- /dev/null +++ b/jar2exe_config.j2e @@ -0,0 +1 @@ +"C:\Program Files\Jar2Exe Wizard\j2ewiz" /jar "C:\Users\null\Documents\GitHub\bytecode-viewer\BytecodeViewer 2.9.0.jar" /o C:\Users\null\Documents\GitHub\bytecode-viewer\BytecodeViewer.exe /m the.bytecode.club.bytecodeviewer.BytecodeViewer /type windows /minjre 1.7 /platform windows /checksum /icon "C:\Users\null\Documents\GitHub\bytecode-viewer\BCV Icon.ico, 0" /pv 1,0,0,1 /fv 1,0,0,1 /ve ProductVersion=1.9.0 /ve "ProductName=The Bytecode Club" /ve "LegalCopyright=Copyright (c) 2014 - 2015 Kalen (Konloch) Kinloch" /ve "SpecialBuild=1, 0, 0, 1" /ve FileVersion=1 /ve "FileDescription=Java Reverse Engineering Suite" /ve "LegalTrademarks=Trade marks" /ve "InternalName=1, 0, 0, 1" /ve "CompanyName=The Bytecode Club" \ No newline at end of file diff --git a/libs/apktool_2.0.0rc4_obf.jar b/libs/apktool_2.0.0rc4_obf.jar new file mode 100644 index 00000000..5a9ae651 Binary files /dev/null and b/libs/apktool_2.0.0rc4_obf.jar differ diff --git a/libs/asm-all-3.3.1.jar b/libs/asm-all-3.3.1.jar deleted file mode 100644 index 39a0d6db..00000000 Binary files a/libs/asm-all-3.3.1.jar and /dev/null differ diff --git a/libs/asm-all-5.0.3.jar b/libs/asm-all-5.0.3.jar new file mode 100644 index 00000000..da5b23e1 Binary files /dev/null and b/libs/asm-all-5.0.3.jar differ diff --git a/libs/commons-compiler-jdk.jar b/libs/commons-compiler-jdk.jar new file mode 100644 index 00000000..08958698 Binary files /dev/null and b/libs/commons-compiler-jdk.jar differ diff --git a/libs/commons-compiler.jar b/libs/commons-compiler.jar new file mode 100644 index 00000000..46cd6630 Binary files /dev/null and b/libs/commons-compiler.jar differ diff --git a/libs/dex-ir-1.12.jar b/libs/dex-ir-1.12.jar deleted file mode 100644 index d0e61c67..00000000 Binary files a/libs/dex-ir-1.12.jar and /dev/null differ diff --git a/libs/dex-reader-1.15.jar b/libs/dex-reader-1.15.jar deleted file mode 100644 index d527f97a..00000000 Binary files a/libs/dex-reader-1.15.jar and /dev/null differ diff --git a/libs/dex-tools-0.0.9.15.jar b/libs/dex-tools-0.0.9.15.jar deleted file mode 100644 index 12002678..00000000 Binary files a/libs/dex-tools-0.0.9.15.jar and /dev/null differ diff --git a/libs/dex-translator-0.0.9.15.jar b/libs/dex-translator-0.0.9.15.jar deleted file mode 100644 index 46bc3806..00000000 Binary files a/libs/dex-translator-0.0.9.15.jar and /dev/null differ diff --git a/libs/dex_obf.jar b/libs/dex_obf.jar new file mode 100644 index 00000000..01bb7e9d Binary files /dev/null and b/libs/dex_obf.jar differ diff --git a/libs/dx.jar b/libs/dx.jar deleted file mode 100644 index fd011921..00000000 Binary files a/libs/dx.jar and /dev/null differ diff --git a/libs/groovy-all-2.1.7.jar b/libs/groovy-all-2.1.7.jar deleted file mode 100644 index e24f3005..00000000 Binary files a/libs/groovy-all-2.1.7.jar and /dev/null differ diff --git a/plugins/EldevinStringDecrypter.gy b/plugins/EldevinStringDecrypter.gy new file mode 100644 index 00000000..2f7e9717 --- /dev/null +++ b/plugins/EldevinStringDecrypter.gy @@ -0,0 +1,49 @@ +import the.bytecode.club.bytecodeviewer.api.*; +import java.util.ArrayList; +import org.objectweb.asm.tree.ClassNode; +import javax.swing.JDialog; +import javax.swing.JOptionPane; +import java.lang.reflect.Field; +import org.objectweb.asm.tree.FieldNode; + +public class EldevinStringDecrypter extends Plugin { + + @Override + public void execute(ArrayList classNodesList) { + PluginConsole gui = new PluginConsole("Eldevin String Decrypter"); + + JOptionPane pane = new JOptionPane("WARNING: This method of decryption loads the classes into a classloader and executes the init function, this could lead to malicious code executing.\n\r\n\rAre you sure you want to run this plugin?"); + String[] options = ["Yes", "No"]; + pane.setOptions(options); + JDialog dialog = pane.createDialog(the.bytecode.club.bytecodeviewer.BytecodeViewer.viewer, "WARNING"); + dialog.show(); + Object obj = pane.getValue(); + int result = -1; + for (int k = 0; k < options.length; k++) + if (options[k].equals(obj)) + result = k; + + + if(result == 0) { + for(ClassNode cn : classNodesList) { + the.bytecode.club.bytecodeviewer.api.BytecodeViewer.getClassNodeLoader().addClass(cn); + + for(Object o : cn.fields.toArray()) { + FieldNode f = (FieldNode) o; + if(f.name.equals("z")) {// && f.desc.equals("([Ljava/lang/String;)V")) { + try { + for(Field f2 : the.bytecode.club.bytecodeviewer.api.BytecodeViewer.getClassNodeLoader().nodeToClass(cn).getFields()) { + String s = f2.get(null); + if(s != null && !s.empty()) + gui.appendText(cn+":"+s); + } + } catch(Exception | StackOverflowError e) {} + } + } + + } + + gui.setVisible(true); + } + } +} \ No newline at end of file diff --git a/plugins/Skeleton.rb b/plugins/Skeleton.rb new file mode 100644 index 00000000..1c587ab5 --- /dev/null +++ b/plugins/Skeleton.rb @@ -0,0 +1,15 @@ +require 'java' + +java_import 'the.bytecode.club.bytecodeviewer.api.Plugin' +java_import 'the.bytecode.club.bytecodeviewer.api.PluginConsole' +java_import 'java.lang.System' +java_import 'java.util.ArrayList' +java_import 'org.objectweb.asm.tree.ClassNode' + +class Skeleton < Plugin + def execute(classNodeList) + gui = PluginConsole.new "Skeleton" + gui.setVisible(true) + gui.appendText("exceuted skeleton") + end +end \ No newline at end of file diff --git a/plugins/Test.gy b/plugins/Test.gy new file mode 100644 index 00000000..1f3b25d8 --- /dev/null +++ b/plugins/Test.gy @@ -0,0 +1,14 @@ +import the.bytecode.club.bytecodeviewer.api.*; +import java.util.ArrayList; +import org.objectweb.asm.tree.ClassNode; +import the.bytecode.club.bytecodeviewer.decompilers.*; + +public class Test extends Plugin { + + @Override + public void execute(ArrayList classNodesList) { + PluginConsole gui = new PluginConsole("Skeleton"); + gui.setVisible(true); + gui.appendText(Smali.decompileClassNode(the.bytecode.club.bytecodeviewer.BytecodeViewer.viewer.workPane.getCurrentClass().cn)); + } +} \ No newline at end of file diff --git a/plugins/skeleton.py b/plugins/skeleton.py new file mode 100644 index 00000000..227870aa --- /dev/null +++ b/plugins/skeleton.py @@ -0,0 +1,13 @@ +from the.bytecode.club.bytecodeviewer.api import Plugin +from the.bytecode.club.bytecodeviewer.api import PluginConsole +from java.lang import System +from java.lang import Boolean +from java.util import ArrayList +from org.objectweb.asm.tree import ClassNode + +class skeleton(Plugin): + + def execute(classNodeList, poop): #for some reason it requires a second arg + gui = PluginConsole("Skeleton") + gui.setVisible(Boolean.TRUE) + gui.appendText("exceuted skeleton") diff --git a/src/org/codehaus/janino/Access.java b/src/org/codehaus/janino/Access.java new file mode 100644 index 00000000..ab6310b3 --- /dev/null +++ b/src/org/codehaus/janino/Access.java @@ -0,0 +1,56 @@ + +/* + * Janino - An embedded Java[TM] compiler + * + * Copyright (c) 2001-2010, Arno Unkrig + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the + * following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the + * following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the + * following disclaimer in the documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package org.codehaus.janino; + +import org.codehaus.janino.util.enumerator.Enumerator; +import org.codehaus.janino.util.enumerator.EnumeratorFormatException; + +/** Return value for {@link IClass.IMember#getAccess}. */ +public final +class Access extends Enumerator { + + /** Representation of PRIVATE accessibility. */ + public static final Access PRIVATE = new Access("private"); + + /** Representation of PROTECTED accessibility. */ + public static final Access PROTECTED = new Access("protected"); + + /** Representation of DEFAULT accessibility. */ + public static final Access DEFAULT = new Access("/*default*/"); + + /** Representation of PUBLIC accessibility. */ + public static final Access PUBLIC = new Access("public"); + + // These MUST be declared exactly like this: + private Access(String name) { super(name); } + + /** @return The {@code name} converted to {@link Access} */ + public static Access + fromString(String name) throws EnumeratorFormatException { + return (Access) Enumerator.fromString(name, Access.class); + } +} diff --git a/src/org/codehaus/janino/ByteArrayClassLoader.java b/src/org/codehaus/janino/ByteArrayClassLoader.java new file mode 100644 index 00000000..c6f3acd6 --- /dev/null +++ b/src/org/codehaus/janino/ByteArrayClassLoader.java @@ -0,0 +1,74 @@ + +/* + * Janino - An embedded Java[TM] compiler + * + * Copyright (c) 2001-2010, Arno Unkrig + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the + * following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the + * following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the + * following disclaimer in the documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package org.codehaus.janino; + +import java.util.Map; + +/** This {@link ClassLoader} allows for the loading of a set of Java™ classes provided in class file format. */ +@SuppressWarnings({ "rawtypes", "unchecked" }) public +class ByteArrayClassLoader extends ClassLoader { + + /** + * The given {@link Map} of classes must not be modified afterwards. + * + * @param classes String className => byte[] data + */ + public + ByteArrayClassLoader(Map classes) { this.classes = classes; } + + /** @see #ByteArrayClassLoader(Map) */ + public + ByteArrayClassLoader(Map classes, ClassLoader parent) { + super(parent); + this.classes = classes; + } + + /** + * Implements {@link ClassLoader#findClass(String)}. + *

+ * Notice that, although nowhere documented, no more than one thread at a time calls this + * method, because {@link ClassLoader#loadClass(java.lang.String)} is + * synchronized. + */ + @Override protected Class + findClass(String name) throws ClassNotFoundException { + byte[] data = (byte[]) this.classes.get(name); + if (data == null) throw new ClassNotFoundException(name); + + // Notice: Not inheriting the protection domain will cause problems with Java Web Start / + // JNLP. See + // http://jira.codehaus.org/browse/JANINO-104 + // http://www.nabble.com/-Help-jel--java.security.AccessControlException-to13073723.html + return super.defineClass( + name, // name + data, 0, data.length, // b, off, len + this.getClass().getProtectionDomain() // protectionDomain + ); + } + + private final Map classes; +} diff --git a/src/org/codehaus/janino/CachingJavaSourceClassLoader.java b/src/org/codehaus/janino/CachingJavaSourceClassLoader.java new file mode 100644 index 00000000..9e94e307 --- /dev/null +++ b/src/org/codehaus/janino/CachingJavaSourceClassLoader.java @@ -0,0 +1,218 @@ + +/* + * Janino - An embedded Java[TM] compiler + * + * Copyright (c) 2001-2010, Arno Unkrig + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the + * following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the + * following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the + * following disclaimer in the documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package org.codehaus.janino; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; + +import org.codehaus.janino.util.ClassFile; +import org.codehaus.janino.util.resource.DirectoryResourceCreator; +import org.codehaus.janino.util.resource.DirectoryResourceFinder; +import org.codehaus.janino.util.resource.PathResourceFinder; +import org.codehaus.janino.util.resource.Resource; +import org.codehaus.janino.util.resource.ResourceCreator; +import org.codehaus.janino.util.resource.ResourceFinder; + +/** + * A {@link org.codehaus.janino.JavaSourceClassLoader} that uses a resource storage provided by the application to cache + * compiled classes and thus saving unnecessary recompilations. + *

+ * The application provides access to the resource storeage through a pair of a {@link + * org.codehaus.janino.util.resource.ResourceFinder} and a {@link org.codehaus.janino.util.resource.ResourceCreator} + * (see {@link #CachingJavaSourceClassLoader(ClassLoader, ResourceFinder, String, ResourceFinder, ResourceCreator)}. + *

+ * See {@link org.codehaus.janino.JavaSourceClassLoader#main(String[])} for an example how to use this class. + *

+ * Notice: You must NOT rely on that this class stores some particular data in some particular resources through + * the given {@code classFileCacheResourceFinder/Creator}! These serve only as a means for the {@link + * CachingJavaSourceClassLoader} to persistently cache some data between invocations. In other words: If you want to + * compile {@code .java} files into {@code .class} files, then don't use this class but {@link Compiler} + * instead! + */ +@SuppressWarnings({ "rawtypes", "unchecked" }) public +class CachingJavaSourceClassLoader extends JavaSourceClassLoader { + private final ResourceFinder classFileCacheResourceFinder; + private final ResourceCreator classFileCacheResourceCreator; + private final ResourceFinder sourceFinder; + + /** + * See {@link #CachingJavaSourceClassLoader(ClassLoader, ResourceFinder, String, ResourceFinder, ResourceCreator)}. + * + * @param optionalSourcePath Directories to scan for source files + * @param cacheDirectory Directory to use for caching generated class files (see class description) + */ + public + CachingJavaSourceClassLoader( + ClassLoader parentClassLoader, + File[] optionalSourcePath, + String optionalCharacterEncoding, + File cacheDirectory + ) { + this( + parentClassLoader, // parentClassLoader + ( // sourceFinder + optionalSourcePath == null + ? (ResourceFinder) new DirectoryResourceFinder(new File(".")) + : (ResourceFinder) new PathResourceFinder(optionalSourcePath) + ), + optionalCharacterEncoding, // optionalCharacterEncoding + new DirectoryResourceFinder(cacheDirectory), // classFileCacheResourceFinder + new DirectoryResourceCreator(cacheDirectory) // classFileCacheResourceCreator + ); + } + + /** + * Notice that this class is thread-safe if and only if the {@code classFileCacheResourceCreator} stores its data + * atomically, i.e. the {@code classFileCacheResourceFinder} sees the resource written by the {@code + * classFileCacheResourceCreator} only after the {@link OutputStream} is closed. + *

+ * In order to make the caching scheme work, both the {@code classFileCacheResourceFinder} and the {@code + * sourceFinder} must support the {@link org.codehaus.janino.util.resource.Resource#lastModified()} method, so that + * the modification time of the source and the class files can be compared. + * + * @param parentClassLoader Attempt to load classes through this one before looking for source files + * @param sourceFinder Finds Java™ source for class {@code pkg.Cls} in resource {@code + * pkg/Cls.java} + * @param optionalCharacterEncoding Encoding of Java™ source or {@code null} for platform default + * encoding + * @param classFileCacheResourceFinder Finds precompiled class {@code pkg.Cls} in resource {@code pkg/Cls.class} + * (see class description) + * @param classFileCacheResourceCreator Stores compiled class {@code pkg.Cls} in resource {@code pkg/Cls.class} (see + * class description) + */ + public + CachingJavaSourceClassLoader( + ClassLoader parentClassLoader, + ResourceFinder sourceFinder, + String optionalCharacterEncoding, + ResourceFinder classFileCacheResourceFinder, + ResourceCreator classFileCacheResourceCreator + ) { + super(parentClassLoader, sourceFinder, optionalCharacterEncoding); + this.classFileCacheResourceFinder = classFileCacheResourceFinder; + this.classFileCacheResourceCreator = classFileCacheResourceCreator; + this.sourceFinder = sourceFinder; + } + + /** + * Override {@link JavaSourceClassLoader#generateBytecodes(String)} to implement class file caching. + * + * @return String name => byte[] bytecode, or {@code null} if no source code could be found + * @throws ClassNotFoundException Compilation problems or class file cache I/O problems + */ + @Override protected Map + generateBytecodes(String className) throws ClassNotFoundException { + // Check whether a class file resource exists in the cache. + { + Resource classFileResource = this.classFileCacheResourceFinder.findResource( + ClassFile.getClassFileResourceName(className) + ); + if (classFileResource != null) { + + // Check whether a source file resource exists. + Resource sourceResource = this.sourceFinder.findResource(ClassFile.getSourceResourceName(className)); + if (sourceResource == null) return null; + + // Check whether the class file is up-to-date. + if (sourceResource.lastModified() < classFileResource.lastModified()) { + + // Yes, it is... read the bytecode from the file and define the class. + byte[] bytecode; + try { + bytecode = CachingJavaSourceClassLoader.readResource(classFileResource); + } catch (IOException ex) { + throw new ClassNotFoundException("Reading class file from \"" + classFileResource + "\"", ex); + } + Map m = new HashMap(); + m.put(className, bytecode); + return m; + } + } + } + + // Cache miss... generate the bytecode from source. + Map bytecodes = super.generateBytecodes(className); + if (bytecodes == null) return null; + + // Write the generated bytecodes to the class file cache. + for (Map.Entry me : bytecodes.entrySet()) { + String className2 = (String) me.getKey(); + byte[] bytecode = (byte[]) me.getValue(); + + try { + CachingJavaSourceClassLoader.writeResource( + this.classFileCacheResourceCreator, + ClassFile.getClassFileResourceName(className2), + bytecode + ); + } catch (IOException ex) { + throw new ClassNotFoundException( + "Writing class file to \"" + ClassFile.getClassFileResourceName(className2) + "\"", + ex + ); + } + } + + return bytecodes; + } + + /** Reads all bytes from the given resource. */ + private static byte[] + readResource(Resource r) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + + InputStream is = r.open(); + try { + for (;;) { + int cnt = is.read(buffer); + if (cnt == -1) break; + baos.write(buffer, 0, cnt); + } + } finally { + try { is.close(); } catch (IOException ex) {} + } + + return baos.toByteArray(); + } + + /** Create a resource with the given name and store the data in it. */ + private static void + writeResource(ResourceCreator resourceCreator, String resourceName, byte[] data) throws IOException { + OutputStream os = resourceCreator.createResource(resourceName); + try { + os.write(data); + } finally { + try { os.close(); } catch (IOException ex) {} + } + } +} diff --git a/src/org/codehaus/janino/ClassBodyEvaluator.java b/src/org/codehaus/janino/ClassBodyEvaluator.java new file mode 100644 index 00000000..04efc2f2 --- /dev/null +++ b/src/org/codehaus/janino/ClassBodyEvaluator.java @@ -0,0 +1,445 @@ + +/* + * Janino - An embedded Java[TM] compiler + * + * Copyright (c) 2001-2010, Arno Unkrig + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the + * following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the + * following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the + * following disclaimer in the documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package org.codehaus.janino; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; + +import org.codehaus.commons.compiler.CompileException; +import org.codehaus.commons.compiler.CompilerFactoryFactory; +import org.codehaus.commons.compiler.Cookable; +import org.codehaus.commons.compiler.IClassBodyEvaluator; +import org.codehaus.commons.compiler.ICompilerFactory; +import org.codehaus.commons.compiler.Location; + +/** + * The optionalClassLoader serves two purposes: + *

    + *
  • It is used to look for classes referenced by the class body. + *
  • It is used to load the generated Java™ class + * into the JVM; directly if it is a subclass of {@link + * ByteArrayClassLoader}, or by creation of a temporary + * {@link ByteArrayClassLoader} if not. + *
+ * A number of "convenience constructors" exist that execute the setup steps instantly. + */ +@SuppressWarnings("rawtypes") public +class ClassBodyEvaluator extends SimpleCompiler implements IClassBodyEvaluator { + + private static final Class[] ZERO_CLASSES = new Class[0]; + + private String[] optionalDefaultImports; + private String className = IClassBodyEvaluator.DEFAULT_CLASS_NAME; + private Class optionalExtendedType; + private Class[] implementedTypes = ClassBodyEvaluator.ZERO_CLASSES; + private Class result; // null=uncooked + + /** + * Equivalent to
+     * ClassBodyEvaluator cbe = new ClassBodyEvaluator();
+     * cbe.cook(classBody);
+ * + * @see #ClassBodyEvaluator() + * @see Cookable#cook(String) + */ + public + ClassBodyEvaluator(String classBody) throws CompileException { this.cook(classBody); } + + /** + * Equivalent to
+     * ClassBodyEvaluator cbe = new ClassBodyEvaluator();
+     * cbe.cook(optionalFileName, is);
+ * + * @see #ClassBodyEvaluator() + * @see Cookable#cook(String, InputStream) + */ + public + ClassBodyEvaluator(String optionalFileName, InputStream is) throws CompileException, IOException { + this.cook(optionalFileName, is); + } + + /** + * Equivalent to
+     * ClassBodyEvaluator cbe = new ClassBodyEvaluator();
+     * cbe.cook(optionalFileName, reader);
+ * + * @see #ClassBodyEvaluator() + * @see Cookable#cook(String, Reader) + */ + public + ClassBodyEvaluator(String optionalFileName, Reader reader) throws CompileException, IOException { + this.cook(optionalFileName, reader); + } + + /** + * Equivalent to
+     * ClassBodyEvaluator cbe = new ClassBodyEvaluator();
+     * cbe.setParentClassLoader(optionalParentClassLoader);
+     * cbe.cook(scanner);
+ * + * @see #ClassBodyEvaluator() + * @see SimpleCompiler#setParentClassLoader(ClassLoader) + * @see Cookable#cook(Reader) + */ + public + ClassBodyEvaluator(Scanner scanner, ClassLoader optionalParentClassLoader) throws CompileException, IOException { + this.setParentClassLoader(optionalParentClassLoader); + this.cook(scanner); + } + + /** + * Equivalent to
+     * ClassBodyEvaluator cbe = new ClassBodyEvaluator();
+     * cbe.setExtendedType(optionalExtendedType);
+     * cbe.setImplementedTypes(implementedTypes);
+     * cbe.setParentClassLoader(optionalParentClassLoader);
+     * cbe.cook(scanner);
+ * + * @see #ClassBodyEvaluator() + * @see #setExtendedClass(Class) + * @see #setImplementedInterfaces(Class[]) + * @see SimpleCompiler#setParentClassLoader(ClassLoader) + * @see Cookable#cook(Reader) + */ + public + ClassBodyEvaluator( + Scanner scanner, + Class optionalExtendedType, + Class[] implementedTypes, + ClassLoader optionalParentClassLoader + ) throws CompileException, IOException { + this.setExtendedClass(optionalExtendedType); + this.setImplementedInterfaces(implementedTypes); + this.setParentClassLoader(optionalParentClassLoader); + this.cook(scanner); + } + + /** + * Equivalent to
+     * ClassBodyEvaluator cbe = new ClassBodyEvaluator();
+     * cbe.setClassName(className);
+     * cbe.setExtendedType(optionalExtendedType);
+     * cbe.setImplementedTypes(implementedTypes);
+     * cbe.setParentClassLoader(optionalParentClassLoader);
+     * cbe.cook(scanner);
+ * + * @see #ClassBodyEvaluator() + * @see #setClassName(String) + * @see #setExtendedClass(Class) + * @see #setImplementedInterfaces(Class[]) + * @see SimpleCompiler#setParentClassLoader(ClassLoader) + * @see Cookable#cook(Reader) + */ + public + ClassBodyEvaluator( + Scanner scanner, + String className, + Class optionalExtendedType, + Class[] implementedTypes, + ClassLoader optionalParentClassLoader + ) throws CompileException, IOException { + this.setClassName(className); + this.setExtendedClass(optionalExtendedType); + this.setImplementedInterfaces(implementedTypes); + this.setParentClassLoader(optionalParentClassLoader); + this.cook(scanner); + } + + public ClassBodyEvaluator() {} + + @Override public void + setDefaultImports(String[] optionalDefaultImports) { + this.assertNotCooked(); + this.optionalDefaultImports = optionalDefaultImports; + } + + @Override public void + setClassName(String className) { + if (className == null) throw new NullPointerException(); + this.assertNotCooked(); + this.className = className; + } + + @Override public void + setExtendedClass(Class optionalExtendedType) { + this.assertNotCooked(); + this.optionalExtendedType = optionalExtendedType; + } + + /** @deprecated */ + @Deprecated @Override public void + setExtendedType(Class optionalExtendedClass) { + this.setExtendedClass(optionalExtendedClass); + } + + @Override public void + setImplementedInterfaces(Class[] implementedTypes) { + if (implementedTypes == null) { + throw new NullPointerException( + "Zero implemented types must be specified as 'new Class[0]', not 'null'" + ); + } + this.assertNotCooked(); + this.implementedTypes = implementedTypes; + } + + /** @deprecated */ + @Deprecated @Override public void + setImplementedTypes(Class[] implementedInterfaces) { + this.setImplementedInterfaces(implementedInterfaces); + } + + @Override public void + cook(Scanner scanner) throws CompileException, IOException { + + Parser parser = new Parser(scanner); + Java.CompilationUnit compilationUnit = this.makeCompilationUnit(parser); + + // Add class declaration. + Java.ClassDeclaration cd = this.addPackageMemberClassDeclaration(scanner.location(), compilationUnit); + + // Parse class body declarations (member declarations) until EOF. + while (!parser.peekEof()) { + parser.parseClassBodyDeclaration(cd); + } + + // Compile and load it. + this.result = this.compileToClass(compilationUnit); + } + + /** + * Create a {@link Java.CompilationUnit}, set the default imports, and parse the import declarations. + *

+ * If the optionalParser is given, a sequence of IMPORT directives is parsed from it and added to the + * compilation unit. + */ + protected final Java.CompilationUnit + makeCompilationUnit(Parser optionalParser) throws CompileException, IOException { + Java.CompilationUnit cu = ( + new Java.CompilationUnit(optionalParser == null + ? null + : optionalParser.getScanner().getFileName()) + ); + + // Set default imports. + if (this.optionalDefaultImports != null) { + for (String defaultImport : this.optionalDefaultImports) { + Scanner s = new Scanner(null, new StringReader(defaultImport)); + Parser parser2 = new Parser(s); + cu.addImportDeclaration(parser2.parseImportDeclarationBody()); + if (!parser2.peekEof()) { + throw new CompileException( + "Unexpected token '" + parser2.peek() + "' in default import", + s.location() + ); + } + } + } + + // Parse all available IMPORT declarations. + if (optionalParser != null) { + while (optionalParser.peek("import")) { + cu.addImportDeclaration(optionalParser.parseImportDeclaration()); + } + } + + return cu; + } + + /** + * To the given {@link Java.CompilationUnit}, add + *

    + *
  • A class declaration with the configured name, superclass and interfaces + *
  • A method declaration with the given return type, name, parameter names and values and thrown exceptions + *
+ * + * @return The created {@link Java.ClassDeclaration} object + */ + protected Java.PackageMemberClassDeclaration + addPackageMemberClassDeclaration(Location location, Java.CompilationUnit compilationUnit) throws CompileException { + String cn = this.className; + int idx = cn.lastIndexOf('.'); + if (idx != -1) { + compilationUnit.setPackageDeclaration(new Java.PackageDeclaration(location, cn.substring(0, idx))); + cn = cn.substring(idx + 1); + } + Java.PackageMemberClassDeclaration tlcd = new Java.PackageMemberClassDeclaration( + location, // location + null, // optionalDocComment + new Java.Modifiers(Mod.PUBLIC), // modifiers + cn, // name + null, // optionalTypeParameters + this.classToType(location, this.optionalExtendedType), // optionalExtendedType + this.classesToTypes(location, this.implementedTypes) // implementedTypes + ); + compilationUnit.addPackageMemberTypeDeclaration(tlcd); + return tlcd; + } + + /** + * Compile the given compilation unit, load all generated classes, and return the class with the given name. + * + * @param compilationUnit + * @return The loaded class + */ + protected final Class + compileToClass(Java.CompilationUnit compilationUnit) throws CompileException { + + // Compile and load the compilation unit. + ClassLoader cl = this.compileToClassLoader(compilationUnit); + + // Find the generated class by name. + try { + return cl.loadClass(this.className); + } catch (ClassNotFoundException ex) { + throw new JaninoRuntimeException(( + "SNO: Generated compilation unit does not declare class '" + + this.className + + "'" + ), ex); + } + } + + @Override public Class + getClazz() { + if (this.getClass() != ClassBodyEvaluator.class) { + throw new IllegalStateException("Must not be called on derived instances"); + } + if (this.result == null) throw new IllegalStateException("Must only be called after 'cook()'"); + return this.result; + } + + @Override public Object + createInstance(Reader reader) throws CompileException, IOException { + this.cook(reader); + + try { + return this.getClazz().newInstance(); + } catch (InstantiationException ie) { + CompileException ce = new CompileException(( + "Class is abstract, an interface, an array class, a primitive type, or void; " + + "or has no zero-parameter constructor" + ), null); + ce.initCause(ie); + throw ce; // SUPPRESS CHECKSTYLE AvoidHidingCause + } catch (IllegalAccessException iae) { + CompileException ce = new CompileException( + "The class or its zero-parameter constructor is not accessible", + null + ); + ce.initCause(iae); + throw ce; // SUPPRESS CHECKSTYLE AvoidHidingCause + } + } + + /** + * Use {@link #createInstance(Reader)} instead: + *
+     * IClassBodyEvaluator cbe = {@link CompilerFactoryFactory}.{@link
+     * CompilerFactoryFactory#getDefaultCompilerFactory() getDefaultCompilerFactory}().{@link
+     * ICompilerFactory#newClassBodyEvaluator() newClassBodyEvaluator}();
+     * if (optionalBaseType != null) {
+     *     if (optionalBaseType.isInterface()) {
+     *         cbe.{@link #setImplementedInterfaces setImplementedInterfaces}(new Class[] { optionalBaseType });
+     *     } else {
+     *         cbe.{@link #setExtendedClass(Class) setExtendedClass}(optionalBaseType);
+     *     }
+     * }
+     * cbe.{@link #setParentClassLoader(ClassLoader) setParentClassLoader}(optionalParentClassLoader);
+     * cbe.{@link IClassBodyEvaluator#createInstance(Reader) createInstance}(reader);
+     * 
+ * + * @see #createInstance(Reader) + */ + public static Object + createFastClassBodyEvaluator( + Scanner scanner, + Class optionalBaseType, + ClassLoader optionalParentClassLoader + ) throws CompileException, IOException { + return ClassBodyEvaluator.createFastClassBodyEvaluator( + scanner, // scanner + IClassBodyEvaluator.DEFAULT_CLASS_NAME, // className + ( // optionalExtendedType + optionalBaseType != null && !optionalBaseType.isInterface() + ? optionalBaseType + : null + ), + ( // implementedTypes + optionalBaseType != null && optionalBaseType.isInterface() + ? new Class[] { optionalBaseType } + : new Class[0] + ), + optionalParentClassLoader // optionalParentClassLoader + ); + } + + /** + * Use {@link #createInstance(Reader)} instead: + *
+     * IClassBodyEvaluator cbe = {@link CompilerFactoryFactory}.{@link
+     * CompilerFactoryFactory#getDefaultCompilerFactory() getDefaultCompilerFactory}().{@link
+     * ICompilerFactory#newClassBodyEvaluator() newClassBodyEvaluator}();
+     * cbe.{@link #setExtendedClass(Class) setExtendedClass}(optionalExtendedClass);
+     * cbe.{@link #setImplementedInterfaces(Class[]) setImplementedInterfaces}(implementedInterfaces);
+     * cbe.{@link #setParentClassLoader(ClassLoader) setParentClassLoader}(optionalParentClassLoader);
+     * cbe.{@link IClassBodyEvaluator#createInstance(Reader) createInstance}(reader);
+     * 
+ * + * @see #createInstance(Reader) + * @deprecated Use {@link #createInstance(Reader)} instead. + */ + @Deprecated public static Object + createFastClassBodyEvaluator( + Scanner scanner, + String className, + Class optionalExtendedClass, + Class[] implementedInterfaces, + ClassLoader optionalParentClassLoader + ) throws CompileException, IOException { + ClassBodyEvaluator cbe = new ClassBodyEvaluator(); + cbe.setClassName(className); + cbe.setExtendedClass(optionalExtendedClass); + cbe.setImplementedInterfaces(implementedInterfaces); + cbe.setParentClassLoader(optionalParentClassLoader); + cbe.cook(scanner); + Class c = cbe.getClazz(); + try { + return c.newInstance(); + } catch (InstantiationException e) { + throw new CompileException( // SUPPRESS CHECKSTYLE AvoidHidingCause + e.getMessage(), + null + ); + } catch (IllegalAccessException e) { + // SNO - type and default constructor of generated class are PUBLIC. + throw new JaninoRuntimeException(e.toString(), e); + } + } +} diff --git a/src/org/codehaus/janino/ClassFileIClass.java b/src/org/codehaus/janino/ClassFileIClass.java new file mode 100644 index 00000000..c00abe37 --- /dev/null +++ b/src/org/codehaus/janino/ClassFileIClass.java @@ -0,0 +1,438 @@ + +/* + * Janino - An embedded Java[TM] compiler + * + * Copyright (c) 2001-2010, Arno Unkrig + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the + * following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the + * following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the + * following disclaimer in the documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package org.codehaus.janino; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.codehaus.commons.compiler.CompileException; +import org.codehaus.janino.util.ClassFile; +import org.codehaus.janino.util.ClassFile.ConstantClassInfo; + +/** A wrapper object that turns a {@link ClassFile} object into an {@link IClass}. */ +@SuppressWarnings({ "rawtypes", "unchecked" }) public +class ClassFileIClass extends IClass { + private static final boolean DEBUG = false; + + private final ClassFile classFile; + private final IClassLoader iClassLoader; + private final short accessFlags; + + private final Map resolvedFields = new HashMap(); + + /** + * @param classFile Source of data + * @param iClassLoader {@link IClassLoader} through which to load other classes + */ + public + ClassFileIClass(ClassFile classFile, IClassLoader iClassLoader) { + this.classFile = classFile; + this.iClassLoader = iClassLoader; + + // Determine class access flags. + this.accessFlags = classFile.accessFlags; + } + + // Implement IClass. + + @Override protected IConstructor[] + getDeclaredIConstructors2() { + List iConstructors = new ArrayList(); + + for (ClassFile.MethodInfo mi : this.classFile.methodInfos) { + IInvocable ii; + try { + ii = this.resolveMethod(mi); + } catch (ClassNotFoundException ex) { + throw new JaninoRuntimeException(ex.getMessage(), ex); + } + if (ii instanceof IConstructor) iConstructors.add(ii); + } + + return (IConstructor[]) iConstructors.toArray(new IConstructor[iConstructors.size()]); + } + + @Override protected IMethod[] + getDeclaredIMethods2() { + List iMethods = new ArrayList(); + + for (ClassFile.MethodInfo mi : this.classFile.methodInfos) { + + // Skip JDK 1.5 synthetic methods (e.g. those generated for + // covariant return values). + if (Mod.isSynthetic(mi.getModifierFlags())) continue; + + IInvocable ii; + try { + ii = this.resolveMethod(mi); + } catch (ClassNotFoundException ex) { + throw new JaninoRuntimeException(ex.getMessage(), ex); + } + if (ii instanceof IMethod) iMethods.add((IMethod) ii); + } + + return (IMethod[]) iMethods.toArray(new IMethod[iMethods.size()]); + } + + @Override protected IField[] + getDeclaredIFields2() { + IField[] ifs = new IClass.IField[this.classFile.fieldInfos.size()]; + for (int i = 0; i < this.classFile.fieldInfos.size(); ++i) { + try { + ifs[i] = this.resolveField((ClassFile.FieldInfo) this.classFile.fieldInfos.get(i)); + } catch (ClassNotFoundException ex) { + throw new JaninoRuntimeException(ex.getMessage(), ex); + } + } + return ifs; + } + + @Override protected IClass[] + getDeclaredIClasses2() throws CompileException { + ClassFile.InnerClassesAttribute ica = this.classFile.getInnerClassesAttribute(); + if (ica == null) return new IClass[0]; + + List res = new ArrayList(); + for (ClassFile.InnerClassesAttribute.Entry e : ica.getEntries()) { + if (e.outerClassInfoIndex == this.classFile.thisClass) { + try { + res.add(this.resolveClass(e.innerClassInfoIndex)); + } catch (ClassNotFoundException ex) { + throw new CompileException(ex.getMessage(), null); // SUPPRESS CHECKSTYLE AvoidHidingCause + } + } + } + return (IClass[]) res.toArray(new IClass[res.size()]); + } + + @Override protected IClass + getDeclaringIClass2() throws CompileException { + ClassFile.InnerClassesAttribute ica = this.classFile.getInnerClassesAttribute(); + if (ica == null) return null; + + for (ClassFile.InnerClassesAttribute.Entry e : ica.getEntries()) { + if (e.innerClassInfoIndex == this.classFile.thisClass) { + // Is this an anonymous class? + if (e.outerClassInfoIndex == 0) return null; + try { + return this.resolveClass(e.outerClassInfoIndex); + } catch (ClassNotFoundException ex) { + throw new CompileException(ex.getMessage(), null); // SUPPRESS CHECKSTYLE AvoidHidingCause + } + } + } + return null; + } + + @Override protected IClass + getOuterIClass2() throws CompileException { + ClassFile.InnerClassesAttribute ica = this.classFile.getInnerClassesAttribute(); + if (ica == null) return null; + + for (ClassFile.InnerClassesAttribute.Entry e : ica.getEntries()) { + if (e.innerClassInfoIndex == this.classFile.thisClass) { + if (e.outerClassInfoIndex == 0) { + + // Anonymous class or local class. + // TODO: Determine enclosing instance of anonymous class or local class + return null; + } else { + + // Member type. + if (Mod.isStatic(e.innerClassAccessFlags)) return null; + try { + return this.resolveClass(e.outerClassInfoIndex); + } catch (ClassNotFoundException ex) { + throw new CompileException(ex.getMessage(), null); // SUPPRESS CHECKSTYLE AvoidHidingCause + } + } + } + } + return null; + } + + @Override protected IClass + getSuperclass2() throws CompileException { + if (this.classFile.superclass == 0) return null; + try { + return this.resolveClass(this.classFile.superclass); + } catch (ClassNotFoundException e) { + throw new CompileException(e.getMessage(), null); // SUPPRESS CHECKSTYLE AvoidHidingCause + } + } + + @Override public Access + getAccess() { return ClassFileIClass.accessFlags2Access(this.accessFlags); } + + @Override public boolean + isFinal() { return Mod.isFinal(this.accessFlags); } + + @Override protected IClass[] + getInterfaces2() throws CompileException { return this.resolveClasses(this.classFile.interfaces); } + + @Override public boolean + isAbstract() { return Mod.isAbstract(this.accessFlags); } + + @Override protected String + getDescriptor2() { return Descriptor.fromClassName(this.classFile.getThisClassName()); } + + @Override public boolean + isInterface() { return Mod.isInterface(this.accessFlags); } + + @Override public boolean + isArray() { return false; } + + @Override public boolean + isPrimitive() { return false; } + + @Override public boolean + isPrimitiveNumeric() { return false; } + + @Override protected IClass + getComponentType2() { return null; } + + /** Resolves all classes referenced by this class file. */ + public void + resolveAllClasses() throws ClassNotFoundException { + for (short i = 0; i < this.classFile.getConstantPoolSize(); ++i) { + ClassFile.ConstantPoolInfo cpi = this.classFile.getConstantPoolInfo(i); + if (cpi instanceof ClassFile.ConstantClassInfo) { + this.resolveClass(i); + } else + if (cpi instanceof ClassFile.ConstantNameAndTypeInfo) { + String descriptor = ((ClassFile.ConstantNameAndTypeInfo) cpi).getDescriptor(this.classFile); + if (descriptor.charAt(0) == '(') { + MethodDescriptor md = new MethodDescriptor(descriptor); + this.resolveClass(md.returnFd); + for (String parameterFd : md.parameterFds) this.resolveClass(parameterFd); + } else { + this.resolveClass(descriptor); + } + } + } + } + + /** @param index Index of the CONSTANT_Class_info to resolve (JVMS 4.4.1) */ + private IClass + resolveClass(short index) throws ClassNotFoundException { + if (ClassFileIClass.DEBUG) System.out.println("index=" + index); + ConstantClassInfo cci = (ConstantClassInfo) this.classFile.getConstantPoolInfo(index); + return this.resolveClass(Descriptor.fromInternalForm(cci.getName(this.classFile))); + } + + private IClass + resolveClass(String descriptor) throws ClassNotFoundException { + if (ClassFileIClass.DEBUG) System.out.println("descriptor=" + descriptor); + + IClass result = (IClass) this.resolvedClasses.get(descriptor); + if (result != null) return result; + + result = this.iClassLoader.loadIClass(descriptor); + if (result == null) throw new ClassNotFoundException(descriptor); + + this.resolvedClasses.put(descriptor, result); + return result; + } + private final Map resolvedClasses = new HashMap(); + + private IClass[] + resolveClasses(short[] ifs) throws CompileException { + IClass[] result = new IClass[ifs.length]; + for (int i = 0; i < result.length; ++i) { + try { + result[i] = this.resolveClass(ifs[i]); + } catch (ClassNotFoundException e) { + throw new CompileException(e.getMessage(), null); // SUPPRESS CHECKSTYLE AvoidHidingCause + } + } + return result; + } + + /** + * Turn a {@link ClassFile.MethodInfo} into an {@link IInvocable}. This includes the checking and the + * removal of the magic first parameter of an inner class constructor. + * + * @param methodInfo + * @throws ClassNotFoundException + */ + private IInvocable + resolveMethod(final ClassFile.MethodInfo methodInfo) throws ClassNotFoundException { + IInvocable result = (IInvocable) this.resolvedMethods.get(methodInfo); + if (result != null) return result; + + // Determine method name. + final String name = methodInfo.getName(); + + // Determine return type. + MethodDescriptor md = new MethodDescriptor(methodInfo.getDescriptor()); + + final IClass returnType = this.resolveClass(md.returnFd); + + // Determine parameter types. + final IClass[] parameterTypes = new IClass[md.parameterFds.length]; + for (int i = 0; i < parameterTypes.length; ++i) parameterTypes[i] = this.resolveClass(md.parameterFds[i]); + + // Determine thrown exceptions. + IClass[] tes = null; + ClassFile.AttributeInfo[] ais = methodInfo.getAttributes(); + for (ClassFile.AttributeInfo ai : ais) { + if (ai instanceof ClassFile.ExceptionsAttribute) { + ConstantClassInfo[] ccis = ((ClassFile.ExceptionsAttribute) ai).getExceptions(this.classFile); + tes = new IClass[ccis.length]; + for (int i = 0; i < tes.length; ++i) { + tes[i] = this.resolveClass(Descriptor.fromInternalForm(ccis[i].getName(this.classFile))); + } + } + } + final IClass[] thrownExceptions = tes == null ? new IClass[0] : tes; + + // Determine access. + final Access access = ClassFileIClass.accessFlags2Access(methodInfo.getModifierFlags()); + + if ("".equals(name)) { + result = new IClass.IConstructor() { + + @Override public boolean + isVarargs() { return Mod.isVarargs(methodInfo.getModifierFlags()); } + + @Override public IClass[] + getParameterTypes2() throws CompileException { + + // Process magic first parameter of inner class constructor. + IClass outerIClass = ClassFileIClass.this.getOuterIClass(); + if (outerIClass != null) { + if (parameterTypes.length < 1) { + throw new JaninoRuntimeException("Inner class constructor lacks magic first parameter"); + } + if (parameterTypes[0] != outerIClass) { + throw new JaninoRuntimeException( + "Magic first parameter of inner class constructor has type \"" + + parameterTypes[0].toString() + + "\" instead of that of its enclosing instance (\"" + + outerIClass.toString() + + "\")" + ); + } + IClass[] tmp = new IClass[parameterTypes.length - 1]; + System.arraycopy(parameterTypes, 1, tmp, 0, tmp.length); + return tmp; + } + + return parameterTypes; + } + + @Override public IClass[] getThrownExceptions2() { return thrownExceptions; } + @Override public Access getAccess() { return access; } + @Override public Java.Annotation[] getAnnotations() { return methodInfo.getAnnotations(); } + }; + } else { + result = new IClass.IMethod() { + + @Override public String + getName() { return name; } + + @Override public IClass + getReturnType() { return returnType; } + + @Override public boolean + isStatic() { return Mod.isStatic(methodInfo.getModifierFlags()); } + + @Override public boolean + isAbstract() { return Mod.isAbstract(methodInfo.getModifierFlags()); } + + @Override public boolean + isVarargs() { return Mod.isVarargs(methodInfo.getModifierFlags()); } + + @Override public IClass[] + getParameterTypes2() { return parameterTypes; } + + @Override public IClass[] + getThrownExceptions2() { return thrownExceptions; } + + @Override public Access + getAccess() { return access; } + + @Override public Java.Annotation[] + getAnnotations() { return methodInfo.getAnnotations(); } + }; + } + this.resolvedMethods.put(methodInfo, result); + return result; + } + private final Map resolvedMethods = new HashMap(); + + private IField + resolveField(final ClassFile.FieldInfo fieldInfo) throws ClassNotFoundException { + IField result = (IField) this.resolvedFields.get(fieldInfo); + if (result != null) return result; + + // Determine field name. + final String name = fieldInfo.getName(this.classFile); + + // Determine field type. + final String descriptor = fieldInfo.getDescriptor(this.classFile); + final IClass type = this.resolveClass(descriptor); + + // Determine optional "constant value" of the field (JLS7 15.28, bullet 14). If a field has a "ConstantValue" + // attribute, we assume that it has a constant value. Notice that this assumption is not always correct, + // because typical Java™ compilers do not generate a "ConstantValue" attribute for fields like + // "int RED = 0", because "0" is the default value for an integer field. + ClassFile.ConstantValueAttribute cva = null; + for (ClassFile.AttributeInfo ai : fieldInfo.getAttributes()) { + if (ai instanceof ClassFile.ConstantValueAttribute) { + cva = (ClassFile.ConstantValueAttribute) ai; + break; + } + } + + final Object optionalConstantValue = cva == null ? IClass.NOT_CONSTANT : cva.getConstantValue(this.classFile); + final Access access = ClassFileIClass.accessFlags2Access(fieldInfo.getModifierFlags()); + + result = new IField() { + @Override public Object getConstantValue() { return optionalConstantValue; } + @Override public String getName() { return name; } + @Override public IClass getType() { return type; } + @Override public boolean isStatic() { return Mod.isStatic(fieldInfo.getModifierFlags()); } + @Override public Access getAccess() { return access; } + @Override public Java.Annotation[] getAnnotations() { return fieldInfo.getAnnotations(); } + }; + this.resolvedFields.put(fieldInfo, result); + return result; + } + + private static Access + accessFlags2Access(short accessFlags) { + return ( + Mod.isPublicAccess(accessFlags) ? Access.PUBLIC + : Mod.isProtectedAccess(accessFlags) ? Access.PROTECTED + : Mod.isPrivateAccess(accessFlags) ? Access.PRIVATE + : Access.DEFAULT + ); + } +} diff --git a/src/org/codehaus/janino/ClassLoaderIClassLoader.java b/src/org/codehaus/janino/ClassLoaderIClassLoader.java new file mode 100644 index 00000000..225278c6 --- /dev/null +++ b/src/org/codehaus/janino/ClassLoaderIClassLoader.java @@ -0,0 +1,95 @@ + +/* + * Janino - An embedded Java[TM] compiler + * + * Copyright (c) 2001-2010, Arno Unkrig + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the + * following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the + * following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the + * following disclaimer in the documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package org.codehaus.janino; + +/** An {@link IClassLoader} that loads {@link IClass}es through a reflection {@link ClassLoader}. */ +@SuppressWarnings("rawtypes") public +class ClassLoaderIClassLoader extends IClassLoader { + private static final boolean DEBUG = false; + + /** @param classLoader The delegate that loads the classes. */ + public + ClassLoaderIClassLoader(ClassLoader classLoader) { + super( + null // optionalParentIClassLoader + ); + + if (classLoader == null) throw new NullPointerException(); + + this.classLoader = classLoader; + super.postConstruct(); + } + + /** + * Equivalent to + *
+     *   ClassLoaderIClassLoader(Thread.currentThread().getContextClassLoader())
+     * 
+ */ + public + ClassLoaderIClassLoader() { this(Thread.currentThread().getContextClassLoader()); } + + /** @return The delegate {@link ClassLoader} */ + public ClassLoader + getClassLoader() { return this.classLoader; } + + @Override protected IClass + findIClass(String descriptor) throws ClassNotFoundException { + + Class clazz; + try { + + // + // See also [ 931385 ] Janino 2.0 throwing exception on arrays of java.io.File: + // + // "ClassLoader.loadClass()" and "Class.forName()" should be identical, + // but "ClassLoader.loadClass("[Ljava.lang.Object;")" throws a + // ClassNotFoundException under JDK 1.5.0 beta. + // Unclear whether this a beta version bug and SUN will fix this in the final + // release, but "Class.forName()" seems to work fine in all cases, so we + // use that. + // + +// clazz = this.classLoader.loadClass(Descriptor.toClassName(descriptor)); + clazz = Class.forName(Descriptor.toClassName(descriptor), false, this.classLoader); + } catch (ClassNotFoundException e) { + if (e.getException() == null) { + return null; + } else + { + throw e; + } + } + if (ClassLoaderIClassLoader.DEBUG) System.out.println("clazz = " + clazz); + + IClass result = new ReflectionIClass(clazz, this); + this.defineIClass(result); + return result; + } + + private final ClassLoader classLoader; +} diff --git a/src/org/codehaus/janino/CodeContext.java b/src/org/codehaus/janino/CodeContext.java new file mode 100644 index 00000000..0016b478 --- /dev/null +++ b/src/org/codehaus/janino/CodeContext.java @@ -0,0 +1,1371 @@ + +/* + * Janino - An embedded Java[TM] compiler + * + * Copyright (c) 2001-2010, Arno Unkrig + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the + * following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the + * following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the + * following disclaimer in the documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package org.codehaus.janino; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.codehaus.janino.util.ClassFile; + +/** + * The context of the compilation of a function (constructor or method). Manages generation of + * byte code, the exception table, generation of line number tables, allocation of local variables, + * determining of stack size and local variable table size and flow analysis. + */ +@SuppressWarnings({ "rawtypes", "unchecked" }) public +class CodeContext { + private static final boolean DEBUG = false; + + private static final int INITIAL_SIZE = 128; + private static final byte UNEXAMINED = -1; + private static final byte INVALID_OFFSET = -2; + private static final int MAX_STACK_SIZE = 254; + + private final ClassFile classFile; + private final String functionName; + + private short maxStack; + private short maxLocals; + private byte[] code; + private final Offset beginning; + private final Inserter end; + private Inserter currentInserter; + private final List exceptionTableEntries; + + /** All the local variables that are allocated in any block in this {@link CodeContext}. */ + private final List allLocalVars = new ArrayList(); + + /** + * List of List of Java.LocalVariableSlot objects. Each List of Java.LocalVariableSlot is + * the local variables allocated for a block. They are pushed and poped onto the list together + * to make allocation of the next local variable slot easy. + */ + private final List> scopedVars = new ArrayList(); + + private short nextLocalVariableSlot; + private final List relocatables = new ArrayList(); + + /** Creates an empty "Code" attribute. */ + public + CodeContext(ClassFile classFile, String functionName) { + this.classFile = classFile; + this.functionName = functionName; + + this.maxStack = 0; + this.maxLocals = 0; + this.code = new byte[CodeContext.INITIAL_SIZE]; + this.beginning = new Offset(); + this.end = new Inserter(); + this.currentInserter = this.end; + this.exceptionTableEntries = new ArrayList(); + + this.beginning.offset = 0; + this.end.offset = 0; + this.beginning.next = this.end; + this.end.prev = this.beginning; + } + + /** The {@link ClassFile} this context is related to. */ + public ClassFile + getClassFile() { return this.classFile; } + + + /** + * Allocate space for a local variable of the given size (1 or 2) + * on the local variable array. + * + * As a side effect, the "max_locals" field of the "Code" attribute + * is updated. + * + * The only way to deallocate local variables is to + * {@link #saveLocalVariables()} and later {@link + * #restoreLocalVariables()}. + * + * @param size The number of slots to allocate (1 or 2) + * @return The slot index of the allocated variable + */ + public short + allocateLocalVariable(short size) { return this.allocateLocalVariable(size, null, null).getSlotIndex(); } + + /** + * Allocate space for a local variable of the given size (1 or 2) + * on the local variable array. + * + * As a side effect, the "max_locals" field of the "Code" attribute + * is updated. + * + * The only way to deallocate local variables is to + * {@link #saveLocalVariables()} and later {@link + * #restoreLocalVariables()}. + * @param size Number of slots to use (1 or 2) + * @param name The variable name, if it's null, the variable won't be written to the localvariabletable + * @param type The variable type. if the name isn't null, the type is needed to write to the localvariabletable + */ + public Java.LocalVariableSlot + allocateLocalVariable(short size, String name, IClass type) { + List currentVars = null; + + if (this.scopedVars.size() == 0) { + throw new Error("saveLocalVariables must be called first"); + } else { + currentVars = (List) this.scopedVars.get(this.scopedVars.size() - 1); + } + + Java.LocalVariableSlot slot = new Java.LocalVariableSlot(name, this.nextLocalVariableSlot, type); + + if (slot.getName() != null) { + slot.setStart(this.newOffset()); + } + + this.nextLocalVariableSlot += size; + currentVars.add(slot); + this.allLocalVars.add(slot); + + if (this.nextLocalVariableSlot > this.maxLocals) { + this.maxLocals = this.nextLocalVariableSlot; + } + + return slot; + } + + /** Remembers the current size of the local variables array. */ + public List + saveLocalVariables() { + + // Push empty list on the stack to hold a new block's local vars. + List l = new ArrayList(); + this.scopedVars.add(l); + + return l; + } + + /** + * Restore the previous size of the local variables array. This MUST to be called for every call + * to saveLocalVariables as it closes the variable extent for all the active local variables in + * the current block. + */ + public void + restoreLocalVariables() { + + // Pop the list containing the current block's local vars. + List slots = ( + (List) this.scopedVars.remove(this.scopedVars.size() - 1) + ); + for (Java.LocalVariableSlot slot : slots) { + if (slot.getName() != null) { + slot.setEnd(this.newOffset()); + } + } + } + + /** + * + * @param dos + * @param lineNumberTableAttributeNameIndex 0 == don't generate a "LineNumberTable" attribute + * @throws IOException + */ + protected void + storeCodeAttributeBody( + DataOutputStream dos, + short lineNumberTableAttributeNameIndex, + short localVariableTableAttributeNameIndex + ) throws IOException { + dos.writeShort(this.maxStack); // max_stack + dos.writeShort(this.maxLocals); // max_locals + dos.writeInt(this.end.offset); // code_length + dos.write(this.code, 0, this.end.offset); // code + dos.writeShort(this.exceptionTableEntries.size()); // exception_table_length + for (ExceptionTableEntry exceptionTableEntry : this.exceptionTableEntries) { // exception_table + dos.writeShort(exceptionTableEntry.startPC.offset); + dos.writeShort(exceptionTableEntry.endPC.offset); + dos.writeShort(exceptionTableEntry.handlerPC.offset); + dos.writeShort(exceptionTableEntry.catchType); + } + + List attributes = new ArrayList(); + + // Add "LineNumberTable" attribute. + if (lineNumberTableAttributeNameIndex != 0) { + List lnt = new ArrayList(); + for (Offset o = this.beginning; o != null; o = o.next) { + if (o instanceof LineNumberOffset) { + lnt.add(new ClassFile.LineNumberTableAttribute.Entry(o.offset, ((LineNumberOffset) o).lineNumber)); + } + } + ClassFile.LineNumberTableAttribute.Entry[] lnte = (ClassFile.LineNumberTableAttribute.Entry[]) lnt.toArray( + new ClassFile.LineNumberTableAttribute.Entry[lnt.size()] + ); + attributes.add(new ClassFile.LineNumberTableAttribute( + lineNumberTableAttributeNameIndex, // attributeNameIndex + lnte // lineNumberTableEntries + )); + } + + // Add "LocalVariableTable" attribute. + if (localVariableTableAttributeNameIndex != 0) { + ClassFile.AttributeInfo ai = this.storeLocalVariableTable(dos, localVariableTableAttributeNameIndex); + + if (ai != null) attributes.add(ai); + } + + dos.writeShort(attributes.size()); // attributes_count + for (ClassFile.AttributeInfo attribute : attributes) { // attributes; + attribute.store(dos); + } + } + + /** + * @return A {@link org.codehaus.janino.util.ClassFile.LocalVariableTableAttribute} for this {@link CodeContext} + */ + protected ClassFile.AttributeInfo + storeLocalVariableTable(DataOutputStream dos, short localVariableTableAttributeNameIndex) { + ClassFile cf = this.getClassFile(); + final List entryList = new ArrayList(); + + for (Java.LocalVariableSlot slot : this.getAllLocalVars()) { + + if (slot.getName() != null) { + String typeName = slot.getType().getDescriptor(); + short classSlot = cf.addConstantUtf8Info(typeName); + short varNameSlot = cf.addConstantUtf8Info(slot.getName()); + +// System.out.println("slot: " + slot + ", typeSlot: " + classSlot + ", varSlot: " + varNameSlot); + + ClassFile.LocalVariableTableAttribute.Entry entry = new ClassFile.LocalVariableTableAttribute.Entry( + (short) slot.getStart().offset, + (short) (slot.getEnd().offset - slot.getStart().offset), + varNameSlot, + classSlot, + slot.getSlotIndex() + ); + entryList.add(entry); + } + } + + if (entryList.size() > 0) { + Object entries = entryList.toArray(new ClassFile.LocalVariableTableAttribute.Entry[entryList.size()]); + + return new ClassFile.LocalVariableTableAttribute( + localVariableTableAttributeNameIndex, + (ClassFile.LocalVariableTableAttribute.Entry[]) entries + ); + } else { + return null; + } + } + + /** + * Checks the code for consistency; updates the "maxStack" member. + * + * Notice: On inconsistencies, a "RuntimeException" is thrown (KLUDGE). + */ + public void + flowAnalysis(String functionName) { + if (CodeContext.DEBUG) { + System.err.println("flowAnalysis(" + functionName + ")"); + } + + short[] stackSizes = new short[this.end.offset]; + Arrays.fill(stackSizes, CodeContext.UNEXAMINED); + + // Analyze flow from offset zero. + this.flowAnalysis( + functionName, + this.code, // code + this.end.offset, // codeSize + 0, // offset + (short) 0, // stackSize + stackSizes // stackSizes + ); + + // Analyze flow from exception handler entry points. + int analyzedExceptionHandlers = 0; + while (analyzedExceptionHandlers != this.exceptionTableEntries.size()) { + for (ExceptionTableEntry exceptionTableEntry : this.exceptionTableEntries) { + if (stackSizes[exceptionTableEntry.startPC.offset] != CodeContext.UNEXAMINED) { + this.flowAnalysis( + functionName, + this.code, // code + this.end.offset, // codeSize + exceptionTableEntry.handlerPC.offset, // offset + (short) (stackSizes[exceptionTableEntry.startPC.offset] + 1), // stackSize + stackSizes // stackSizes + ); + ++analyzedExceptionHandlers; + } + } + } + + // Check results and determine maximum stack size. + this.maxStack = 0; + for (int i = 0; i < stackSizes.length; ++i) { + short ss = stackSizes[i]; + if (ss == CodeContext.UNEXAMINED) { + if (CodeContext.DEBUG) { + System.out.println(functionName + ": Unexamined code at offset " + i); + return; + } else { + throw new JaninoRuntimeException(functionName + ": Unexamined code at offset " + i); + } + } + if (ss > this.maxStack) this.maxStack = ss; + } + } + + private void + flowAnalysis( + String functionName, + byte[] code, // Bytecode + int codeSize, // Size + int offset, // Current PC + short stackSize, // Stack size on entry + short[] stackSizes // Stack sizes in code + ) { + for (;;) { + if (CodeContext.DEBUG) System.out.println("Offset = " + offset + ", stack size = " + stackSize); + + // Check current bytecode offset. + if (offset < 0 || offset >= codeSize) { + throw new JaninoRuntimeException(functionName + ": Offset out of range"); + } + + // Have we hit an area that has already been analyzed? + int css = stackSizes[offset]; + if (css == stackSize) return; // OK. + if (css == CodeContext.INVALID_OFFSET) throw new JaninoRuntimeException(functionName + ": Invalid offset"); + if (css != CodeContext.UNEXAMINED) { + if (CodeContext.DEBUG) { + System.err.println( + functionName + + ": Operand stack inconsistent at offset " + + offset + + ": Previous size " + + css + + ", now " + + stackSize + ); + return; + } else { + throw new JaninoRuntimeException( + functionName + + ": Operand stack inconsistent at offset " + + offset + + ": Previous size " + + css + + ", now " + + stackSize + ); + } + } + stackSizes[offset] = stackSize; + + // Analyze current opcode. + byte opcode = code[offset]; + int operandOffset = offset + 1; + short props; + if (opcode == Opcode.WIDE) { + opcode = code[operandOffset++]; + props = Opcode.WIDE_OPCODE_PROPERTIES[0xff & opcode]; + } else { + props = Opcode.OPCODE_PROPERTIES[0xff & opcode]; + } + if (props == Opcode.INVALID_OPCODE) { + throw new JaninoRuntimeException( + functionName + + ": Invalid opcode " + + (0xff & opcode) + + " at offset " + + offset + ); + } + + switch (props & Opcode.SD_MASK) { + + case Opcode.SD_M4: + case Opcode.SD_M3: + case Opcode.SD_M2: + case Opcode.SD_M1: + case Opcode.SD_P0: + case Opcode.SD_P1: + case Opcode.SD_P2: + stackSize += (props & Opcode.SD_MASK) - Opcode.SD_P0; + break; + + case Opcode.SD_0: + stackSize = 0; + break; + + case Opcode.SD_GETFIELD: + --stackSize; + /* FALL THROUGH */ + case Opcode.SD_GETSTATIC: + stackSize += this.determineFieldSize((short) ( + CodeContext.extract16BitValue(0, operandOffset, code) + )); + break; + + case Opcode.SD_PUTFIELD: + --stackSize; + /* FALL THROUGH */ + case Opcode.SD_PUTSTATIC: + stackSize -= this.determineFieldSize((short) ( + CodeContext.extract16BitValue(0, operandOffset, code) + )); + break; + + case Opcode.SD_INVOKEVIRTUAL: + case Opcode.SD_INVOKESPECIAL: + case Opcode.SD_INVOKEINTERFACE: + --stackSize; + /* FALL THROUGH */ + case Opcode.SD_INVOKESTATIC: + stackSize -= this.determineArgumentsSize((short) ( + CodeContext.extract16BitValue(0, operandOffset, code) + )); + break; + + case Opcode.SD_MULTIANEWARRAY: + stackSize -= code[operandOffset + 2] - 1; + break; + + default: + throw new JaninoRuntimeException(functionName + ": Invalid stack delta"); + } + + if (stackSize < 0) { + String msg = ( + this.classFile.getThisClassName() + + '.' + + functionName + + ": Operand stack underrun at offset " + + offset + ); + if (CodeContext.DEBUG) { + System.err.println(msg); + return; + } else { + throw new JaninoRuntimeException(msg); + } + } + + if (stackSize > CodeContext.MAX_STACK_SIZE) { + String msg = ( + this.classFile.getThisClassName() + + '.' + + functionName + + ": Operand stack overflow at offset " + + offset + ); + if (CodeContext.DEBUG) { + System.err.println(msg); + return; + } else { + throw new JaninoRuntimeException(msg); + } + } + + switch (props & Opcode.OP1_MASK) { + + case 0: + ; + break; + + case Opcode.OP1_SB: + case Opcode.OP1_UB: + case Opcode.OP1_CP1: + case Opcode.OP1_LV1: + ++operandOffset; + break; + + case Opcode.OP1_SS: + case Opcode.OP1_CP2: + case Opcode.OP1_LV2: + operandOffset += 2; + break; + + case Opcode.OP1_BO2: + if (CodeContext.DEBUG) { + System.out.println("Offset = " + offset); + System.out.println("Operand offset = " + operandOffset); + System.out.println(code[operandOffset]); + System.out.println(code[operandOffset + 1]); + } + this.flowAnalysis( + functionName, + code, codeSize, + CodeContext.extract16BitValue(offset, operandOffset, code), + stackSize, + stackSizes + ); + operandOffset += 2; + break; + + case Opcode.OP1_JSR: + if (CodeContext.DEBUG) { + System.out.println("Offset = " + offset); + System.out.println("Operand offset = " + operandOffset); + System.out.println(code[operandOffset]); + System.out.println(code[operandOffset + 1]); + } + int targetOffset = CodeContext.extract16BitValue(offset, operandOffset, code); + operandOffset += 2; + if (stackSizes[targetOffset] == CodeContext.UNEXAMINED) { + this.flowAnalysis( + functionName, + code, codeSize, + targetOffset, + (short) (stackSize + 1), + stackSizes + ); + } + break; + + case Opcode.OP1_BO4: + this.flowAnalysis( + functionName, + code, codeSize, + CodeContext.extract32BitValue(offset, operandOffset, code), + stackSize, stackSizes + ); + operandOffset += 4; + break; + + case Opcode.OP1_LOOKUPSWITCH: + while ((operandOffset & 3) != 0) ++operandOffset; + this.flowAnalysis( + functionName, + code, codeSize, + CodeContext.extract32BitValue(offset, operandOffset, code), + stackSize, stackSizes + ); + operandOffset += 4; + + int npairs = CodeContext.extract32BitValue(0, operandOffset, code); + operandOffset += 4; + + for (int i = 0; i < npairs; ++i) { + operandOffset += 4; //skip match value + this.flowAnalysis( + functionName, + code, codeSize, + CodeContext.extract32BitValue(offset, operandOffset, code), + stackSize, stackSizes + ); + operandOffset += 4; //advance over offset + } + break; + + case Opcode.OP1_TABLESWITCH: + while ((operandOffset & 3) != 0) ++operandOffset; + this.flowAnalysis( + functionName, + code, codeSize, + CodeContext.extract32BitValue(offset, operandOffset, code), + stackSize, stackSizes + ); + operandOffset += 4; + int low = CodeContext.extract32BitValue(offset, operandOffset, code); + operandOffset += 4; + int hi = CodeContext.extract32BitValue(offset, operandOffset, code); + operandOffset += 4; + for (int i = low; i <= hi; ++i) { + this.flowAnalysis( + functionName, + code, codeSize, + CodeContext.extract32BitValue(offset, operandOffset, code), + stackSize, stackSizes + ); + operandOffset += 4; + } + break; + + default: + throw new JaninoRuntimeException(functionName + ": Invalid OP1"); + } + + switch (props & Opcode.OP2_MASK) { + + case 0: + ; + break; + + case Opcode.OP2_SB: + ++operandOffset; + break; + + case Opcode.OP2_SS: + operandOffset += 2; + break; + + default: + throw new JaninoRuntimeException(functionName + ": Invalid OP2"); + } + + switch (props & Opcode.OP3_MASK) { + + case 0: + ; + break; + + case Opcode.OP3_SB: + ++operandOffset; + break; + + default: + throw new JaninoRuntimeException(functionName + ": Invalid OP3"); + } + + Arrays.fill(stackSizes, offset + 1, operandOffset, CodeContext.INVALID_OFFSET); + + if ((props & Opcode.NO_FALLTHROUGH) != 0) return; + offset = operandOffset; + } + } + + /** + * Extract a 16 bit value at offset in code and add bias to it + * + * @param bias An int to skew the final result by (useful for calculating relative offsets) + * @param offset The position in the code array to extract the bytes from + * @param code The array of bytes + * @return An integer that treats the two bytes at position offset as an UNSIGNED SHORT + */ + private static int + extract16BitValue(int bias, int offset, byte[] code) { + int res = bias + ( + ((code[offset]) << 8) + + (code[offset + 1] & 0xff) + ); + if (CodeContext.DEBUG) { + System.out.println("extract16BitValue(bias, offset) = (" + bias + ", " + offset + ")"); + System.out.println("bytes = {" + code[offset] + ", " + code[offset + 1] + "}"); + System.out.println("result = " + res); + } + return res; + } + + /** + * Extract a 32 bit value at offset in code and add bias to it + * + * @param bias An int to skew the final result by (useful for calculating relative offsets) + * @param offset The position in the code array to extract the bytes from + * @param code The array of bytes + * @return The 4 bytes at position offset + bias + */ + private static int + extract32BitValue(int bias, int offset, byte[] code) { + int res = bias + ( + (code[offset] << 24) + + ((0xff & code[offset + 1]) << 16) + + ((0xff & code[offset + 2]) << 8) + + (0xff & code[offset + 3]) + ); + if (CodeContext.DEBUG) { + System.out.println("extract32BitValue(bias, offset) = (" + bias + ", " + offset + ")"); + System.out.println( + "" + + "bytes = {" + + code[offset] + + ", " + + code[offset + 1] + + ", " + + code[offset + 2] + + ", " + + code[offset + 3] + + "}" + ); + System.out.println("result = " + res); + } + return res; + } + + /** Fixes up all of the offsets and relocate() all relocatables. */ + public void + fixUpAndRelocate() { + + // We do this in a loop to allow relocatables to adjust the size + // of things in the byte stream. It is extremely unlikely, but possible + // that a late relocatable will grow the size of the bytecode, and require + // an earlier relocatable to switch from 32K mode to 64K mode branching + do { + this.fixUp(); + } while (!this.relocate()); + } + + /** Fixes up all offsets. */ + private void + fixUp() { + for (Offset o = this.beginning; o != this.end; o = o.next) { + if (o instanceof FixUp) ((FixUp) o).fixUp(); + } + } + + /** + * Relocate all relocatables and aggregate their response into a single one + * @return true if all of them relocated successfully + * false if any of them needed to change size + */ + private boolean + relocate() { + boolean finished = true; + for (Relocatable relocatable : this.relocatables) { + + // Do not terminate earlier so that everything gets a chance to grow in the first pass changes the common + // case for this to be O(n) instead of O(n**2). + finished &= relocatable.relocate(); + } + return finished; + } + + /** Analyses the descriptor of the Fieldref and return its size. */ + private int + determineFieldSize(short idx) { + ClassFile.ConstantFieldrefInfo cfi = ( + (ClassFile.ConstantFieldrefInfo) this.classFile.getConstantPoolInfo(idx) + ); + return Descriptor.size(cfi.getNameAndType(this.classFile).getDescriptor(this.classFile)); + } + + /** + * Analyse the descriptor of the Methodref and return the sum of the + * arguments' sizes minus the return value's size. + */ + private int + determineArgumentsSize(short idx) { + ClassFile.ConstantPoolInfo cpi = this.classFile.getConstantPoolInfo(idx); + ClassFile.ConstantNameAndTypeInfo nat = ( + cpi instanceof ClassFile.ConstantInterfaceMethodrefInfo + ? ((ClassFile.ConstantInterfaceMethodrefInfo) cpi).getNameAndType(this.classFile) + : ((ClassFile.ConstantMethodrefInfo) cpi).getNameAndType(this.classFile) + ); + String desc = nat.getDescriptor(this.classFile); + + if (desc.charAt(0) != '(') throw new JaninoRuntimeException("Method descriptor does not start with \"(\""); + int i = 1; + int res = 0; + for (;;) { + switch (desc.charAt(i++)) { + case ')': + return res - Descriptor.size(desc.substring(i)); + case 'B': case 'C': case 'F': case 'I': case 'S': case 'Z': + res += 1; + break; + case 'D': case 'J': + res += 2; + break; + case '[': + res += 1; + while (desc.charAt(i) == '[') ++i; + if ("BCFISZDJ".indexOf(desc.charAt(i)) != -1) { ++i; break; } + if (desc.charAt(i) != 'L') throw new JaninoRuntimeException("Invalid char after \"[\""); + ++i; + while (desc.charAt(i++) != ';'); + break; + case 'L': + res += 1; + while (desc.charAt(i++) != ';'); + break; + default: + throw new JaninoRuntimeException("Invalid method descriptor"); + } + } + } + + /** + * Inserts a sequence of bytes at the current insertion position. Creates + * {@link LineNumberOffset}s as necessary. + * + * @param lineNumber The line number that corresponds to the byte code, or -1 + * @param b + */ + public void + write(short lineNumber, byte[] b) { + if (b.length == 0) return; + + int ico = this.currentInserter.offset; + this.makeSpace(lineNumber, b.length); + System.arraycopy(b, 0, this.code, ico, b.length); + } + + /** + * Inserts a byte at the current insertion position. Creates + * {@link LineNumberOffset}s as necessary. + *

+ * This method is an optimization to avoid allocating small byte[] and ease + * GC load. + * + * @param lineNumber The line number that corresponds to the byte code, or -1 + * @param b1 + */ + public void + write(short lineNumber, byte b1) { + int ico = this.currentInserter.offset; + this.makeSpace(lineNumber, 1); + this.code[ico] = b1; + } + + /** + * Inserts bytes at the current insertion position. Creates + * {@link LineNumberOffset}s as necessary. + *

+ * This method is an optimization to avoid allocating small byte[] and ease + * GC load. + * + * @param lineNumber The line number that corresponds to the byte code, or -1 + * @param b1 + * @param b2 + */ + public void + write(short lineNumber, byte b1, byte b2) { + int ico = this.currentInserter.offset; + this.makeSpace(lineNumber, 2); + this.code[ico++] = b1; + this.code[ico] = b2; + } + + /** + * Inserts bytes at the current insertion position. Creates + * {@link LineNumberOffset}s as necessary. + *

+ * This method is an optimization to avoid allocating small byte[] and ease + * GC load. + * + * @param lineNumber The line number that corresponds to the byte code, or -1 + * @param b1 + * @param b2 + * @param b3 + */ + public void + write(short lineNumber, byte b1, byte b2, byte b3) { + int ico = this.currentInserter.offset; + this.makeSpace(lineNumber, 3); + this.code[ico++] = b1; + this.code[ico++] = b2; + this.code[ico] = b3; + } + + /** + * Inserts bytes at the current insertion position. Creates + * {@link LineNumberOffset}s as necessary. + *

+ * This method is an optimization to avoid allocating small byte[] and ease + * GC load. + * + * @param lineNumber The line number that corresponds to the byte code, or -1 + * @param b1 + * @param b2 + * @param b3 + * @param b4 + */ + public void + write(short lineNumber, byte b1, byte b2, byte b3, byte b4) { + int ico = this.currentInserter.offset; + this.makeSpace(lineNumber, 4); + this.code[ico++] = b1; + this.code[ico++] = b2; + this.code[ico++] = b3; + this.code[ico] = b4; + } + + /** + * Add space for {@code size} bytes at current offset. Creates {@link LineNumberOffset}s as necessary. + * + * @param lineNumber The line number that corresponds to the byte code, or -1 + * @param size The size in bytes to inject + */ + public void + makeSpace(short lineNumber, int size) { + if (size == 0) return; + + INSERT_LINE_NUMBER_OFFSET: + if (lineNumber != -1) { + Offset o; + for (o = this.currentInserter.prev; o != this.beginning; o = o.prev) { + if (o instanceof LineNumberOffset) { + if (((LineNumberOffset) o).lineNumber == lineNumber) break INSERT_LINE_NUMBER_OFFSET; + break; + } + } + LineNumberOffset lno = new LineNumberOffset(this.currentInserter.offset, lineNumber); + lno.prev = this.currentInserter.prev; + lno.next = this.currentInserter; + + this.currentInserter.prev.next = lno; + this.currentInserter.prev = lno; + } + + int ico = this.currentInserter.offset; + if (this.end.offset + size <= this.code.length) { + // Optimization to avoid a trivial method call in the common case + if (ico != this.end.offset) { + System.arraycopy(this.code, ico, this.code, ico + size, this.end.offset - ico); + } + } else { + byte[] oldCode = this.code; + //double size to avoid horrible performance, but don't grow over our limit + int newSize = Math.max(Math.min(oldCode.length * 2, 0xffff), oldCode.length + size); + if (newSize > 0xffff) { + throw new JaninoRuntimeException( + "Code of method \"" + + this.functionName + + "\" of class \"" + + this.classFile.getThisClassName() + + "\" grows beyond 64 KB" + ); + } + this.code = new byte[newSize]; + System.arraycopy(oldCode, 0, this.code, 0, ico); + System.arraycopy(oldCode, ico, this.code, ico + size, this.end.offset - ico); + } + Arrays.fill(this.code, ico, ico + size, (byte) 0); + for (Offset o = this.currentInserter; o != null; o = o.next) o.offset += size; + } + + /** @param lineNumber The line number that corresponds to the byte code, or -1 */ + public void + writeShort(short lineNumber, int v) { this.write(lineNumber, (byte) (v >> 8), (byte) v); } + + /** @param lineNumber The line number that corresponds to the byte code, or -1 */ + public void + writeBranch(short lineNumber, int opcode, final Offset dst) { + this.relocatables.add(new Branch(opcode, dst)); + this.write(lineNumber, (byte) opcode, (byte) -1, (byte) -1); + } + + private + class Branch extends Relocatable { + + public + Branch(int opcode, Offset destination) { + this.opcode = opcode; + this.source = CodeContext.this.newInserter(); + this.destination = destination; + if (opcode == Opcode.JSR_W || opcode == Opcode.GOTO_W) { + //no need to expand wide opcodes + this.expanded = true; + } else { + this.expanded = false; + } + } + + @Override public boolean + relocate() { + if (this.destination.offset == Offset.UNSET) { + throw new JaninoRuntimeException("Cannot relocate branch to unset destination offset"); + } + int offset = this.destination.offset - this.source.offset; + + if (!this.expanded && (offset > Short.MAX_VALUE || offset < Short.MIN_VALUE)) { + //we want to insert the data without skewing our source position, + //so we will cache it and then restore it later. + final int pos = this.source.offset; + CodeContext.this.pushInserter(this.source); + { + // promotion to a wide instruction only requires 2 extra bytes + // everything else requires a new GOTO_W instruction after a negated if + CodeContext.this.makeSpace( + (short) -1, + this.opcode == Opcode.GOTO ? 2 : this.opcode == Opcode.JSR ? 2 : 5 + ); + } + CodeContext.this.popInserter(); + this.source.offset = pos; + this.expanded = true; + return false; + } + + final byte[] ba; + if (!this.expanded) { + //we fit in a 16-bit jump + ba = new byte[] { (byte) this.opcode, (byte) (offset >> 8), (byte) offset }; + } else { + if (this.opcode == Opcode.GOTO || this.opcode == Opcode.JSR) { + ba = new byte[] { + (byte) (this.opcode + 33), // GOTO => GOTO_W; JSR => JSR_W + (byte) (offset >> 24), + (byte) (offset >> 16), + (byte) (offset >> 8), + (byte) offset + }; + } else + { + //exclude the if-statement from jump target + //if jumping backwards this will increase the jump to go over it + //if jumping forwards this will decrease the jump by it + offset -= 3; + + // [if cond offset] + //expands to + // [if !cond skip_goto] + // [GOTO_W offset] + ba = new byte[] { + CodeContext.invertBranchOpcode((byte) this.opcode), + 0, + 8, // Jump from this instruction past the GOTO_W + Opcode.GOTO_W, + (byte) (offset >> 24), + (byte) (offset >> 16), + (byte) (offset >> 8), + (byte) offset + }; + } + } + System.arraycopy(ba, 0, CodeContext.this.code, this.source.offset, ba.length); + return true; + } + + private boolean expanded; //marks whether this has been expanded to account for a wide branch + private final int opcode; + private final Inserter source; + private final Offset destination; + } + + /** E.g. {@link Opcode#IFLT} ("less than") inverts to {@link Opcode#IFGE} ("greater than or equal to"). */ + private static byte + invertBranchOpcode(byte branchOpcode) { + return ((Byte) CodeContext.BRANCH_OPCODE_INVERSION.get(new Byte(branchOpcode))).byteValue(); + } + + private static final Map + BRANCH_OPCODE_INVERSION = CodeContext.createBranchOpcodeInversion(); + private static Map + createBranchOpcodeInversion() { + Map m = new HashMap(); + m.put(new Byte(Opcode.IF_ACMPEQ), new Byte(Opcode.IF_ACMPNE)); + m.put(new Byte(Opcode.IF_ACMPNE), new Byte(Opcode.IF_ACMPEQ)); + m.put(new Byte(Opcode.IF_ICMPEQ), new Byte(Opcode.IF_ICMPNE)); + m.put(new Byte(Opcode.IF_ICMPNE), new Byte(Opcode.IF_ICMPEQ)); + m.put(new Byte(Opcode.IF_ICMPGE), new Byte(Opcode.IF_ICMPLT)); + m.put(new Byte(Opcode.IF_ICMPLT), new Byte(Opcode.IF_ICMPGE)); + m.put(new Byte(Opcode.IF_ICMPGT), new Byte(Opcode.IF_ICMPLE)); + m.put(new Byte(Opcode.IF_ICMPLE), new Byte(Opcode.IF_ICMPGT)); + m.put(new Byte(Opcode.IFEQ), new Byte(Opcode.IFNE)); + m.put(new Byte(Opcode.IFNE), new Byte(Opcode.IFEQ)); + m.put(new Byte(Opcode.IFGE), new Byte(Opcode.IFLT)); + m.put(new Byte(Opcode.IFLT), new Byte(Opcode.IFGE)); + m.put(new Byte(Opcode.IFGT), new Byte(Opcode.IFLE)); + m.put(new Byte(Opcode.IFLE), new Byte(Opcode.IFGT)); + m.put(new Byte(Opcode.IFNULL), new Byte(Opcode.IFNONNULL)); + m.put(new Byte(Opcode.IFNONNULL), new Byte(Opcode.IFNULL)); + return Collections.unmodifiableMap(m); + } + + /** Writes a four-byte offset (as it is used in TABLESWITCH and LOOKUPSWITCH) into this code context. */ + public void + writeOffset(short lineNumber, Offset src, final Offset dst) { + this.relocatables.add(new OffsetBranch(this.newOffset(), src, dst)); + this.write(lineNumber, (byte) -1, (byte) -1, (byte) -1, (byte) -1); + } + + private + class OffsetBranch extends Relocatable { + + public + OffsetBranch(Offset where, Offset source, Offset destination) { + this.where = where; + this.source = source; + this.destination = destination; + } + + @Override public boolean + relocate() { + if (this.source.offset == Offset.UNSET || this.destination.offset == Offset.UNSET) { + throw new JaninoRuntimeException("Cannot relocate offset branch to unset destination offset"); + } + int offset = this.destination.offset - this.source.offset; + byte[] ba = new byte[] { + (byte) (offset >> 24), + (byte) (offset >> 16), + (byte) (offset >> 8), + (byte) offset + }; + System.arraycopy(ba, 0, CodeContext.this.code, this.where.offset, 4); + return true; + } + private final Offset where, source, destination; + } + + /** Creates and inserts an {@link CodeContext.Offset} at the current inserter's current position. */ + public Offset + newOffset() { + Offset o = new Offset(); + o.set(); + return o; + } + + /** + * Allocate an {@link Inserter}, set it to the current offset, and + * insert it before the current offset. + * + * In clear text, this means that you can continue writing to the + * "Code" attribute, then {@link #pushInserter(CodeContext.Inserter)} the + * {@link Inserter}, then write again (which inserts bytes into the + * "Code" attribute at the previously remembered position), and then + * {@link #popInserter()}. + */ + public Inserter + newInserter() { Inserter i = new Inserter(); i.set(); return i; } + + /** @return The current inserter */ + public Inserter + currentInserter() { return this.currentInserter; } + + /** Remember the current {@link Inserter}, then replace it with the new one. */ + public void + pushInserter(Inserter ins) { + if (ins.nextInserter != null) throw new JaninoRuntimeException("An Inserter can only be pushed once at a time"); + ins.nextInserter = this.currentInserter; + this.currentInserter = ins; + } + + /** + * Replace the current {@link Inserter} with the remembered one (see + * {@link #pushInserter(CodeContext.Inserter)}). + */ + public void + popInserter() { + Inserter ni = this.currentInserter.nextInserter; + if (ni == null) throw new JaninoRuntimeException("Code inserter stack underflow"); + this.currentInserter.nextInserter = null; // Mark it as "unpushed". + this.currentInserter = ni; + } + + /** + * A class that represents an offset within a "Code" attribute. + * + * The concept of an "offset" is that if one writes into the middle of + * a "Code" attribute, all offsets behind the insertion point are + * automatically shifted. + */ + public + class Offset { + + /** The offset in the code attribute that this object represents. */ + int offset = Offset.UNSET; + + /** Links to preceding and succeding offsets. */ + Offset prev, next; + + /** + * Special value for {@link #offset} which indicates that this {@link Offset} has not yet been {@link #set()} + */ + static final int UNSET = -1; + + /** + * Sets this "Offset" to the offset of the current inserter; inserts this "Offset" before the current inserter. + */ + public void + set() { + if (this.offset != Offset.UNSET) throw new JaninoRuntimeException("Cannot \"set()\" Offset more than once"); + + this.offset = CodeContext.this.currentInserter.offset; + + this.prev = CodeContext.this.currentInserter.prev; + this.next = CodeContext.this.currentInserter; + this.prev.next = this; + this.next.prev = this; + } + + /** @return The {@link CodeContext} that this {@link Offset} belongs to */ + public final CodeContext getCodeContext() { return CodeContext.this; } + + @Override public String + toString() { return CodeContext.this.classFile.getThisClassName() + ": " + this.offset; } + } + + /** + * Add another entry to the "exception_table" of this code attribute (see JVMS 4.7.3). + * + * @param catchTypeFd null == "finally" clause + */ + public void + addExceptionTableEntry(Offset startPc, Offset endPc, Offset handlerPc, String catchTypeFd) { + this.exceptionTableEntries.add(new ExceptionTableEntry( + startPc, + endPc, + handlerPc, + catchTypeFd == null ? (short) 0 : this.classFile.addConstantClassInfo(catchTypeFd) + )); + } + + /** Representation of an entry in the "exception_table" of a "Code" attribute (see JVMS 4.7.3). */ + private static + class ExceptionTableEntry { + ExceptionTableEntry(Offset startPc, Offset endPc, Offset handlerPc, short catchType) { + this.startPC = startPc; + this.endPC = endPc; + this.handlerPC = handlerPc; + this.catchType = catchType; + } + final Offset startPC, endPC, handlerPC; + final short catchType; // 0 == "finally" clause + } + + /** A class that implements an insertion point into a "Code" attribute. */ + public + class Inserter extends Offset { + private Inserter nextInserter; // null == not in "currentInserter" stack + } + + /** An {@link Offset} who#s sole purpose is to later create a 'LneNumberTable' attribute. */ + public + class LineNumberOffset extends Offset { + private final int lineNumber; + + public + LineNumberOffset(int offset, int lineNumber) { + this.lineNumber = lineNumber; + this.offset = offset; + } + } + + private abstract + class Relocatable { + + /** + * Relocate this object. + * @return true if the relocation succeeded in place + * false if the relocation grew the number of bytes required + */ + public abstract boolean relocate(); + } + + /** + * A throw-in interface that marks {@link CodeContext.Offset}s + * as "fix-ups": During the execution of + * {@link CodeContext#fixUp}, all "fix-ups" are invoked and + * can do last touches to the code attribute. + *

+ * This is currently used for inserting the "padding bytes" into the + * TABLESWITCH and LOOKUPSWITCH instructions. + */ + public + interface FixUp { + + /** @see FixUp */ + void fixUp(); + } + + /** @return All the local variables that are allocated in any block in this {@link CodeContext} */ + public List + getAllLocalVars() { return this.allLocalVars; } + + /** + * Removes all code between {@code from} and {@code to}. Also removes any {@link CodeContext.Relocatable}s existing + * in that range. + */ + public void + removeCode(Offset from, Offset to) { + + if (from == to) return; + + int size = to.offset - from.offset; + assert size >= 0; + + if (size == 0) return; // Short circuit. + + // Shift down the bytecode past 'to'. + System.arraycopy(this.code, to.offset, this.code, from.offset, this.end.offset - to.offset); + + // Invalidate all offsets between 'from' and 'to'. + // Remove all relocatables that originate between 'from' and 'to'. + Set invalidOffsets = new HashSet(); + { + Offset o = from.next; + + for (; o != to;) { + if (o == null) { + System.currentTimeMillis(); + } + invalidOffsets.add(o); + + // Invalidate the offset for fast failure. + o.offset = -77; + o.prev = null; + o = o.next; + o.prev.next = null; + } + + for (;; o = o.next) { + o.offset -= size; + if (o == this.end) break; + } + } + + // Invalidate all relocatables which originate or target a removed offset. + for (Iterator it = this.relocatables.iterator(); it.hasNext();) { + Relocatable r = (Relocatable) it.next(); + + if (r instanceof Branch) { + Branch b = (Branch) r; + + if (invalidOffsets.contains(b.source)) { + it.remove(); + } else { + assert !invalidOffsets.contains(b.destination); + } + } + + if (r instanceof OffsetBranch) { + OffsetBranch ob = (OffsetBranch) r; + + if (invalidOffsets.contains(ob.source)) { + it.remove(); + } else { + assert !invalidOffsets.contains(ob.destination); + } + } + } + + for (Iterator it = this.exceptionTableEntries.iterator(); it.hasNext();) { + ExceptionTableEntry ete = (ExceptionTableEntry) it.next(); + + // Start, end and handler must either ALL lie IN the range to remove or ALL lie outside. + + if (invalidOffsets.contains(ete.startPC)) { + assert invalidOffsets.contains(ete.endPC); + assert invalidOffsets.contains(ete.handlerPC); + it.remove(); + } else { + assert !invalidOffsets.contains(ete.endPC); + assert !invalidOffsets.contains(ete.handlerPC); + } + } + + from.next = to; + to.prev = from; + } +} diff --git a/src/org/codehaus/janino/Compiler.java b/src/org/codehaus/janino/Compiler.java new file mode 100644 index 00000000..8335608f --- /dev/null +++ b/src/org/codehaus/janino/Compiler.java @@ -0,0 +1,830 @@ + +/* + * Janino - An embedded Java[TM] compiler + * + * Copyright (c) 2001-2010, Arno Unkrig + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the + * following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the + * following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the + * following disclaimer in the documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package org.codehaus.janino; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +import org.codehaus.commons.compiler.CompileException; +import org.codehaus.commons.compiler.ErrorHandler; +import org.codehaus.commons.compiler.Location; +import org.codehaus.commons.compiler.WarningHandler; +import org.codehaus.janino.Java.CompilationUnit; +import org.codehaus.janino.util.Benchmark; +import org.codehaus.janino.util.ClassFile; +import org.codehaus.janino.util.StringPattern; +import org.codehaus.janino.util.resource.DirectoryResourceCreator; +import org.codehaus.janino.util.resource.DirectoryResourceFinder; +import org.codehaus.janino.util.resource.FileResource; +import org.codehaus.janino.util.resource.FileResourceCreator; +import org.codehaus.janino.util.resource.PathResourceFinder; +import org.codehaus.janino.util.resource.Resource; +import org.codehaus.janino.util.resource.ResourceCreator; +import org.codehaus.janino.util.resource.ResourceFinder; + + +/** + * A simplified substitute for the javac tool. + * + * Usage: + *

+ * java org.codehaus.janino.Compiler \
+ *           [ -d destination-dir ] \
+ *           [ -sourcepath dirlist ] \
+ *           [ -classpath dirlist ] \
+ *           [ -extdirs dirlist ] \
+ *           [ -bootclasspath dirlist ] \
+ *           [ -encoding encoding ] \
+ *           [ -verbose ] \
+ *           [ -g:none ] \
+ *           [ -g:{source,lines,vars} ] \
+ *           [ -warn:pattern-list ] \
+ *           source-file ...
+ * java org.codehaus.janino.Compiler -help
+ * 
+ */ +@SuppressWarnings({ "rawtypes", "unchecked" }) public +class Compiler { + private static final boolean DEBUG = false; + + /** Command line interface. */ + public static void BCV(String[] args) throws Exception { + File destinationDirectory = Compiler.NO_DESTINATION_DIRECTORY; + File[] optionalSourcePath = null; + File[] classPath = { new File(".") }; + File[] optionalExtDirs = null; + File[] optionalBootClassPath = null; + String optionalCharacterEncoding = null; + boolean verbose = false; + boolean debugSource = true; + boolean debugLines = true; + boolean debugVars = false; + StringPattern[] warningHandlePatterns = Compiler.DEFAULT_WARNING_HANDLE_PATTERNS; + boolean rebuild = false; + + // Process command line options. + int i; + for (i = 0; i < args.length; ++i) { + String arg = args[i]; + if (arg.charAt(0) != '-') break; + if ("-d".equals(arg)) { + destinationDirectory = new File(args[++i]); + } else + if ("-sourcepath".equals(arg)) { + optionalSourcePath = PathResourceFinder.parsePath(args[++i]); + } else + if ("-classpath".equals(arg)) { + classPath = PathResourceFinder.parsePath(args[++i]); + } else + if ("-extdirs".equals(arg)) { + optionalExtDirs = PathResourceFinder.parsePath(args[++i]); + } else + if ("-bootclasspath".equals(arg)) { + optionalBootClassPath = PathResourceFinder.parsePath(args[++i]); + } else + if ("-encoding".equals(arg)) { + optionalCharacterEncoding = args[++i]; + } else + if ("-verbose".equals(arg)) { + verbose = true; + } else + if ("-g".equals(arg)) { + debugSource = true; + debugLines = true; + debugVars = true; + } else + if (arg.startsWith("-g:")) { + if (arg.indexOf("none") != -1) debugSource = (debugLines = (debugVars = false)); + if (arg.indexOf("source") != -1) debugSource = true; + if (arg.indexOf("lines") != -1) debugLines = true; + if (arg.indexOf("vars") != -1) debugVars = true; + } else + if (arg.startsWith("-warn:")) { + warningHandlePatterns = StringPattern.parseCombinedPattern(arg.substring(6)); + } else + if ("-rebuild".equals(arg)) { + rebuild = true; + } else + if ("-help".equals(arg)) { + System.out.printf(Compiler.USAGE, (Object[]) null); + } else + { + System.err.println("Unrecognized command line option \"" + arg + "\"; try \"-help\"."); + } + } + + // Get source file names. + if (i == args.length) { + System.err.println("No source files given on command line; try \"-help\"."); + } + File[] sourceFiles = new File[args.length - i]; + for (int j = i; j < args.length; ++j) sourceFiles[j - i] = new File(args[j]); + + // Create the compiler object. + final Compiler compiler = new Compiler( + optionalSourcePath, + classPath, + optionalExtDirs, + optionalBootClassPath, + destinationDirectory, + optionalCharacterEncoding, + verbose, + debugSource, + debugLines, + debugVars, + warningHandlePatterns, + rebuild + ); + + // Compile source files. + compiler.compile(sourceFiles); + } + + private static final String USAGE = ( + "" + + "Usage:%n" + + "%n" + + " java " + Compiler.class.getName() + " [