/*
* 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.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.NoSuchFileException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import ccre.deployment.Artifact;
import ccre.log.Logger;
import ccre.storage.StorageSegment;
import ccre.verifier.BytecodeParser.ReferenceInfo;
import ccre.verifier.ClassParser.CPInfo;
import ccre.verifier.ClassParser.ClassFile;
import ccre.verifier.ClassParser.MethodInfo;
import ccre.verifier.ClassParser.TypeInfo;
/**
* A tool that can verify that a chunk of code does not include any phase
* mismatches that could lead to obscure robot code issues.
*
* @author skeggsc
*/
public class PhaseVerifier {
private static final HashMap<String, Phase> externals = new HashMap<>();
static {
try (InputStream phin = PhaseVerifier.class.getResourceAsStream("java_phases.properties")) {
HashMap<String, String> loaded = new HashMap<>();
StorageSegment.loadProperties(phin, false, loaded);
for (Map.Entry<String, String> ent : loaded.entrySet()) {
externals.put(ent.getKey(), Phase.valueOf(ent.getValue().toUpperCase()));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// public static void main(String[] args) throws Exception {
// PhaseVerifier.verify(new Jar(new File("../CommonChickenRuntimeEngine/CCRE.jar")));
// }
private final Artifact artifact;
private final HashMap<MethodInfo, Phase> known = new HashMap<>();
private final HashMap<String, ClassFile> loaded = new HashMap<>();
private int warnings;
private final Artifact[] deps;
private PhaseVerifier(Artifact artifact, Artifact[] deps) {
this.artifact = artifact;
this.deps = deps;
}
/**
* Verifies that all classes in <code>target</code> are properly phased.
* Warnings will be logged for every mistake.
*
* @param target the classes to verify.
* @param deps all possible non-built-in dependencies of the classes.
*/
public static void verify(Artifact target, Artifact... deps) {
int warnings = new PhaseVerifier(target, deps).verifyAll();
Logger.warning("Found " + warnings + " warnings during phase verification.");
}
private Phase getPhase(MethodInfo method) throws ClassNotFoundException, ClassFormatException {
if (method == null) {
throw new NullPointerException();
}
if (known.containsKey(method)) {
return known.get(method);
}
Phase phase = getDeclaredPhase(method);
if (!"<init>".equals(method.name)) {
MethodInfo[] superMethods = getMethodSuperMatches(method);
// TODO: handle the case of external overridden target
if (superMethods.length == 0 && method.isAnnotationPresent(Override.class.getName())) {
warn(method.declaringClass.getSourceFile(), method.getLineNumberFor(0) - 1, "@Override method does not actually override superclass method");
}
for (MethodInfo superMethod : superMethods) {
Phase superPhase = getPhase(superMethod);
if (superPhase == null) {
continue;
}
if (phase == null) {
phase = superPhase;
} else if (phase != superPhase && phase != Phase.IGNORED) {
warn(method.declaringClass.getSourceFile(), method.getLineNumberFor(0) - 1, "Mismatched phase between method and overridden method: " + method + " overrides " + superMethod);
// TODO: update output phase
}
}
}
if (phase == null) {
for (MethodInfo mi : method.declaringClass.methods) {
if ((mi.access & ClassParser.ACC_BRIDGE) != 0 && mi.name.equals(method.name) && mi.parameters.length == method.parameters.length) {
BytecodeParser bcp = new BytecodeParser(mi);
ReferenceInfo[] refs = bcp.getReferences();
if (refs.length == 1) {
ReferenceInfo ri = refs[0];
MethodInfo target = getMethodRef(mi, ri.target);
if (target == method) {
phase = getPhase(mi);
break;
}
}
}
}
if (phase == null && method.isSolitary()) {
phase = Phase.IGNORED;
}
if (phase == null && (method.access & ClassParser.ACC_SYNTHETIC) != 0 && method.name.startsWith("access$")) {
ReferenceInfo ref = method.getOneRef();
if (ref != null) {
MethodInfo target = getMethodRef(method, ref.target);
if (target != method) {
phase = getPhase(target);
}
}
}
}
known.put(method, phase);
return phase;
}
private MethodInfo[] getMethodSuperMatches(MethodInfo m) throws ClassNotFoundException {
ClassFile c = getSuperClass(m.declaringClass);
ArrayList<MethodInfo> mis = new ArrayList<>();
while (c != null) {
MethodInfo info = getDeclaredMethod(c, m.name, m.parameters);
if (info != null) {
mis.add(info);
}
c = getSuperClass(c);
}
for (ClassFile ci : getAllSuperInterfaces(m.declaringClass)) {
MethodInfo info = this.getDeclaredMethod(ci, m.name, m.parameters);
if (info != null) {
mis.add(info);
}
}
return mis.toArray(new MethodInfo[mis.size()]);
}
private ClassFile getSuperClass(ClassFile c) throws ClassNotFoundException {
return c.super_class == null ? null : loadClass(c.super_class);
}
private MethodInfo getProvidedMethod(ClassFile clas, String name, TypeInfo[] parameters) throws ClassNotFoundException {
ClassFile c = clas;
while (c != null) {
MethodInfo info = this.getDeclaredMethod(c, name, parameters);
if (info != null) {
return info;
}
c = getSuperClass(c);
}
for (ClassFile ci : getAllSuperInterfaces(clas)) {
MethodInfo info = this.getDeclaredMethod(ci, name, parameters);
if (info != null) {
return info;
}
}
return null;
}
private ClassFile[] getAllSuperInterfaces(ClassFile c) throws ClassNotFoundException {
HashSet<ClassFile> cf = new HashSet<>();
enumerateAllSuperInterfaces(c, cf);
cf.remove(c);
return cf.toArray(new ClassFile[cf.size()]);
}
private void enumerateAllSuperInterfaces(ClassFile c, Collection<ClassFile> cf) throws ClassNotFoundException {
if (c == null) {
throw new NullPointerException();
}
if (cf.add(c)) {
ClassFile superClass = getSuperClass(c);
if (superClass != null) {
enumerateAllSuperInterfaces(superClass, cf);
}
for (ClassFile ci : getSuperInterfaces(c)) {
enumerateAllSuperInterfaces(ci, cf);
}
}
}
private ClassFile[] getSuperInterfaces(ClassFile c) throws ClassNotFoundException {
ClassFile[] cf = new ClassFile[c.interfaces.length];
for (int i = 0; i < cf.length; i++) {
cf[i] = loadClass(c.interfaces[i]);
}
return cf;
}
private MethodInfo getDeclaredMethod(ClassFile c, String name, TypeInfo[] parameters) throws ClassNotFoundException {
MethodInfo result = null;
for (MethodInfo m : c.methods) {
if (m.name.equals(name) && Arrays.equals(m.parameters, parameters) && (result == null || isSuperTypeOf(result.returnType, m.returnType))) {
result = m;
}
}
return result;
}
private boolean isSuperTypeOf(TypeInfo sup, TypeInfo sub) throws ClassNotFoundException {
if (sup.equals(sub)) {
return true;
} else if (sup.isArray()) {
return sub.isArray() && isSuperTypeOf(sup.element, sub.element);
} else if (sub.isArray()) {
return sup.isClass && sup.name.equals("java/lang/Object");
} else if (sup.isClass && sub.isClass) {
ClassFile superClass = loadClass(sup.name);
ClassFile subSuper = getSuperClass(loadClass(sub.name));
return subSuper != null && isSuperOrSameClassOf(superClass, subSuper);
} else {
return false;
}
}
private boolean isSuperOrSameClassOf(ClassFile sup, ClassFile sub) throws ClassNotFoundException {
if (sup.equals(sub)) {
return true;
} else {
ClassFile superClass = getSuperClass(sub);
return superClass != null && isSuperOrSameClassOf(sup, superClass);
}
}
private InputStream loadClassFile(String class_) throws IOException {
try {
return artifact.loadClassFile(class_);
} catch (IOException e) {
for (Artifact art : deps) {
try {
return art.loadClassFile(class_);
} catch (IOException ex) {
// continue
}
}
throw e;
}
}
private ClassFile loadClass(String class_) throws ClassNotFoundException {
if (class_.indexOf('/') != -1) {
throw new IllegalArgumentException("Class names cannot contain slashes!");
}
if (loaded.containsKey(class_)) {
return loaded.get(class_);
} else {
ClassFile cf;
try {
InputStream art = null;
try {
art = loadClassFile(class_);
} catch (FileNotFoundException | NoSuchFileException e1) {
if (isNameExternal(class_)) {
art = Object.class.getResourceAsStream("/" + class_.replace('.', '/') + ".class");
}
if (art == null) {
throw e1;
}
}
cf = ClassParser.parse(art);
if (!cf.this_class.equals(class_)) {
throw new ClassNotFoundException("Could not load class due to mismatched names!");
}
} catch (IOException e) {
throw new ClassNotFoundException("Could not load class: " + class_, e);
}
loaded.put(class_, cf);
return cf;
}
}
private boolean isNameExternal(String class_) {
if (class_.contains("/")) {
throw new IllegalArgumentException("Invalid slash in isNameExternal");
}
return class_.startsWith("java.");
}
private Phase getDeclaredPhase(MethodInfo m) throws ClassFormatException, ClassNotFoundException {
if (m == null) {
throw new NullPointerException();
}
Phase found = null;
for (Phase p : Phase.values()) {
if (m.isAnnotationPresent(p.annot.getName())) {
if (found == null) {
found = p;
} else {
warn(m.declaringClass.getSourceFile(), m.getLineNumberFor(0) - 1, "Multiple phases declared on method: " + m);
}
}
}
boolean fromInit = false;
if (m.name.equals("<clinit>")) {
if (found != null) {
throw new ClassFormatException("Expected no annotations on static initializers.");
}
found = Phase.SETUP;
} else if (m.name.equals("<init>")) {
if (found == null) {
found = isException(m.declaringClass) ? Phase.IGNORED : Phase.SETUP;
fromInit = true;
}
} else if (m.isGetter() && found == null) {
found = Phase.IGNORED;
} else if ((m.declaringClass.access & ClassParser.ACC_ENUM) != 0 && m.name.equals("values") && m.parameters.length == 0) {
if (found == null) {
found = Phase.IGNORED;
} else {
warn(m.declaringClass.getSourceFile(), m.getLineNumberFor(0) - 1, "Enum values() declared with a phase");
}
}
if (isNameExternal(m.declaringClass.this_class)) {
Phase a = externals.get(m.declaringClass.this_class + "." + m.name + "(" + m.parameters.length + ")");
Phase b = externals.get(m.declaringClass.this_class + "." + m.name);
Phase c = externals.get(m.declaringClass.this_class + ".*");
Phase look = (a != null ? a : b != null ? b : c);
if (look != null) {
if (found == null) {
found = look;
} else if (found != look) {
if (fromInit) {
found = look;
} else {
warn(m.declaringClass.getSourceFile(), m.getLineNumberFor(0) - 1, "Attempt to declare external phase override on " + m);
}
}
}
}
return found;
}
private boolean isException(ClassFile cls) throws ClassNotFoundException {
return cls.this_class.equals("java.lang.Throwable") || (cls.super_class != null && isException(getSuperClass(cls)));
}
private int verifyAll() {
warnings = 0;
for (String className : artifact.listClassNames()) {
try {
verify(className);
} catch (ClassNotFoundException | ClassFormatException e) {
Logger.severe("Could not phase-verify class: " + className + ": " + e.getMessage());
}
}
return warnings;
}
private void verify(String className) throws ClassNotFoundException, ClassFormatException {
verify(loadClass(className));
}
private void verify(ClassFile cls) throws ClassNotFoundException, ClassFormatException {
for (MethodInfo m : cls.methods) {
verify(m);
}
}
private void verify(MethodInfo m) throws ClassNotFoundException, ClassFormatException {
if (m == null) {
throw new NullPointerException();
}
if (m.isAnnotationPresent(SuppressPhaseWarnings.class.getName())) {
return;
}
Phase p = getPhase(m);
if (p == null) {
// warn("No phase declared on method: " + m);
} else {
for (RefInfo target : enumerateReferences(m)) {
// TODO: handle the case where this is actually an unrelated
// construction of the superclass
if ("<init>".equals(m.name) && "<init>".equals(target.callee.name) && target.callee.declaringClass == getSuperClass(m.declaringClass)) {
// don't worry about superclass constructors.
continue;
}
Phase tp = getPhase(target.callee);
if (tp == null) {
warn(target.callerFile, target.callerLine, "Call to unphased method " + target.callee.declaringClass.this_class + "." + target.callee.name + " from " + m.name + m.descriptor);
} else if (!tp.allowedFrom(p)) {
warn(target.callerFile, target.callerLine, "Out-of-phase call from " + p + " to " + tp);
}
}
}
}
private static final class RefInfo {
public String callerFile;
public int callerLine;
public MethodInfo callee;
}
private RefInfo[] enumerateReferences(MethodInfo m) throws ClassFormatException, ClassNotFoundException {
if ((m.access & ClassParser.ACC_ABSTRACT) != 0) {
if (m.code != null || m.handlers != null) {
throw new ClassFormatException("An abstract method incorrectly contains code.");
}
return new RefInfo[0];
}
BytecodeParser parser = new BytecodeParser(m);
ArrayList<RefInfo> refs = new ArrayList<>();
for (ReferenceInfo ri : parser.getReferences()) {
CPInfo cp = ri.target;
if (cp.tag == ClassParser.CONSTANT_InvokeDynamic) {
// shouldn't actually matter... ignoring these. TODO: DON'T
// IGNORE THE METHOD BODIES - THEY SHOULD BE VERIFIED.
continue;
}
RefInfo rfi = new RefInfo();
rfi.callerFile = ri.getSourceFile();
rfi.callerLine = ri.getLineNumber();
rfi.callee = getMethodRef(m, cp);
refs.add(rfi);
}
return refs.toArray(new RefInfo[refs.size()]);
}
private MethodInfo getMethodRef(MethodInfo m, CPInfo cp) throws ClassFormatException, ClassNotFoundException {
MethodInfo referencedMethod;
cp.requireTagOf(ClassParser.CONSTANT_Methodref, ClassParser.CONSTANT_InterfaceMethodref);
String class_name = m.declaringClass.getConst(cp.alpha).asClass();
CPInfo name_and_type = m.declaringClass.getConst(cp.beta);
name_and_type.requireTag(ClassParser.CONSTANT_NameAndType);
String name = m.declaringClass.getConst(name_and_type.alpha).asUTF8();
String descriptor = m.declaringClass.getConst(name_and_type.beta).asUTF8();
ClassFile file = class_name.startsWith("[") ? loadClass("java.lang.Object") : loadClass(class_name);
referencedMethod = this.getProvidedMethod(file, name, ClassParser.parseMethodDescriptorArguments(descriptor));
if (referencedMethod == null) {
throw new ClassFormatException("Cannot resolve reference to " + class_name + "." + name + descriptor + " in " + m);
}
return referencedMethod;
}
private void warn(String file, int line, String string) {
warnings++;
Logger.warning("[VERIFIER] (" + file + ":" + line + ") " + string);
}
}