package com.comphenix.protocol.injector; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.logging.Level; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginLoadOrder; import com.google.common.collect.Sets; /** * Determine if a plugin using ProtocolLib is correct. * * @author Kristian */ class PluginVerifier { /** * A named plugin cannot be found. * @author Kristian */ public static class PluginNotFoundException extends RuntimeException { /** * Generated by Eclipse. */ private static final long serialVersionUID = 8956699101336877611L; public PluginNotFoundException() { super(); } public PluginNotFoundException(String message) { super(message); } } public enum VerificationResult { VALID, /** * The plugin doesn't depend on ProtocolLib directly or indirectly. */ NO_DEPEND; /** * Determine if the verification was valid. */ public boolean isValid() { return this == VALID; } } /** * Contains a list of plugins that will detect and use ProtocolLib dynamically, instead of relying on the dependency system. */ private static final Set DYNAMIC_DEPENDENCY = Sets.newHashSet("mcore", "MassiveCore"); /** * Set of plugins that have been loaded after ProtocolLib. */ private final Set loadedAfter = new HashSet(); /** * Reference to ProtocolLib. */ private final Plugin dependency; /** * Construct a new plugin verifier. * @param dependency - reference to ProtocolLib, a dependency we require of plugins. */ public PluginVerifier(Plugin dependency) { if (dependency == null) throw new IllegalArgumentException("dependency cannot be NULL."); try { // This would screw with the assumption in hasDependency(Plugin, Plugin) if (safeConversion(dependency.getDescription().getLoadBefore()).size() > 0) throw new IllegalArgumentException("dependency cannot have a load directives."); } catch (LinkageError e) { // They're probably using an ancient version of Bukkit dependency.getLogger().log(Level.WARNING, "Failed to determine loadBefore: " + e); } this.dependency = dependency; } /** * Retrieve a plugin by name. * @param pluginName - the non-null name of the plugin to retrieve. * @return The retrieved plugin. * @throws PluginNotFoundException If a plugin with the given name cannot be found. */ private Plugin getPlugin(String pluginName) { Plugin plugin = getPluginOrDefault(pluginName); // Ensure that the plugin exists if (plugin != null) return plugin; else throw new PluginNotFoundException("Cannot find plugin " + pluginName); } /** * Retrieve a plugin by name. * @param pluginName - the non-null name of the plugin to retrieve. * @return The retrieved plugin, or NULL if not found. */ private Plugin getPluginOrDefault(String pluginName) { return dependency.getServer().getPluginManager().getPlugin(pluginName); } /** * Performs simple verifications on the given plugin. *

* Results may be cached. * @param pluginName - the plugin to verify. * @return A verification result. * @throws IllegalArgumentException If plugin name is NULL. * @throws PluginNotFoundException If a plugin with the given name cannot be found. */ public VerificationResult verify(String pluginName) { if (pluginName == null) throw new IllegalArgumentException("pluginName cannot be NULL."); return verify(getPlugin(pluginName)); } /** * Performs simple verifications on the given plugin. *

* Results may be cached. * @param plugin - the plugin to verify. * @return A verification result. * @throws IllegalArgumentException If plugin name is NULL. * @throws PluginNotFoundException If a plugin with the given name cannot be found. */ public VerificationResult verify(Plugin plugin) { if (plugin == null) throw new IllegalArgumentException("plugin cannot be NULL."); String name = plugin.getName(); // Skip the load order check for ProtocolLib itself if (!dependency.equals(plugin)) { if (!loadedAfter.contains(name) && !DYNAMIC_DEPENDENCY.contains(name)) { if (verifyLoadOrder(dependency, plugin)) { // Memorize loadedAfter.add(plugin.getName()); } else { return VerificationResult.NO_DEPEND; } } } // Everything seems to be in order return VerificationResult.VALID; } /** * Determine if a given plugin is guarenteed to be loaded before the other. *

* Note that the before plugin is assumed to have no "load" directives - that is, plugins to be * loaded after itself. The after plugin may have "load" directives, but it is irrelevant for our purposes. * @param beforePlugin - the plugin that is loaded first. * @param afterPlugin - the plugin that is loaded last. * @return TRUE if it will, FALSE if it may or must load in the opposite other. */ private boolean verifyLoadOrder(Plugin beforePlugin, Plugin afterPlugin) { // If a plugin has a dependency, it will be loaded after its dependency if (hasDependency(afterPlugin, beforePlugin)) { return true; } // No dependency - check the load order if (beforePlugin.getDescription().getLoad() == PluginLoadOrder.STARTUP && afterPlugin.getDescription().getLoad() == PluginLoadOrder.POSTWORLD) { return true; } return false; } /** * Determine if a plugin has a given dependency, either directly or indirectly. * @param plugin - the plugin to check. * @param dependency - the dependency to find. * @return TRUE if the plugin has the given dependency, FALSE otherwise. */ private boolean hasDependency(Plugin plugin, Plugin dependency) { return hasDependency(plugin, dependency, Sets.newHashSet()); } /** * Convert a list to a set. *

* A null list will be converted to an empty set. * @param list - the list to convert. * @return The converted list. */ private Set safeConversion(List list) { if (list == null) return Collections.emptySet(); else return Sets.newHashSet(list); } // Avoid cycles. DFS. private boolean hasDependency(Plugin plugin, Plugin dependency, Set checking) { Set childNames = Sets.union( safeConversion(plugin.getDescription().getDepend()), safeConversion(plugin.getDescription().getSoftDepend()) ); // Ensure that the same plugin isn't processed twice if (!checking.add(plugin.getName())) { throw new IllegalStateException("Cycle detected in dependency graph: " + plugin); } // Look for the dependency in the immediate children if (childNames.contains(dependency.getName())) { return true; } // Recurse through their dependencies for (String childName : childNames) { Plugin childPlugin = getPluginOrDefault(childName); if (childPlugin != null && hasDependency(childPlugin, dependency, checking)) { return true; } } // Cross edges are permitted checking.remove(plugin.getName()); // No dependency found! return false; } }