JAL-3130 adapted getdown src. attempt 2. first attempt failed due to cp'ed .git files
[jalview.git] / getdown / src / getdown / core / src / main / java / com / threerings / getdown / tools / JarDiffPatcher.java
1 //
2 // Getdown - application installer, patcher and launcher
3 // Copyright (C) 2004-2018 Getdown authors
4 // https://github.com/threerings/getdown/blob/master/LICENSE
5
6 package com.threerings.getdown.tools;
7
8 import java.io.File;
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;
14
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;
21 import java.util.Map;
22 import java.util.Set;
23
24 import java.util.jar.JarEntry;
25 import java.util.jar.JarFile;
26 import java.util.jar.JarOutputStream;
27
28 import com.threerings.getdown.util.ProgressObserver;
29
30 import static java.nio.charset.StandardCharsets.UTF_8;
31
32 /**
33  * Applies a jardiff patch to a jar file.
34  */
35 public class JarDiffPatcher implements JarDiffCodes
36 {
37     /**
38      * Patches the specified jar file using the supplied patch file and writing
39      * the new jar file to the supplied target.
40      *
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.
45      *
46      * @throws IOException if any problem occurs during patching.
47      */
48     public void patchJar (String jarPath, String diffPath, File target, ProgressObserver observer)
49         throws IOException
50     {
51         File oldFile = new File(jarPath), diffFile = new File(diffPath);
52
53         try (JarFile oldJar = new JarFile(oldFile);
54              JarFile jarDiff = new JarFile(diffFile);
55              JarOutputStream jos = new JarOutputStream(new FileOutputStream(target))) {
56
57             Set<String> ignoreSet = new HashSet<>();
58             Map<String, String> renameMap = new HashMap<>();
59             determineNameMapping(jarDiff, ignoreSet, renameMap);
60
61             // get all keys in renameMap
62             String[] keys = renameMap.keySet().toArray(new String[renameMap.size()]);
63
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());
70                 }
71             }
72
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()
77             // changes
78             double size = oldjarNames.size() + keys.length + jarDiff.size();
79             double currentEntry = 0;
80
81             // Handle all remove commands
82             oldjarNames.removeAll(ignoreSet);
83             size -= ignoreSet.size();
84
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);
92                         currentEntry++;
93                         writeEntry(jos, entry, jarDiff);
94
95                         // Remove entry from oldjarNames since no implicit move is
96                         // needed
97                         boolean wasInOld = oldjarNames.remove(entry.getName());
98
99                         // Update progress counters. If it was in old, we do not
100                         // need an implicit move, so adjust total size.
101                         if (wasInOld) {
102                             size--;
103                         }
104
105                     } else {
106                         // no write is done, decrement size
107                         size--;
108                     }
109                 }
110             }
111
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);
116
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);
122                 }
123
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());
133
134                 updateObserver(observer, currentEntry, size);
135                 currentEntry++;
136
137                 try (InputStream data = oldJar.getInputStream(oldEntry)) {
138                     writeEntry(jos, newEntry, data);
139                 }
140
141                 // Remove entry from oldjarNames since no implicit move is needed
142                 boolean wasInOld = oldjarNames.remove(oldName);
143
144                 // Update progress counters. If it was in old, we do not need an
145                 // implicit move, so adjust total size.
146                 if (wasInOld) {
147                     size--;
148                 }
149             }
150
151             // implicit move
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);
157                     if (entry == null) {
158                         // names originally retrieved from the JAR, so this should never happen
159                         throw new AssertionError("JAR entry not found: " + name);
160                     }
161                     updateObserver(observer, currentEntry, size);
162                     currentEntry++;
163                     writeEntry(jos, entry, oldJar);
164                 }
165             }
166             updateObserver(observer, currentEntry, size);
167         }
168     }
169
170     protected void updateObserver (ProgressObserver observer, double currentSize, double size)
171     {
172         if (observer != null) {
173             observer.progress((int)(100*currentSize/size));
174         }
175     }
176
177     protected void determineNameMapping (
178         JarFile jarDiff, Set<String> ignoreSet, Map<String, String> renameMap)
179         throws IOException
180     {
181         InputStream is = jarDiff.getInputStream(jarDiff.getEntry(INDEX_NAME));
182         if (is == null) {
183             throw new IOException("error.noindex");
184         }
185
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);
191         }
192
193         while ((line = indexReader.readLine()) != null) {
194             if (line.startsWith(REMOVE_COMMAND)) {
195                 List<String> sub = getSubpaths(
196                     line.substring(REMOVE_COMMAND.length()));
197
198                 if (sub.size() != 1) {
199                     throw new IOException("error.badremove: " + line);
200                 }
201                 ignoreSet.add(sub.get(0));
202
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);
208                 }
209
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);
214                 }
215
216             } else if (line.length() > 0) {
217                 throw new IOException("error.badcommand: " + line);
218             }
219         }
220     }
221
222     protected List<String> getSubpaths (String path)
223     {
224         int index = 0;
225         int length = path.length();
226         ArrayList<String> sub = new ArrayList<>();
227
228         while (index < length) {
229             while (index < length && Character.isWhitespace
230                    (path.charAt(index))) {
231                 index++;
232             }
233             if (index < length) {
234                 int start = index;
235                 int last = start;
236                 String subString = null;
237
238                 while (index < length) {
239                     char aChar = path.charAt(index);
240                     if (aChar == '\\' && (index + 1) < length &&
241                         path.charAt(index + 1) == ' ') {
242
243                         if (subString == null) {
244                             subString = path.substring(last, index);
245                         } else {
246                             subString += path.substring(last, index);
247                         }
248                         last = ++index;
249                     } else if (Character.isWhitespace(aChar)) {
250                         break;
251                     }
252                     index++;
253                 }
254                 if (last != index) {
255                     if (subString == null) {
256                         subString = path.substring(last, index);
257                     } else {
258                         subString += path.substring(last, index);
259                     }
260                 }
261                 sub.add(subString);
262             }
263         }
264         return sub;
265     }
266
267     protected void writeEntry (JarOutputStream jos, JarEntry entry, JarFile file)
268         throws IOException
269     {
270         try (InputStream data = file.getInputStream(entry)) {
271             writeEntry(jos, entry, data);
272         }
273     }
274
275     protected void writeEntry (JarOutputStream jos, JarEntry entry, InputStream data)
276         throws IOException
277     {
278         jos.putNextEntry(new JarEntry(entry.getName()));
279
280         // Read the entry
281         int size = data.read(newBytes);
282         while (size != -1) {
283             jos.write(newBytes, 0, size);
284             size = data.read(newBytes);
285         }
286     }
287
288     protected static final int DEFAULT_READ_SIZE = 2048;
289
290     protected static byte[] newBytes = new byte[DEFAULT_READ_SIZE];
291     protected static byte[] oldBytes = new byte[DEFAULT_READ_SIZE];
292 }