1 /*
2  * Copyright (c) 2014, 2020, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 package jdk.tools.jimage;
27 
28 import java.io.File;
29 import java.io.IOException;
30 import java.io.PrintWriter;
31 import java.nio.file.FileSystem;
32 import java.nio.file.Files;
33 import java.nio.file.PathMatcher;
34 import java.util.ArrayList;
35 import java.util.Arrays;
36 import java.util.LinkedList;
37 import java.util.List;
38 import java.util.Locale;
39 import java.util.MissingResourceException;
40 import java.util.function.Predicate;
41 import java.util.stream.Collectors;
42 import java.util.stream.Stream;
43 
44 import jdk.internal.jimage.BasicImageReader;
45 import jdk.internal.jimage.ImageHeader;
46 import jdk.internal.jimage.ImageLocation;
47 import jdk.internal.org.objectweb.asm.ClassReader;
48 import jdk.internal.org.objectweb.asm.tree.ClassNode;
49 import jdk.tools.jlink.internal.ImageResourcesTree;
50 import jdk.tools.jlink.internal.TaskHelper;
51 import jdk.tools.jlink.internal.TaskHelper.BadArgs;
52 import static jdk.tools.jlink.internal.TaskHelper.JIMAGE_BUNDLE;
53 import jdk.tools.jlink.internal.TaskHelper.Option;
54 import jdk.tools.jlink.internal.TaskHelper.OptionsHelper;
55 import jdk.tools.jlink.internal.Utils;
56 
57 class JImageTask {
58     private static final Option<?>[] RECOGNIZED_OPTIONS = {
59         new Option<JImageTask>(true, (task, option, arg) -> {
60             task.options.directory = arg;
61         }, "--dir"),
62 
63         new Option<JImageTask>(true, (task, option, arg) -> {
64             task.options.include = arg;
65         }, "--include"),
66 
67         new Option<JImageTask>(false, (task, option, arg) -> {
68             task.options.fullVersion = true;
69         }, true, "--full-version"),
70 
71         new Option<JImageTask>(false, (task, option, arg) -> {
72             task.options.help = true;
73         }, "--help", "-h", "-?"),
74 
75         new Option<JImageTask>(false, (task, option, arg) -> {
76             task.options.verbose = true;
77         }, "--verbose"),
78 
79         new Option<JImageTask>(false, (task, option, arg) -> {
80             task.options.version = true;
81         }, "--version")
82     };
83     private static final TaskHelper TASK_HELPER
84             = new TaskHelper(JIMAGE_BUNDLE);
85     private static final OptionsHelper<JImageTask> OPTION_HELPER
86             = TASK_HELPER.newOptionsHelper(JImageTask.class, RECOGNIZED_OPTIONS);
87     private static final String PROGNAME = "jimage";
88     private static final FileSystem JRT_FILE_SYSTEM = Utils.jrtFileSystem();
89 
90     private final OptionsValues options;
91     private final List<Predicate<String>> includePredicates;
92     private PrintWriter log;
93 
JImageTask()94     JImageTask() {
95         this.options = new OptionsValues();
96         this.includePredicates = new ArrayList<>();
97         log = null;
98     }
99 
setLog(PrintWriter out)100     void setLog(PrintWriter out) {
101         log = out;
102         TASK_HELPER.setLog(log);
103     }
104 
105     static class OptionsValues {
106         Task task = null;
107         String directory = ".";
108         String include = "";
109         boolean fullVersion;
110         boolean help;
111         boolean verbose;
112         boolean version;
113         List<File> jimages = new LinkedList<>();
114     }
115 
116     enum Task {
117         EXTRACT,
118         INFO,
119         LIST,
120         VERIFY
121     };
122 
pad(String string, int width, boolean justifyRight)123     private String pad(String string, int width, boolean justifyRight) {
124         int length = string.length();
125 
126         if (length == width) {
127             return string;
128         }
129 
130         if (length > width) {
131             return string.substring(0, width);
132         }
133 
134         int padding = width - length;
135 
136         StringBuilder sb = new StringBuilder(width);
137         if (justifyRight) {
138             for (int i = 0; i < padding; i++) {
139                 sb.append(' ');
140             }
141         }
142 
143         sb.append(string);
144 
145         if (!justifyRight) {
146             for (int i = 0; i < padding; i++) {
147                 sb.append(' ');
148             }
149         }
150 
151         return sb.toString();
152     }
153 
pad(String string, int width)154     private String pad(String string, int width) {
155         return pad(string, width, false);
156     }
157 
pad(long value, int width)158     private String pad(long value, int width) {
159         return pad(Long.toString(value), width, true);
160     }
161 
162     private static final int EXIT_OK = 0;        // No errors.
163     private static final int EXIT_ERROR = 1;     // Completed but reported errors.
164     private static final int EXIT_CMDERR = 2;    // Bad command-line arguments and/or switches.
165     private static final int EXIT_ABNORMAL = 4;  // Terminated abnormally.
166 
run(String[] args)167     int run(String[] args) {
168         if (log == null) {
169             setLog(new PrintWriter(System.out, true));
170         }
171 
172         if (args.length == 0) {
173             log.println(TASK_HELPER.getMessage("main.usage.summary", PROGNAME));
174             return EXIT_ABNORMAL;
175         }
176 
177         try {
178             String command;
179             String[] remaining = args;
180             try {
181                 command = args[0];
182                 options.task = Enum.valueOf(Task.class, args[0].toUpperCase(Locale.ENGLISH));
183                 remaining = args.length > 1 ? Arrays.copyOfRange(args, 1, args.length)
184                                             : new String[0];
185             } catch (IllegalArgumentException ex) {
186                 command = null;
187                 options.task = null;
188             }
189 
190             // process arguments
191             List<String> unhandled = OPTION_HELPER.handleOptions(this, remaining);
192             for (String f : unhandled) {
193                 options.jimages.add(new File(f));
194             }
195 
196             if (options.task == null && !options.help && !options.version && !options.fullVersion) {
197                 throw TASK_HELPER.newBadArgs("err.not.a.task",
198                     command != null ? command : "<unspecified>");
199             }
200 
201             if (options.help) {
202                 if (options.task == null) {
203                     log.println(TASK_HELPER.getMessage("main.usage", PROGNAME));
204                     Arrays.asList(RECOGNIZED_OPTIONS).stream()
205                         .filter(option -> !option.isHidden())
206                         .sorted()
207                         .forEach(option -> {
208                              log.println(TASK_HELPER.getMessage(option.resourceName()));
209                         });
210                     log.println(TASK_HELPER.getMessage("main.opt.footer"));
211                 } else {
212                     try {
213                         log.println(TASK_HELPER.getMessage("main.usage." +
214                                 options.task.toString().toLowerCase()));
215                     } catch (MissingResourceException ex) {
216                         throw TASK_HELPER.newBadArgs("err.not.a.task", command);
217                     }
218                 }
219                 return EXIT_OK;
220             }
221 
222             if (options.version || options.fullVersion) {
223                 if (options.task == null && !unhandled.isEmpty()) {
224                     throw TASK_HELPER.newBadArgs("err.not.a.task",
225                         Stream.of(args).collect(Collectors.joining(" ")));
226                 }
227 
228                 TASK_HELPER.showVersion(options.fullVersion);
229                 if (unhandled.isEmpty()) {
230                     return EXIT_OK;
231                 }
232             }
233 
234             processInclude(options.include);
235 
236             return run() ? EXIT_OK : EXIT_ERROR;
237         } catch (BadArgs e) {
238             TASK_HELPER.reportError(e.key, e.args);
239 
240             if (e.showUsage) {
241                 log.println(TASK_HELPER.getMessage("main.usage.summary", PROGNAME));
242             }
243 
244             return EXIT_CMDERR;
245         } catch (Exception x) {
246             x.printStackTrace();
247 
248             return EXIT_ABNORMAL;
249         } finally {
250             log.flush();
251         }
252     }
253 
processInclude(String include)254     private void processInclude(String include) {
255         if (include.isEmpty()) {
256             return;
257         }
258 
259         for (String filter : include.split(",")) {
260             final PathMatcher matcher = Utils.getPathMatcher(JRT_FILE_SYSTEM, filter);
261             Predicate<String> predicate = (path) -> matcher.matches(JRT_FILE_SYSTEM.getPath(path));
262             includePredicates.add(predicate);
263         }
264     }
265 
listTitle(File file, BasicImageReader reader)266     private void listTitle(File file, BasicImageReader reader) {
267         log.println("jimage: " + file);
268     }
269 
270     private interface JImageAction {
apply(File file, BasicImageReader reader)271         public void apply(File file, BasicImageReader reader) throws IOException, BadArgs;
272     }
273 
274     private interface ModuleAction {
apply(BasicImageReader reader, String oldModule, String newModule)275          public void apply(BasicImageReader reader,
276                  String oldModule, String newModule) throws IOException, BadArgs;
277     }
278 
279     private interface ResourceAction {
apply(BasicImageReader reader, String name, ImageLocation location)280         public void apply(BasicImageReader reader, String name,
281                 ImageLocation location) throws IOException, BadArgs;
282     }
283 
extract(BasicImageReader reader, String name, ImageLocation location)284     private void extract(BasicImageReader reader, String name,
285             ImageLocation location) throws IOException, BadArgs {
286         File directory = new File(options.directory);
287         byte[] bytes = reader.getResource(location);
288         File resource =  new File(directory, name);
289         File parent = resource.getParentFile();
290 
291         if (parent.exists()) {
292             if (!parent.isDirectory()) {
293                 throw TASK_HELPER.newBadArgs("err.cannot.create.dir",
294                                             parent.getAbsolutePath());
295             }
296         } else if (!parent.mkdirs()) {
297             throw TASK_HELPER.newBadArgs("err.cannot.create.dir",
298                                         parent.getAbsolutePath());
299         }
300 
301         if (!ImageResourcesTree.isTreeInfoResource(name)) {
302             Files.write(resource.toPath(), bytes);
303         }
304     }
305 
306     private static final int OFFSET_WIDTH = 12;
307     private static final int SIZE_WIDTH = 10;
308     private static final int COMPRESSEDSIZE_WIDTH = 10;
309 
trimModule(String name)310     private String trimModule(String name) {
311         int offset = name.indexOf('/', 1);
312 
313         if (offset != -1 && offset + 1 < name.length()) {
314             return name.substring(offset + 1);
315         }
316 
317         return name;
318     }
319 
print(String name, ImageLocation location)320     private void print(String name, ImageLocation location) {
321         log.print(pad(location.getContentOffset(), OFFSET_WIDTH) + " ");
322         log.print(pad(location.getUncompressedSize(), SIZE_WIDTH) + " ");
323         log.print(pad(location.getCompressedSize(), COMPRESSEDSIZE_WIDTH) + " ");
324         log.println(trimModule(name));
325     }
326 
print(BasicImageReader reader, String name)327     private void print(BasicImageReader reader, String name) {
328         if (options.verbose) {
329             print(name, reader.findLocation(name));
330         } else {
331             log.println("    " + trimModule(name));
332         }
333     }
334 
info(File file, BasicImageReader reader)335     private void info(File file, BasicImageReader reader) throws IOException {
336         ImageHeader header = reader.getHeader();
337 
338         log.println(" Major Version:  " + header.getMajorVersion());
339         log.println(" Minor Version:  " + header.getMinorVersion());
340         log.println(" Flags:          " + Integer.toHexString(header.getFlags()));
341         log.println(" Resource Count: " + header.getResourceCount());
342         log.println(" Table Length:   " + header.getTableLength());
343         log.println(" Offsets Size:   " + header.getOffsetsSize());
344         log.println(" Redirects Size: " + header.getRedirectSize());
345         log.println(" Locations Size: " + header.getLocationsSize());
346         log.println(" Strings Size:   " + header.getStringsSize());
347         log.println(" Index Size:     " + header.getIndexSize());
348     }
349 
listModule(BasicImageReader reader, String oldModule, String newModule)350     private void listModule(BasicImageReader reader, String oldModule, String newModule) {
351         log.println();
352         log.println("Module: " + newModule);
353 
354         if (options.verbose) {
355             log.print(pad("Offset", OFFSET_WIDTH) + " ");
356             log.print(pad("Size", SIZE_WIDTH) + " ");
357             log.print(pad("Compressed", COMPRESSEDSIZE_WIDTH) + " ");
358             log.println("Entry");
359         }
360     }
361 
list(BasicImageReader reader, String name, ImageLocation location)362     private void list(BasicImageReader reader, String name, ImageLocation location) {
363         print(reader, name);
364     }
365 
verify(BasicImageReader reader, String name, ImageLocation location)366       void verify(BasicImageReader reader, String name, ImageLocation location) {
367         if (name.endsWith(".class") && !name.endsWith("module-info.class")) {
368             try {
369                 byte[] bytes = reader.getResource(location);
370                 ClassReader cr = new ClassReader(bytes);
371                 ClassNode cn = new ClassNode();
372                 cr.accept(cn, 0);
373             } catch (Exception ex) {
374                 log.println("Error(s) in Class: " + name);
375             }
376         }
377     }
378 
iterate(JImageAction jimageAction, ModuleAction moduleAction, ResourceAction resourceAction)379     private void iterate(JImageAction jimageAction,
380             ModuleAction moduleAction,
381             ResourceAction resourceAction) throws IOException, BadArgs {
382         if (options.jimages.isEmpty()) {
383             throw TASK_HELPER.newBadArgs("err.no.jimage");
384         }
385 
386         for (File file : options.jimages) {
387             if (!file.exists() || !file.isFile()) {
388                 throw TASK_HELPER.newBadArgs("err.not.a.jimage", file);
389             }
390 
391             try (BasicImageReader reader = BasicImageReader.open(file.toPath())) {
392                 if (jimageAction != null) {
393                     jimageAction.apply(file, reader);
394                 }
395 
396                 if (resourceAction != null) {
397                     String[] entryNames = reader.getEntryNames();
398                     String oldModule = "";
399 
400                     for (String name : entryNames) {
401                         boolean match = includePredicates.isEmpty();
402 
403                         for (Predicate<String> predicate : includePredicates) {
404                             if (predicate.test(name)) {
405                                 match = true;
406                                 break;
407                             }
408                         }
409 
410                         if (!match) {
411                             continue;
412                         }
413 
414                         if (!ImageResourcesTree.isTreeInfoResource(name)) {
415                             if (moduleAction != null) {
416                                 int offset = name.indexOf('/', 1);
417 
418                                 String newModule = offset != -1 ?
419                                         name.substring(1, offset) :
420                                         "<unknown>";
421 
422                                 if (!oldModule.equals(newModule)) {
423                                     moduleAction.apply(reader, oldModule, newModule);
424                                     oldModule = newModule;
425                                 }
426                             }
427 
428                             ImageLocation location = reader.findLocation(name);
429                             resourceAction.apply(reader, name, location);
430                         }
431                     }
432                 }
433             } catch (IOException ioe) {
434                 throw TASK_HELPER.newBadArgs("err.invalid.jimage", file, ioe.getMessage());
435             }
436         }
437     }
438 
run()439     private boolean run() throws Exception, BadArgs {
440         switch (options.task) {
441             case EXTRACT:
442                 iterate(null, null, this::extract);
443                 break;
444             case INFO:
445                 iterate(this::info, null, null);
446                 break;
447             case LIST:
448                 iterate(this::listTitle, this::listModule, this::list);
449                 break;
450             case VERIFY:
451                 iterate(this::listTitle, null, this::verify);
452                 break;
453             default:
454                 throw TASK_HELPER.newBadArgs("err.not.a.task",
455                         options.task.name()).showUsage(true);
456         }
457         return true;
458     }
459 }
460