package jk_5.nailed.server.tweaker.patcher;
import LZMA.LzmaInputStream;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.hash.Hashing;
import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import com.nothome.delta.GDiffPatcher;
import net.minecraft.launchwrapper.LaunchClassLoader;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.*;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
import java.util.jar.Pack200;
import java.util.regex.Pattern;
public class BinPatchManager {
private static final boolean dumpPatched = System.getProperty("nailed.dumpPatchedClasses", "false").equalsIgnoreCase("true");
private static final boolean ignorePatchDiscrepancies = System.getProperty("nailed.ignorePatchDiscrepancies", "false").equalsIgnoreCase("true");
private static final Logger logger = LogManager.getLogger();
private static final BinPatchManager INSTANCE = new BinPatchManager();
private final GDiffPatcher patcher = new GDiffPatcher();
private Multimap<String, ClassPatch> patches;
private Map<String, byte[]> cache = new HashMap<String, byte[]>();
private File tempDir;
public BinPatchManager() {
if(dumpPatched){
//try{
tempDir = Files.createTempDir();
logger.info("Dumping patched classes to {}", tempDir.getAbsolutePath());
//}catch(IOException e){
//}
}
}
public byte[] getPatchedResource(String name, String mappedName, LaunchClassLoader loader) throws IOException {
return applyPatch(name, mappedName, loader.getClassBytes(name));
}
public byte[] applyPatch(String name, String mappedName, byte[] inputData){
if(this.patches == null){
return inputData;
}
if(this.cache.containsKey(name)){
return this.cache.get(name);
}
Collection<ClassPatch> list = patches.get(name);
if(list.isEmpty()){
return inputData;
}
for(ClassPatch patch : list){
if(!patch.targetClassName.equals(mappedName) && !patch.sourceClassName.equals(name)){
logger.warn("Binary patch found %s for wrong class %s", patch.targetClassName, mappedName);
}
if(!patch.existsAtTarget && (inputData == null || inputData.length == 0)){
inputData = new byte[0];
}else if(!patch.existsAtTarget){
logger.warn("Patcher expecting empty class data file for %s, but received non-empty", patch.targetClassName);
}else{
int inputChecksum = Hashing.adler32().hashBytes(inputData).asInt();
if(patch.inputChecksum != inputChecksum){
logger.fatal("There is a binary discrepency between the expected input class %s (%s) and the actual class. Checksum on disk is %x, in patch %x. Things are probably about to go very wrong. Did you put something into the jar file?", mappedName, name, inputChecksum, patch.inputChecksum);
if(!ignorePatchDiscrepancies){
logger.fatal("Server is shutting down now! (You can try doing -Dnailed.ignorePatchDiscrepancies=true to ignore this error)");
System.exit(1);
}else{
logger.warn("We are going to ignore this error. Chances are that the server won't be able to load properly");
continue;
}
}
}
synchronized(patcher){
try{
inputData = patcher.patch(inputData, patch.patch);
}catch (IOException e){
logger.error("Encountered a problem while runtime patching class " + name, e);
}
}
}
if(dumpPatched){
try{
Files.write(inputData, new File(tempDir,mappedName));
}catch(IOException e){
logger.error("Failed to write patched class " + mappedName + " to " + tempDir.getAbsolutePath(), e);
}
}
cache.put(name, inputData);
return inputData;
}
public void setup(){
logger.info("Loading binary patches...");
Pattern binpatchMatcher = Pattern.compile("binpatch/server/.*.binpatch");
JarInputStream jis = null;
try{
InputStream compressed = this.getClass().getResourceAsStream("/binpatches.pack.lzma");
if(compressed == null){
logger.warn("Was not able to find binary patches. Assuming development environment");
return;
}
LzmaInputStream decompressed = new LzmaInputStream(compressed);
ByteArrayOutputStream jarBytes = new ByteArrayOutputStream();
JarOutputStream jos = new JarOutputStream(jarBytes);
Pack200.newUnpacker().unpack(decompressed, jos);
jis = new JarInputStream(new ByteArrayInputStream(jarBytes.toByteArray()));
}catch(Exception e){
logger.error("Error occurred while reading binary patches", e);
throw new RuntimeException(e);
}
this.patches = ArrayListMultimap.create();
while(true){
try{
JarEntry entry = jis.getNextJarEntry();
if(entry == null){
break;
}
if(binpatchMatcher.matcher(entry.getName()).matches()){
ClassPatch patch = readPatch(entry, jis);
if(patch != null){
patches.put(patch.sourceClassName, patch);
}
}else{
jis.closeEntry();
}
}catch(IOException e){
}
}
logger.info("Successfully loaded {} binary patches", patches.size());
cache.clear();
}
private ClassPatch readPatch(JarEntry entry, JarInputStream jis){
ByteArrayDataInput input;
try{
input = ByteStreams.newDataInput(ByteStreams.toByteArray(jis));
}catch(IOException e){
logger.warn("Unable to read binpatch file {}. Ignoring it", entry.getName());
return null;
}
String name = input.readUTF();
String sourceName = input.readUTF();
String targetName = input.readUTF();
boolean exists = input.readBoolean();
int inputChecksum = exists ? input.readInt() : 0;
int patchLength = input.readInt();
byte[] patchBytes = new byte[patchLength];
input.readFully(patchBytes);
return new ClassPatch(name, sourceName, targetName, exists, inputChecksum, patchBytes);
}
public static BinPatchManager instance(){
return INSTANCE;
}
}