package openeye.logic;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.hash.Hashing;
import com.google.common.io.Files;
import java.io.File;
import java.lang.reflect.Field;
import java.net.URL;
import java.security.CodeSource;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.minecraft.launchwrapper.IClassTransformer;
import net.minecraft.launchwrapper.ITweaker;
import net.minecraft.launchwrapper.LaunchClassLoader;
import net.minecraftforge.fml.common.Loader;
import net.minecraftforge.fml.common.ModContainer;
import net.minecraftforge.fml.common.ModMetadata;
import net.minecraftforge.fml.common.discovery.ASMDataTable;
import net.minecraftforge.fml.common.discovery.ContainerType;
import net.minecraftforge.fml.common.discovery.ModCandidate;
import net.minecraftforge.fml.common.versioning.ArtifactVersion;
import net.minecraftforge.fml.common.versioning.VersionRange;
import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin;
import net.minecraftforge.fml.relauncher.ReflectionHelper;
import openeye.Log;
import openeye.protocol.Artifact;
import openeye.protocol.FileSignature;
import openeye.protocol.reports.ReportCrash.FileState;
import openeye.protocol.reports.ReportCrash.ModState;
import openeye.protocol.reports.ReportFileContents;
import openeye.protocol.reports.ReportFileInfo;
import openeye.protocol.reports.ReportFileInfo.SerializableMod;
import openeye.protocol.reports.ReportFileInfo.SerializableTweak;
import org.apache.logging.log4j.Level;
public class ModMetaCollector {
private static final String SIGNATURE_MINECRAFT_SERVER_JAR = "special:minecraft_server";
private static final String SIGNATURE_MINECRAFT_JAR = "special:minecraft";
private static final String SIGNATURE_NONE = "special:none";
private static class TweakMeta {
private final String pluginName;
private final String className;
private TweakMeta(String pluginName, String className) {
this.pluginName = Strings.nullToEmpty(pluginName);
this.className = Strings.nullToEmpty(className);
}
public SerializableTweak toSerializable() {
SerializableTweak result = new SerializableTweak();
result.plugin = pluginName;
result.cls = className;
return result;
}
}
private static class ModMeta {
private final String modId;
private final String name;
private final String version;
private final ModMetadata metadata;
private final ModContainer container;
private ModMeta(ModContainer container) {
this.container = container;
this.modId = Strings.nullToEmpty(container.getModId());
this.name = Strings.nullToEmpty(container.getName());
this.version = Strings.nullToEmpty(container.getVersion());
this.metadata = container.getMetadata();
}
private static <T> Collection<T> safeCopy(Collection<T> input) {
if (input == null) return ImmutableList.of();
return ImmutableList.copyOf(input);
}
private static Collection<Artifact> copyArtifacts(Collection<ArtifactVersion> input) {
if (input == null) return ImmutableList.of();
ImmutableList.Builder<Artifact> result = ImmutableList.builder();
for (ArtifactVersion version : input) {
final Artifact tmp = new Artifact();
tmp.label = version.getLabel();
tmp.version = version.getVersionString();
result.add(tmp);
}
return result.build();
}
@SuppressWarnings("deprecation")
public SerializableMod toSerializable() {
SerializableMod result = new SerializableMod();
result.modId = modId;
result.name = name;
result.version = version;
VersionRange mcVersionRange = container.acceptableMinecraftVersionRange();
if (mcVersionRange != null) result.mcVersion = Strings.nullToEmpty(mcVersionRange.toString());
result.description = Strings.nullToEmpty(metadata.description);
result.url = Strings.nullToEmpty(metadata.url);
result.updateUrl = Strings.nullToEmpty(metadata.updateUrl);
result.credits = Strings.nullToEmpty(metadata.credits);
result.parent = Strings.nullToEmpty(metadata.parent);
result.authors = safeCopy(metadata.authorList);
result.requiredMods = copyArtifacts(metadata.requiredMods);
result.dependants = copyArtifacts(metadata.dependants);
result.dependencies = copyArtifacts(metadata.dependencies);
return result;
}
public ModState getState() {
ModState state = new ModState();
state.modId = modId;
state.state = Loader.instance().getModState(container).toString();
return state;
}
}
private static class FileMeta {
public final Set<String> classTransformers = Sets.newHashSet();
public final Map<String, ModMeta> mods = Maps.newHashMap();
public final List<TweakMeta> tweakers = Lists.newArrayList();
public final Set<String> packages = Sets.newHashSet();
public final File container;
private String signature;
private FileMeta(File container, String signature) {
this.container = container;
this.signature = signature;
}
public String signature() {
if (signature == null) {
signature = calculateSignature(container);
}
return signature;
}
public Long getSize() {
try {
return container.length();
} catch (Throwable t) {
Log.info(t, "Can't get size info for file %s", container);
}
return null;
}
public FileState getStates() {
FileState result = new FileState();
result.signature = signature();
List<ModState> mods = Lists.newArrayList();
for (ModMeta meta : this.mods.values())
mods.add(meta.getState());
result.mods = mods;
return result;
}
public ReportFileInfo generateReport() {
ReportFileInfo report = new ReportFileInfo();
report.signature = signature();
report.size = getSize();
{
ImmutableList.Builder<SerializableMod> modsBuilder = ImmutableList.builder();
for (ModMeta m : mods.values())
modsBuilder.add(m.toSerializable());
report.mods = modsBuilder.build();
}
{
ImmutableList.Builder<SerializableTweak> tweaksBuilder = ImmutableList.builder();
for (TweakMeta t : tweakers)
tweaksBuilder.add(t.toSerializable());
report.tweakers = tweaksBuilder.build();
}
report.classTransformers = ImmutableList.copyOf(classTransformers);
report.packages = ImmutableList.copyOf(packages);
return report;
}
public ReportFileContents generateFileContentsReport() {
ReportFileContents report = new ReportFileContents();
report.signature = signature();
ReportBuilders.fillFileContents(container, report);
return report;
}
public FileSignature createFileSignature() {
FileSignature result = new FileSignature();
result.signature = signature();
result.filename = container.getName();
return result;
}
}
private final Map<File, FileMeta> files = Maps.newHashMap();
private final Map<String, FileMeta> signatures = Maps.newHashMap();
private final long operationDuration;
private final ASMDataTable table;
private Map<File, String> specialSignatures = Maps.newHashMap();
ModMetaCollector(ASMDataTable table, LaunchClassLoader loader, Collection<ITweaker> tweakers) {
Log.debug("Starting mod metadata collection");
this.table = table;
long start = System.nanoTime();
// Special handling for minecraft jars - some launchers like to repackage.
// Since it creates unique signatures, it may allow identification, if not obfuscated
// (order of calls is significant: client package contains both client and server classes)
assignSignatureToClassSource(SIGNATURE_MINECRAFT_SERVER_JAR, "net.minecraft.server.MinecraftServer");
assignSignatureToClassSource(SIGNATURE_MINECRAFT_JAR, "net.minecraft.client.main.Main");
Collection<ModCandidate> allCandidates = stealCandidates(table);
collectFilesFromModCandidates(allCandidates);
collectFilesFromClassTransformers(loader, table);
collectFilesFromTweakers(tweakers, table);
collectFilesFromModContainers(table);
fillSignaturesMap();
operationDuration = System.nanoTime() - start;
Log.debug("Collection of mod metadata finished. Duration: %.4f ms", operationDuration / 1000000.0d);
}
private FileMeta createFileMeta(File file) {
final String signature = specialSignatures.get(file);
return new FileMeta(file, signature);
}
private void assignSignatureToClassSource(String signature, String mainCls) {
try {
Class<?> cls = Class.forName(mainCls);
CodeSource src = cls.getProtectionDomain().getCodeSource();
URL jarUrl = extractJarUrl(src.getLocation());
File sourceFile = new File(jarUrl.toURI());
Preconditions.checkState(sourceFile.isFile(), "Path %s is not file", sourceFile);
specialSignatures.put(sourceFile, signature);
Log.debug("Signature '%s' assigned to file '%s'", signature, sourceFile);
} catch (ClassNotFoundException e) {
Log.debug("Failed to assign signature '%s' to source of class %s - class not found", signature, mainCls);
} catch (Exception e) {
Log.log(Level.DEBUG, e, "Failed to assign signature '%s' to source of class %s", signature, mainCls);
}
}
private static URL extractJarUrl(URL sourceUrl) throws Exception {
Preconditions.checkState(sourceUrl.getProtocol().equalsIgnoreCase("jar"), "%s is not jar path", sourceUrl);
final String jarFileSource = sourceUrl.getFile();
final int separator = jarFileSource.indexOf("!/");
if (separator == -1) throw new IllegalStateException("no !/ found in url spec:" + jarFileSource);
return new URL(jarFileSource.substring(0, separator));
}
private FileMeta fromModCandidate(ModCandidate candidate) {
FileMeta fileMeta = createFileMeta(candidate.getModContainer());
fileMeta.packages.addAll(candidate.getContainedPackages());
for (ModContainer c : candidate.getContainedMods())
fileMeta.mods.put(c.getModId(), new ModMeta(c));
return fileMeta;
}
private static String calculateSignature(File file) {
try {
return "sha256:" + Files.hash(file, Hashing.sha256()).toString();
} catch (Throwable t) {
Log.warn(t, "Can't hash file %s", file);
return SIGNATURE_NONE;
}
}
private FileMeta getOrCreateData(File file) {
FileMeta data = files.get(file);
if (data == null) {
data = createFileMeta(file);
files.put(file, data);
}
return data;
}
private void collectFilesFromModCandidates(Collection<ModCandidate> candidates) {
for (ModCandidate c : candidates) {
File modContainer = c.getModContainer();
if (!files.containsKey(modContainer) && c.getSourceType() == ContainerType.JAR) {
FileMeta meta = fromModCandidate(c);
files.put(modContainer, meta);
}
}
}
private static String extractPackage(String className) {
int pkgIdx = className.lastIndexOf('.');
if (pkgIdx == -1) return null;
return className.substring(0, pkgIdx);
}
private static Set<ModCandidate> getCandidatesForClass(ASMDataTable table, String cls) {
String packageName = extractPackage(cls);
if (Strings.isNullOrEmpty(packageName)) return null;
return table.getCandidatesFor(packageName);
}
private interface IFileMetaVisitor {
public void visit(FileMeta fileMeta);
}
private void visitMeta(ASMDataTable table, String cls, IFileMetaVisitor visitor) {
Set<ModCandidate> candidates = getCandidatesForClass(table, cls);
if (candidates != null) {
for (ModCandidate c : candidates) {
File container = c.getModContainer();
if (container.isDirectory()) continue;
FileMeta fileMeta = files.get(container);
if (fileMeta == null) {
fileMeta = fromModCandidate(c);
files.put(container, fileMeta);
}
visitor.visit(fileMeta);
}
}
}
private void registerClassTransformer(ASMDataTable table, final String cls) {
visitMeta(table, cls, new IFileMetaVisitor() {
@Override
public void visit(FileMeta fileMeta) {
fileMeta.classTransformers.add(cls);
}
});
}
private static Collection<ModCandidate> stealCandidates(ASMDataTable table) {
// I'm very sorry for that
try {
Multimap<String, ModCandidate> packageMap = ReflectionHelper.getPrivateValue(ASMDataTable.class, table, "packageMap");
if (packageMap != null) return packageMap.values();
} catch (Throwable t) {
Log.warn(t, "Can't get ModCandidate map, data will be missing from report");
}
return ImmutableList.of();
}
private void collectFilesFromModContainers(ASMDataTable table) {
final File dummyEntry = new File("minecraft.jar"); // dummy entry comes from MCP container
for (ModContainer c : Loader.instance().getModList()) {
File f = c.getSource();
if (f != null && !f.equals(dummyEntry) && !f.isDirectory()) {
FileMeta meta = getOrCreateData(f);
meta.mods.put(c.getModId(), new ModMeta(c));
}
}
}
private void collectFilesFromTweakers(Collection<ITweaker> tweakers, ASMDataTable table) {
try {
Class<?> coreModWrapper = Class.forName("net.minecraftforge.fml.relauncher.CoreModManager$FMLPluginWrapper");
Field nameField = coreModWrapper.getField("name");
nameField.setAccessible(true);
Field pluginField = coreModWrapper.getField("coreModInstance");
pluginField.setAccessible(true);
Field locationField = coreModWrapper.getField("location");
locationField.setAccessible(true);
for (ITweaker tweaker : tweakers) {
try {
File location = (File)locationField.get(tweaker);
if (location == null || location.isDirectory()) continue;
String name = (String)nameField.get(tweaker);
IFMLLoadingPlugin plugin = (IFMLLoadingPlugin)pluginField.get(tweaker);
FileMeta meta = getOrCreateData(location);
meta.tweakers.add(new TweakMeta(name, plugin.getClass().getName()));
} catch (Throwable t) {
Log.warn(t, "Can't get data for tweaker %s", tweaker);
}
}
} catch (Throwable t) {
Log.warn(t, "Can't get tweaker data");
}
}
private void collectFilesFromClassTransformers(LaunchClassLoader loader, ASMDataTable table) {
if (loader != null) {
List<IClassTransformer> transformers = loader.getTransformers();
for (IClassTransformer transformer : transformers)
registerClassTransformer(table, transformer.getClass().getName());
} else {
Log.warn("LaunchClassLoader not available");
}
}
private void fillSignaturesMap() {
for (FileMeta meta : files.values())
signatures.put(meta.signature(), meta);
}
public List<String> listSignatures() {
List<String> result = Lists.newArrayList();
for (FileMeta meta : files.values())
result.add(meta.signature);
return result;
}
public List<FileState> collectStates() {
List<FileState> result = Lists.newArrayList();
for (FileMeta meta : files.values())
result.add(meta.getStates());
return result;
}
public List<FileSignature> getAllFiles() {
List<FileSignature> result = Lists.newArrayList();
for (FileMeta meta : files.values()) {
FileSignature tmp = new FileSignature();
tmp.signature = meta.signature();
tmp.filename = meta.container.getName();
result.add(meta.createFileSignature());
}
return result;
}
public Set<String> getAllSignatures() {
Set<String> result = Sets.newHashSet();
for (FileMeta meta : files.values())
result.add(meta.signature());
return result;
}
public long getCollectingDuration() {
return operationDuration;
}
public ReportFileInfo generateFileReport(String signature) {
FileMeta meta = signatures.get(signature);
return meta != null? meta.generateReport() : null;
}
public ReportFileContents generateFileContentsReport(String signature) {
FileMeta meta = signatures.get(signature);
return meta != null? meta.generateFileContentsReport() : null;
}
public Set<String> getModsForSignature(String signature) {
FileMeta meta = signatures.get(signature);
if (meta != null) return ImmutableSet.copyOf(meta.mods.keySet());
else return ImmutableSet.of();
}
public FileSignature getFileForSignature(String signature) {
FileMeta meta = signatures.get(signature);
return meta != null? meta.createFileSignature() : null;
}
public File getContainerForSignature(String signature) {
FileMeta meta = signatures.get(signature);
return meta != null? meta.container : null;
}
public static class ClassSource {
public String loadedFrom;
public final Set<String> containingClasses = Sets.newHashSet();
}
public ClassSource identifyClassSource(String className) {
String packageName = extractPackage(className);
ClassSource result = new ClassSource();
if (packageName != null && (packageName.startsWith("net.minecraft") || packageName.startsWith("net.minecraftforge") || packageName.startsWith("cpw.mods.fml")))
return null;
try {
Class<?> cls = Class.forName(className);
CodeSource src = cls.getProtectionDomain().getCodeSource();
if (src != null) {
URL sourceUrl = extractJarUrl(src.getLocation());
File sourceFile = new File(sourceUrl.toURI());
FileMeta meta = files.get(sourceFile);
if (meta != null) result.loadedFrom = meta.signature();
}
} catch (Throwable t) {
// NO-OP - nothing to save
}
Set<ModCandidate> candidates = table.getCandidatesFor(packageName);
for (ModCandidate c : candidates) {
File container = c.getModContainer();
if (container != null) {
FileMeta meta = files.get(container);
if (meta != null) result.containingClasses.add(meta.signature());
}
}
return result;
}
}