2 // Getdown - application installer, patcher and launcher
3 // Copyright (C) 2004-2018 Getdown authors
4 // https://github.com/threerings/getdown/blob/master/LICENSE
6 package com.threerings.getdown.tools;
9 import java.io.FileOutputStream;
10 import java.io.IOException;
11 import java.io.InputStream;
12 import java.io.InputStreamReader;
13 import java.io.LineNumberReader;
15 import java.util.ArrayList;
16 import java.util.Enumeration;
17 import java.util.HashMap;
18 import java.util.HashSet;
19 import java.util.Iterator;
20 import java.util.List;
24 import java.util.jar.JarEntry;
25 import java.util.jar.JarFile;
26 import java.util.jar.JarOutputStream;
28 import com.threerings.getdown.util.ProgressObserver;
30 import static java.nio.charset.StandardCharsets.UTF_8;
33 * Applies a jardiff patch to a jar file.
35 public class JarDiffPatcher implements JarDiffCodes
38 * Patches the specified jar file using the supplied patch file and writing
39 * the new jar file to the supplied target.
41 * @param jarPath the path to the original jar file.
42 * @param diffPath the path to the jardiff patch file.
43 * @param target the output stream to which we will write the patched jar.
44 * @param observer an optional observer to be notified of patching progress.
46 * @throws IOException if any problem occurs during patching.
48 public void patchJar (String jarPath, String diffPath, File target, ProgressObserver observer)
51 File oldFile = new File(jarPath), diffFile = new File(diffPath);
53 try (JarFile oldJar = new JarFile(oldFile);
54 JarFile jarDiff = new JarFile(diffFile);
55 JarOutputStream jos = new JarOutputStream(new FileOutputStream(target))) {
57 Set<String> ignoreSet = new HashSet<>();
58 Map<String, String> renameMap = new HashMap<>();
59 determineNameMapping(jarDiff, ignoreSet, renameMap);
61 // get all keys in renameMap
62 String[] keys = renameMap.keySet().toArray(new String[renameMap.size()]);
64 // Files to implicit move
65 Set<String> oldjarNames = new HashSet<>();
66 Enumeration<JarEntry> oldEntries = oldJar.entries();
67 if (oldEntries != null) {
68 while (oldEntries.hasMoreElements()) {
69 oldjarNames.add(oldEntries.nextElement().getName());
73 // size depends on the three parameters below, which is basically the
74 // counter for each loop that do the actual writes to the output file
75 // since oldjarNames.size() changes in the first two loop below, we
76 // need to adjust the size accordingly also when oldjarNames.size()
78 double size = oldjarNames.size() + keys.length + jarDiff.size();
79 double currentEntry = 0;
81 // Handle all remove commands
82 oldjarNames.removeAll(ignoreSet);
83 size -= ignoreSet.size();
85 // Add content from JARDiff
86 Enumeration<JarEntry> entries = jarDiff.entries();
87 if (entries != null) {
88 while (entries.hasMoreElements()) {
89 JarEntry entry = entries.nextElement();
90 if (!INDEX_NAME.equals(entry.getName())) {
91 updateObserver(observer, currentEntry, size);
93 writeEntry(jos, entry, jarDiff);
95 // Remove entry from oldjarNames since no implicit move is
97 boolean wasInOld = oldjarNames.remove(entry.getName());
99 // Update progress counters. If it was in old, we do not
100 // need an implicit move, so adjust total size.
106 // no write is done, decrement size
112 // go through the renameMap and apply move for each entry
113 for (String newName : keys) {
114 // Apply move <oldName> <newName> command
115 String oldName = renameMap.get(newName);
117 // Get source JarEntry
118 JarEntry oldEntry = oldJar.getJarEntry(oldName);
119 if (oldEntry == null) {
120 String moveCmd = MOVE_COMMAND + oldName + " " + newName;
121 throw new IOException("error.badmove: " + moveCmd);
124 // Create dest JarEntry
125 JarEntry newEntry = new JarEntry(newName);
126 newEntry.setTime(oldEntry.getTime());
127 newEntry.setSize(oldEntry.getSize());
128 newEntry.setCompressedSize(oldEntry.getCompressedSize());
129 newEntry.setCrc(oldEntry.getCrc());
130 newEntry.setMethod(oldEntry.getMethod());
131 newEntry.setExtra(oldEntry.getExtra());
132 newEntry.setComment(oldEntry.getComment());
134 updateObserver(observer, currentEntry, size);
137 try (InputStream data = oldJar.getInputStream(oldEntry)) {
138 writeEntry(jos, newEntry, data);
141 // Remove entry from oldjarNames since no implicit move is needed
142 boolean wasInOld = oldjarNames.remove(oldName);
144 // Update progress counters. If it was in old, we do not need an
145 // implicit move, so adjust total size.
152 Iterator<String> iEntries = oldjarNames.iterator();
153 if (iEntries != null) {
154 while (iEntries.hasNext()) {
155 String name = iEntries.next();
156 JarEntry entry = oldJar.getJarEntry(name);
158 // names originally retrieved from the JAR, so this should never happen
159 throw new AssertionError("JAR entry not found: " + name);
161 updateObserver(observer, currentEntry, size);
163 writeEntry(jos, entry, oldJar);
166 updateObserver(observer, currentEntry, size);
170 protected void updateObserver (ProgressObserver observer, double currentSize, double size)
172 if (observer != null) {
173 observer.progress((int)(100*currentSize/size));
177 protected void determineNameMapping (
178 JarFile jarDiff, Set<String> ignoreSet, Map<String, String> renameMap)
181 InputStream is = jarDiff.getInputStream(jarDiff.getEntry(INDEX_NAME));
183 throw new IOException("error.noindex");
186 LineNumberReader indexReader =
187 new LineNumberReader(new InputStreamReader(is, UTF_8));
188 String line = indexReader.readLine();
189 if (line == null || !line.equals(VERSION_HEADER)) {
190 throw new IOException("jardiff.error.badheader: " + line);
193 while ((line = indexReader.readLine()) != null) {
194 if (line.startsWith(REMOVE_COMMAND)) {
195 List<String> sub = getSubpaths(
196 line.substring(REMOVE_COMMAND.length()));
198 if (sub.size() != 1) {
199 throw new IOException("error.badremove: " + line);
201 ignoreSet.add(sub.get(0));
203 } else if (line.startsWith(MOVE_COMMAND)) {
204 List<String> sub = getSubpaths(
205 line.substring(MOVE_COMMAND.length()));
206 if (sub.size() != 2) {
207 throw new IOException("error.badmove: " + line);
210 // target of move should be the key
211 if (renameMap.put(sub.get(1), sub.get(0)) != null) {
212 // invalid move - should not move to same target twice
213 throw new IOException("error.badmove: " + line);
216 } else if (line.length() > 0) {
217 throw new IOException("error.badcommand: " + line);
222 protected List<String> getSubpaths (String path)
225 int length = path.length();
226 ArrayList<String> sub = new ArrayList<>();
228 while (index < length) {
229 while (index < length && Character.isWhitespace
230 (path.charAt(index))) {
233 if (index < length) {
236 String subString = null;
238 while (index < length) {
239 char aChar = path.charAt(index);
240 if (aChar == '\\' && (index + 1) < length &&
241 path.charAt(index + 1) == ' ') {
243 if (subString == null) {
244 subString = path.substring(last, index);
246 subString += path.substring(last, index);
249 } else if (Character.isWhitespace(aChar)) {
255 if (subString == null) {
256 subString = path.substring(last, index);
258 subString += path.substring(last, index);
267 protected void writeEntry (JarOutputStream jos, JarEntry entry, JarFile file)
270 try (InputStream data = file.getInputStream(entry)) {
271 writeEntry(jos, entry, data);
275 protected void writeEntry (JarOutputStream jos, JarEntry entry, InputStream data)
278 jos.putNextEntry(new JarEntry(entry.getName()));
281 int size = data.read(newBytes);
283 jos.write(newBytes, 0, size);
284 size = data.read(newBytes);
288 protected static final int DEFAULT_READ_SIZE = 2048;
290 protected static byte[] newBytes = new byte[DEFAULT_READ_SIZE];
291 protected static byte[] oldBytes = new byte[DEFAULT_READ_SIZE];