package bytecode.patchfile;
import installer.ProgressDialog;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class PatchFile {
// path -> list of alternative lists of hunks
public Map<String, List<List<PatchHunk>>> hunks = new HashMap<>();
public static PatchFile load(BufferedReader in) throws IOException {
String line;
// one object for each patch in the file (from +++ to next +++)
class PatchedFileInfo {
List<String> patchLines = new LinkedList<String>(); // XXX Why LinkedList? Why are we iterating over this with .remove(0) until empty?
int firstLineInPatchFileIndex;
}
List<PatchedFileInfo> splitLines = new ArrayList<>();
int currentLineInPatchFile = 0;
PatchedFileInfo curPatch = null;
while(true) {
line = in.readLine(); currentLineInPatchFile++;
if(line == null)
break;
if(line.startsWith("diff "))
continue;
if(line.startsWith("---")) {
curPatch = new PatchedFileInfo();
curPatch.firstLineInPatchFileIndex = currentLineInPatchFile;
splitLines.add(curPatch);
}
if(curPatch == null)
continue;
curPatch.patchLines.add(line);
}
PatchFile pf = new PatchFile();
Pattern hunkHeaderPattern = Pattern.compile("@@ -([0-9]+)(,[0-9]+|) \\+([0-9]+)(,[0-9]+|) @@");
for(PatchedFileInfo pfi : splitLines) {
List<String> patchLines = pfi.patchLines;
String path = patchLines.remove(0).substring(3).trim().replace("\\","/");
if(!patchLines.get(0).startsWith("+++"))
throw new IOException("corrupted patch file: --- not followed by +++ but by "+patchLines.get(0));
patchLines.remove(0);
if(path.contains("\t"))
path = path.substring(0, path.indexOf('\t'));
// XXX Minecraft-specific
if(path.startsWith("../src-base/")) path = path.substring(12);
if(path.startsWith("minecraft/")) path = path.substring(10);
currentLineInPatchFile = pfi.firstLineInPatchFileIndex + 1;
List<PatchHunk> patchHunks = new ArrayList<>();
while(!patchLines.isEmpty()) {
String header = patchLines.remove(0); currentLineInPatchFile++;
Matcher headerMatcher = hunkHeaderPattern.matcher(header);
PatchHunk h = new PatchHunk();
h.path = path;
if(headerMatcher.matches()) {
h.oldStart = Integer.parseInt(headerMatcher.group(1));
if(headerMatcher.group(2).equals(""))
h.oldCount = 1;
else
h.oldCount = Integer.parseInt(headerMatcher.group(2).substring(1));
h.newStart = Integer.parseInt(headerMatcher.group(3));
if(headerMatcher.group(4).equals(""))
h.newCount = 1;
else
h.newCount = Integer.parseInt(headerMatcher.group(4).substring(1));
} else
throw new IOException("corrupted patch file: unparseable hunk header: "+header+" on line " + currentLineInPatchFile);
while(h.oldLines.size() != h.oldCount || h.newLines.size() != h.newCount) {
if(h.oldLines.size() > h.oldCount || h.newLines.size() > h.newCount)
throw new IOException("corrupted patch file; line count mismatch, path is "+path);
line = patchLines.remove(0); currentLineInPatchFile++;
if(!line.startsWith("+")) h.oldLines.add(line.substring(1));
if(!line.startsWith("-")) h.newLines.add(line.substring(1));
}
patchHunks.add(h);
}
//System.err.println(path);
if(!pf.hunks.containsKey(path))
pf.hunks.put(path, new ArrayList<List<PatchHunk>>());
pf.hunks.get(path).add(patchHunks);
}
return pf;
}
public byte[] applyPatches(byte[] bytes, String path) {
return applyPatches(bytes, path, null);
}
// TODO: streaming is even faster than this in-memory list shuffling algorithm.
// See if we can remove this algorithm. Note that streaming requires an exactly applicable
// patch; it can't search for context.
public byte[] applyPatches(byte[] bytes, String path, ProgressDialog dlg) {
if(!hunks.containsKey(path))
return bytes;
// Split into lines, then trim \r (if we have Windows line endings).
// This uses one less copy of the input than the previous .replace("\r\n", "\n").split("\n") method.
List<String> lines = Arrays.asList(new String(bytes, StandardCharsets.UTF_8).split("\n"));
for(int k = 0; k < lines.size(); k++) {
String line = lines.get(k);
if(line.endsWith("\r"))
lines.set(k, line.substring(0, line.length() - 1));
}
// Measured time for the installer bytecode patching:
// 0.99s using applyPatchesStreaming instead
// 3.29s with this and TreeList
// 39.1s with this and ArrayList
// Over 5 minutes (after which I stopped waiting) with this and LinkedList (since it does not support efficient random access)
lines = new TreeList<>(lines);
List<List<PatchHunk>> alternatives = hunks.get(path);
if(alternatives.size() == 1) {
if(dlg != null) dlg.initProgressBar(0, alternatives.get(0).size());
for(PatchHunk hunk : alternatives.get(0))
try {
if(dlg != null) dlg.incrementProgress(1);
hunk.apply(lines);
} catch(RuntimeException e) {
throw new RuntimeException("Failed patching hunk -"+hunk.oldStart+","+hunk.oldCount, e);
}
} else {
boolean anyOK = false;
RuntimeException lastException = null;
ArrayList<String> origLines = new ArrayList<>(lines);
alternatives: for(List<PatchHunk> alternative : alternatives) {
lines.clear();
lines.addAll(origLines);
for(PatchHunk hunk : alternative)
try {
hunk.apply(lines);
} catch(RuntimeException e) {
lastException = new RuntimeException("Failed patching hunk -"+hunk.oldStart+","+hunk.oldCount, e);
continue alternatives;
}
anyOK = true;
break;
}
if(!anyOK)
throw lastException;
}
StringBuilder rv = new StringBuilder();
for(String l : lines) {
rv.append(l);
rv.append('\n');
}
return rv.toString().getBytes(StandardCharsets.UTF_8);
}
public void applyPatchesStreaming(BufferedReader in, PrintWriter out, String path, ProgressDialog dlg) throws IOException {
List<PatchHunk> hunks;
{
List<List<PatchHunk>> alternatives = this.hunks.get(path);
if(alternatives == null || alternatives.size() == 0) throw new RuntimeException("no hunks for "+path);
if(alternatives.size() != 1) throw new RuntimeException("can't try alternative patches when streaming (for "+path+")");
hunks = alternatives.get(0);
}
if(dlg != null)
dlg.initProgressBar(0, hunks.size());
StreamingPatchContext ctx = new StreamingPatchContext(in, out);
for(PatchHunk hunk : hunks)
try {
if(dlg != null)
dlg.incrementProgress(1);
hunk.applyStreaming(ctx);
} catch(RuntimeException e) {
throw new RuntimeException("Failed patching hunk -"+hunk.oldStart+","+hunk.oldCount, e);
}
ctx.skipRestOfFile();
}
}