/* * Copyright 2016 Cel Skeggs. * * This file is part of the CCRE, the Common Chicken Runtime Engine. * * The CCRE is free software: you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) any * later version. * * The CCRE is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. * * You should have received a copy of the GNU Lesser General Public License * along with the CCRE. If not, see <http://www.gnu.org/licenses/>. */ package ccre.verifier; import java.io.DataInputStream; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Objects; import ccre.drivers.ByteFiddling; import ccre.verifier.BytecodeParser.ReferenceInfo; class ClassParser extends DataInputStream { public static final int ACC_PUBLIC = 0x0001, ACC_PRIVATE = 0x0002, ACC_PROTECTED = 0x0004, ACC_STATIC = 0x0008, ACC_FINAL = 0x0010, ACC_SUPER = 0x0020, // reused ACC_SYNCHRONIZED = 0x0020, // reuse ACC_VOLATILE = 0x0040, // reused ACC_BRIDGE = 0x0040, // reuse ACC_TRANSIENT = 0x0080, // reused ACC_VARARGS = 0x0080, // reuse ACC_NATIVE = 0x0100, ACC_INTERFACE = 0x0200, ACC_ABSTRACT = 0x0400, ACC_STRICT = 0x0800, ACC_SYNTHETIC = 0x1000, ACC_ANNOTATION = 0x2000, ACC_ENUM = 0x4000; public static final int CONSTANT_Class = 7, CONSTANT_Fieldref = 9, CONSTANT_Methodref = 10, CONSTANT_InterfaceMethodref = 11, CONSTANT_String = 8, CONSTANT_Integer = 3, CONSTANT_Float = 4, CONSTANT_Long = 5, CONSTANT_Double = 6, CONSTANT_NameAndType = 12, CONSTANT_Utf8 = 1, CONSTANT_MethodHandle = 15, CONSTANT_MethodType = 16, CONSTANT_InvokeDynamic = 18; public static enum ClassFormatVersion { JAVA_8 } public static class CPInfo { public CPInfo[] pool; public int tag; // name_index, class_index, string_index, constant_int, constant_float, // reference_kind, bootstrap_method_attr_index public int alpha; // name_and_type_index, descriptor_index, reference_index public int beta; // bytes (when int, float) public long u64; // bytes (when Utf8) public String bytes; @Override public String toString() { return "[" + tag + "] " + alpha + " " + beta + ": " + u64 + " " + bytes; } public String asClass() throws ClassFormatException { this.requireTag(CONSTANT_Class); String class_name = this.getConst(this.alpha).asUTF8(); if (class_name.indexOf('.') != -1) { throw new ClassFormatException("Binary class name should not include dots!"); } return class_name.replace('/', '.'); } public String asUTF8() throws ClassFormatException { this.requireTag(CONSTANT_Utf8); return this.bytes; } private CPInfo getConst(int i) { if (i == 0) { throw new IllegalArgumentException("Zeroth index of constant pool is reserved."); } return pool[i]; } public void requireTag(int tag) throws ClassFormatException { if (this.tag != tag) { throw new ClassFormatException("Expected a tag of " + tag + " but got " + this.tag + "!"); } } public void requireTagOf(int... tags) throws ClassFormatException { for (int tag : tags) { if (tag == this.tag) { return; // success } } throw new ClassFormatException("Expected a tag in " + Arrays.toString(tags) + " but got " + this.tag + "!"); } } public static class FieldInfo { public ClassFile declaringClass; public int access; public String name; public String descriptor; public AttributeInfo[] attributes; } public static class MethodInfo { public ClassFile declaringClass; public int access; public String name; public String descriptor; public AttributeInfo[] attributes; // calculated from descriptor public TypeInfo[] parameters; public TypeInfo returnType; public byte[] code; public ExceptionHandlerInfo[] handlers; public int[] linenumtable; @Override public String toString() { return declaringClass + "." + name + descriptor; } public void fillOutContents() throws ClassFormatException { parameters = parseMethodDescriptorArguments(descriptor); returnType = parseMethodDescriptorReturnType(descriptor); byte[] attr = getAttribute("Code"); code = null; handlers = null; if (attr != null) { try { ByteArrayDataInput din = new ByteArrayDataInput(attr); // ignore max_stack and max_locals din.readUnsignedShort(); din.readUnsignedShort(); byte[] code = new byte[din.readInt()]; din.readFully(code); ExceptionHandlerInfo[] handlers = new ExceptionHandlerInfo[din.readUnsignedShort()]; for (int i = 0; i < handlers.length; i++) { ExceptionHandlerInfo info = new ExceptionHandlerInfo(); info.start_pc = din.readUnsignedShort(); info.end_pc = din.readUnsignedShort(); info.handler_pc = din.readUnsignedShort(); int cti = din.readUnsignedShort(); info.catch_type = cti == 0 ? null : this.declaringClass.getConst(cti).asClass(); handlers[i] = info; } AttributeInfo[] info = new AttributeInfo[din.readUnsignedShort()]; for (int i = 0; i < info.length; i++) { info[i] = new AttributeInfo(); info[i].name = this.declaringClass.getConst(din.readUnsignedShort()).asUTF8(); info[i].bytes = new byte[din.readInt()]; din.readFully(info[i].bytes); } if (!din.isEOF()) { throw new ClassFormatException("Junk found at end of Code attribute!"); } int[] linenumtable = null; for (AttributeInfo in : info) { if (in.name.equals("LineNumberTable")) { if (in.bytes.length < 2) { throw new ClassFormatException("Incorrectly-sized LineNumberTable!"); } int line_number_table_length = ByteFiddling.asInt16BE(in.bytes, 0); if (in.bytes.length - 2 != line_number_table_length * 4) { throw new ClassFormatException("Incorrectly-sized LineNumberTable!"); } linenumtable = new int[line_number_table_length * 2]; for (int i = 0; i < linenumtable.length; i++) { linenumtable[i] = ByteFiddling.asInt16BE(in.bytes, 2 + i * 2); } break; } } this.code = code; this.handlers = handlers; this.linenumtable = linenumtable; } catch (EOFException ex) { throw new ClassFormatException("Code parser reached the end of the code attribute!", ex); } } } public byte[] getAttribute(String name) { for (AttributeInfo info : attributes) { if (name.equals(info.name)) { return info.bytes; } } return null; } public boolean isAnnotationPresent(String name) throws ClassFormatException { if (name.contains("/")) { throw new IllegalArgumentException("Annotation name must contain only slashes; no dots."); } for (String attr : new String[] { "RuntimeVisibleAnnotations", "RuntimeInvisibleAnnotations" }) { byte[] attrs = getAttribute(attr); if (attrs == null) { continue; } try { ByteArrayDataInput din = new ByteArrayDataInput(attrs); int num_annotations = din.readUnsignedShort(); for (int i = 0; i < num_annotations; i++) { String desc = parseAnnotationInfo(din); TypeInfo type = parseFieldDescriptor(desc); if (!type.isClass) { throw new ClassFormatException("Expected annotations to be of annotation types!"); } if (name.equals(type.name)) { return true; } } } catch (EOFException ex) { throw new ClassFormatException("Annotation parser reached the end of the attribute!", ex); } } return false; } private String parseAnnotationInfo(ByteArrayDataInput din) throws ClassFormatException, EOFException { String type = declaringClass.getConst(din.readUnsignedShort()).asUTF8(); int num_element_value_pairs = din.readUnsignedShort(); for (int j = 0; j < num_element_value_pairs; j++) { String element_name = declaringClass.getConst(din.readUnsignedShort()).asUTF8(); element_name.hashCode(); // discard element_name discardElementValue(din); } return type; } private void discardElementValue(ByteArrayDataInput din) throws EOFException, ClassFormatException { int tag = din.readUnsignedByte(); switch (tag) { case 'B': case 'C': case 'D': case 'F': case 'I': case 'J': case 'S': case 'Z': case 's': case 'c': din.readUnsignedShort(); // and discard it break; case 'e': din.readUnsignedShort(); din.readUnsignedShort(); // and discard them break; case '[': int count = din.readUnsignedShort(); for (int i = 0; i < count; i++) { discardElementValue(din); } break; case '@': parseAnnotationInfo(din); // and discard its } } public int getLineNumberFor(int bytecode_index) { if (linenumtable == null) { return 0; } int found_from = -1, line_number = 0; for (int i = 0; i < linenumtable.length; i += 2) { int start_pc = linenumtable[i], line_no = linenumtable[i + 1]; if (bytecode_index >= start_pc && start_pc > found_from) { found_from = start_pc; line_number = line_no; } } return line_number; } public boolean isGetter() { // TODO: do this better? // Look for: aload_0, getfield, *return (except just return) if (this.code == null) { return false; } else if (this.code.length == 4) { return (this.code[0] & 0xFF) == 0xB2 && (this.code[3] & 0xFF) >= 0xAC && (this.code[3] & 0xFF) <= 0xB0; } else if (this.code.length == 5) { return (this.code[0] & 0xFF) == 0x2A && (this.code[1] & 0xFF) == 0xB4 && (this.code[4] & 0xFF) >= 0xAC && (this.code[4] & 0xFF) <= 0xB0; } else { return false; } } public boolean isSolitary() throws ClassFormatException { return this.code != null && new BytecodeParser(this).getReferenceCount() == 0; } public ReferenceInfo getOneRef() throws ClassFormatException { BytecodeParser bytecode = new BytecodeParser(this); if (this.code == null || bytecode.getReferenceCount() != 1) { return null; } ReferenceInfo[] ri = bytecode.getReferences(); if (ri.length == 1) { return ri[0]; } return null; } } public static class TypeInfo { public static final TypeInfo VOID = new TypeInfo(false, "void", null); public static final TypeInfo BYTE = new TypeInfo(false, "byte", null), CHAR = new TypeInfo(false, "char", null); public static final TypeInfo DOUBLE = new TypeInfo(false, "double", null), FLOAT = new TypeInfo(false, "float", null); public static final TypeInfo INT = new TypeInfo(false, "int", null), LONG = new TypeInfo(false, "long", null); public static final TypeInfo SHORT = new TypeInfo(false, "short", null), BOOL = new TypeInfo(false, "boolean", null); public final boolean isClass; public final String name; // contains no slashes, just dots public final TypeInfo element; private TypeInfo(boolean isClass, String name, TypeInfo element) { if (name.contains("/")) { throw new IllegalArgumentException("Invalid TypeInfo"); } this.isClass = isClass; this.name = name; this.element = element; } public static TypeInfo getArrayOf(TypeInfo element) { return new TypeInfo(false, element.name + "[]", element); } public static TypeInfo getClassFor(String name) { return new TypeInfo(true, name, null); } public boolean isArray() { return element != null; } @Override public String toString() { return "[type " + name + "]"; } @Override public int hashCode() { return Objects.hash(name, isClass, element); } @Override public boolean equals(Object obj) { if (obj instanceof TypeInfo) { TypeInfo ti = (TypeInfo) obj; return Objects.equals(name, ti.name) && isClass == ti.isClass && Objects.equals(element, ti.element); } else { return false; } } } private static int scanMethodDescriptor(String descriptor) throws ClassFormatException { if (descriptor.charAt(0) != '(') { throw new ClassFormatException("Invalid method descriptor: missing opening paren"); } int close = descriptor.indexOf(')'); if (close == -1) { throw new ClassFormatException("Invalid method descriptor: missing closing paren"); } if (descriptor.indexOf('(', 1) != -1 || descriptor.indexOf(')', close + 1) != -1) { throw new ClassFormatException("Invalid method descriptor: too many parens"); } return close; } public static TypeInfo parseMethodDescriptorReturnType(String descriptor) throws ClassFormatException { int close = scanMethodDescriptor(descriptor); String ret = descriptor.substring(close + 1); if (ret.isEmpty()) { throw new ClassFormatException("Invalid method descriptor: no return type"); } if (ret.charAt(0) == 'V') { if (ret.length() > 1) { throw new ClassFormatException("Invalid method descriptor: return type has garbage at the end"); } return TypeInfo.VOID; } else { ArrayList<TypeInfo> list = new ArrayList<>(); if (parseTypeDescriptor(ret, list) != ret.length()) { throw new ClassFormatException("Invalid method descriptor: return type has garbage at the end"); } if (list.size() != 1) { throw new RuntimeException("Oops... that really shouldn't have happened."); } return list.get(0); } } public static TypeInfo[] parseMethodDescriptorArguments(String descriptor) throws ClassFormatException { int close = scanMethodDescriptor(descriptor); ArrayList<TypeInfo> list = new ArrayList<>(); for (int i = 1; i < close;) { i += parseTypeDescriptor(descriptor.substring(i, close), list); } return list.toArray(new TypeInfo[list.size()]); } public static TypeInfo parseFieldDescriptor(String descriptor) throws ClassFormatException { ArrayList<TypeInfo> arr = new ArrayList<>(); // not a good way to do it // TODO: refactor if (parseTypeDescriptor(descriptor, arr) != descriptor.length()) { throw new ClassFormatException("Did not consume entire field descriptor!"); } if (arr.size() != 1) { throw new RuntimeException("Oops... that really shouldn't have happened."); } return arr.get(0); } // returns the number of consumed bytes private static int parseTypeDescriptor(String descriptor, ArrayList<TypeInfo> out) throws ClassFormatException { switch (descriptor.charAt(0)) { case 'B': out.add(TypeInfo.BYTE); return 1; case 'C': out.add(TypeInfo.CHAR); return 1; case 'D': out.add(TypeInfo.DOUBLE); return 1; case 'F': out.add(TypeInfo.FLOAT); return 1; case 'I': out.add(TypeInfo.INT); return 1; case 'J': out.add(TypeInfo.LONG); return 1; case 'S': out.add(TypeInfo.SHORT); return 1; case 'Z': out.add(TypeInfo.BOOL); return 1; case '[': int prev = parseTypeDescriptor(descriptor.substring(1), out); out.set(out.size() - 1, TypeInfo.getArrayOf(out.get(out.size() - 1))); return prev + 1; case 'L': int last = descriptor.indexOf(';'); if (descriptor.indexOf('.') != -1) { throw new ClassFormatException("Did not expect dots in type descriptor."); } out.add(TypeInfo.getClassFor(descriptor.substring(1, last).replace('/', '.'))); return last + 1; default: throw new ClassFormatException("Invalid type descriptor: invalid descriptor header " + descriptor.charAt(0)); } } public static class AttributeInfo { public String name; public byte[] bytes; } public static class ExceptionHandlerInfo { public int start_pc, end_pc, handler_pc; public String catch_type; } public static class ClassFile { public ClassFormatVersion version; public CPInfo[] constant_pool; public int access; public String this_class; public String super_class; public String[] interfaces; public FieldInfo[] fields; public MethodInfo[] methods; public AttributeInfo[] attributes; public CPInfo getConst(int i) { if (i == 0) { throw new IllegalArgumentException("Zeroth index of constant pool is reserved."); } return constant_pool[i]; } public String getSourceFile() throws ClassFormatException { for (AttributeInfo info : attributes) { if (info.name.equals("SourceFile")) { if (info.bytes.length < 2) { throw new ClassFormatException("SourceFile attribute is too short!"); } int sourcefile_index = ByteFiddling.asInt16BE(info.bytes, 0); return this.getConst(sourcefile_index).asUTF8(); } } return null; } @Override public String toString() { return "[class " + this_class + "]"; } } public ClassParser(InputStream input) { super(input); } public ClassFile readClassFile() throws IOException { ClassFile file = new ClassFile(); file.version = readClassHeader(); file.constant_pool = readConstantPool(); file.access = readUnsignedShort(); file.this_class = readConstant(file).asClass(); int superIndex = readUnsignedShort(); file.super_class = superIndex == 0 ? null : file.getConst(superIndex).asClass(); file.interfaces = readInterfaces(file); file.fields = readFields(file); file.methods = readMethods(file); file.attributes = readAttributes(file); return file; } // note: consumes a byte if wrong public void requireEOF() throws IOException { if (this.read() != -1) { throw new IOException("Not at EOF, as expected!"); } } public FieldInfo[] readFields(ClassFile file) throws IOException { FieldInfo[] out = new FieldInfo[readUnsignedShort()]; for (int i = 0; i < out.length; i++) { out[i] = readField(file); } return out; } public FieldInfo readField(ClassFile file) throws IOException { FieldInfo info = new FieldInfo(); info.declaringClass = file; info.access = readUnsignedShort(); info.name = readConstant(file).asUTF8(); info.descriptor = readConstant(file).asUTF8(); info.attributes = readAttributes(file); return info; } public MethodInfo[] readMethods(ClassFile file) throws IOException { MethodInfo[] out = new MethodInfo[readUnsignedShort()]; for (int i = 0; i < out.length; i++) { out[i] = readMethod(file); } return out; } public MethodInfo readMethod(ClassFile file) throws IOException { MethodInfo info = new MethodInfo(); info.declaringClass = file; info.access = readUnsignedShort(); info.name = readConstant(file).asUTF8(); info.descriptor = readConstant(file).asUTF8(); info.attributes = readAttributes(file); info.fillOutContents(); return info; } public AttributeInfo[] readAttributes(ClassFile file) throws IOException { AttributeInfo[] info = new AttributeInfo[readUnsignedShort()]; for (int i = 0; i < info.length; i++) { info[i] = readAttribute(file); } return info; } public AttributeInfo readAttribute(ClassFile file) throws IOException { AttributeInfo info = new AttributeInfo(); info.name = readConstant(file).asUTF8(); info.bytes = new byte[readInt()]; readFully(info.bytes); return info; } public String[] readInterfaces(ClassFile file) throws IOException { String[] out = new String[readUnsignedShort()]; for (int i = 0; i < out.length; i++) { out[i] = readConstant(file).asClass(); } return out; } public CPInfo readConstant(ClassFile file) throws IOException { return file.getConst(readUnsignedShort()); } public CPInfo readNullableConstant(ClassFile file) throws IOException { int c = readUnsignedShort(); return c == 0 ? null : file.getConst(c); } public ClassFormatVersion readClassHeader() throws IOException { if (readInt() != 0xCAFEBABE) { throw new ClassFormatException("Invalid magic number"); } int minor = readUnsignedShort(); int major = readUnsignedShort(); if (minor != 0 || major != 0x34) { throw new ClassFormatException("Unsupported class version: " + major + "." + minor); } return ClassFormatVersion.JAVA_8; } public CPInfo[] readConstantPool() throws IOException { CPInfo[] info = new CPInfo[readUnsignedShort()]; // we skip over info[0] because that's not really a thing according to // Java. for (int i = 1; i < info.length; i++) { info[i] = readConstantPoolEntry(); info[i].pool = info; if (info[i].tag == CONSTANT_Long || info[i].tag == CONSTANT_Double) { i++; // double-wide } } return info; } public CPInfo readConstantPoolEntry() throws IOException { CPInfo info = new CPInfo(); info.tag = readUnsignedByte(); switch (info.tag) { case CONSTANT_Class: case CONSTANT_String: info.alpha = readUnsignedShort(); break; case CONSTANT_Fieldref: case CONSTANT_Methodref: case CONSTANT_InterfaceMethodref: case CONSTANT_NameAndType: case CONSTANT_InvokeDynamic: info.alpha = readUnsignedShort(); info.beta = readUnsignedShort(); break; case CONSTANT_Integer: case CONSTANT_Float: info.alpha = readInt(); break; case CONSTANT_Long: case CONSTANT_Double: info.u64 = readLong(); break; case CONSTANT_Utf8: info.bytes = readUTF(); break; case CONSTANT_MethodHandle: info.alpha = readUnsignedByte(); info.beta = readUnsignedShort(); break; case CONSTANT_MethodType: // yes, this is supposed to be beta. because the same // descriptor_index mapping as another method. info.beta = readUnsignedShort(); break; default: throw new ClassFormatException("Constant pool type not understood: " + info.tag); } return info; } public static ClassFile parse(InputStream input) throws IOException { try (ClassParser parser = new ClassParser(input)) { ClassFile f = parser.readClassFile(); parser.requireEOF(); return f; } } }