1 /*
  2  * Copyright (c) 2014, 2024, 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.List;
 37 import java.util.Locale;
 38 import java.util.MissingResourceException;
 39 import java.util.function.Predicate;
 40 import java.util.stream.Collectors;
 41 import java.util.stream.Stream;
 42 import java.lang.classfile.ClassFile;
 43 import java.lang.classfile.CodeModel;
 44 import java.lang.classfile.MethodModel;
 45 
 46 import jdk.internal.jimage.BasicImageReader;
 47 import jdk.internal.jimage.ImageHeader;
 48 import jdk.internal.jimage.ImageLocation;
 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 
 94     JImageTask() {
 95         this.options = new OptionsValues();
 96         this.includePredicates = new ArrayList<>();
 97         log = null;
 98     }
 99 
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 ArrayList<>();
114     }
115 
116     enum Task {
117         EXTRACT,
118         INFO,
119         LIST,
120         VERIFY
121     };
122 
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 
154     private String pad(String string, int width) {
155         return pad(string, width, false);
156     }
157 
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 
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.ROOT));
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(Locale.ROOT)));
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 
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 
266     private void listTitle(File file, BasicImageReader reader) {
267         log.println("jimage: " + file);
268     }
269 
270     private interface JImageAction {
271         public void apply(File file, BasicImageReader reader) throws IOException, BadArgs;
272     }
273 
274     private interface ModuleAction {
275          public void apply(BasicImageReader reader,
276                  String oldModule, String newModule) throws IOException, BadArgs;
277     }
278 
279     private interface ResourceAction {
280         public void apply(BasicImageReader reader, String name,
281                 ImageLocation location) throws IOException, BadArgs;
282     }
283 
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 (location.getType() == ImageLocation.LocationType.RESOURCE) {
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 
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 
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 
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 
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 
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 
362     private void list(BasicImageReader reader, String name, ImageLocation location) {
363         print(reader, name);
364     }
365 
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                 ClassFile.of().parse(bytes).forEach(cle -> {
371                     if (cle instanceof MethodModel mm) mm.forEach(me -> {
372                         if (me instanceof CodeModel com) com.forEach(coe -> {
373                             //do nothing here, just visit each model element
374                         });
375                     });
376                 });
377             } catch (Exception ex) {
378                 log.println("Error(s) in Class: " + name);
379             }
380         }
381     }
382 
383     private void iterate(JImageAction jimageAction,
384             ModuleAction moduleAction,
385             ResourceAction resourceAction) throws IOException, BadArgs {
386         if (options.jimages.isEmpty()) {
387             throw TASK_HELPER.newBadArgs("err.no.jimage");
388         }
389 
390         for (File file : options.jimages) {
391             if (!file.exists() || !file.isFile()) {
392                 throw TASK_HELPER.newBadArgs("err.not.a.jimage", file);
393             }
394 
395             try (BasicImageReader reader = BasicImageReader.open(file.toPath())) {
396                 if (jimageAction != null) {
397                     jimageAction.apply(file, reader);
398                 }
399 
400                 if (resourceAction != null) {
401                     String[] entryNames = reader.getEntryNames();
402                     String oldModule = "";
403 
404                     for (String name : entryNames) {
405                         boolean match = includePredicates.isEmpty();
406 
407                         for (Predicate<String> predicate : includePredicates) {
408                             if (predicate.test(name)) {
409                                 match = true;
410                                 break;
411                             }
412                         }
413 
414                         if (!match) {
415                             continue;
416                         }
417 
418                         ImageLocation location = reader.findLocation(name);
419                         if (location.getType() == ImageLocation.LocationType.RESOURCE) {
420                             if (moduleAction != null) {
421                                 String newModule = location.getModule();
422                                 if (newModule.isEmpty()) {
423                                     newModule = "<unknown>";
424                                 }
425                                 if (!oldModule.equals(newModule)) {
426                                     moduleAction.apply(reader, oldModule, newModule);
427                                     oldModule = newModule;
428                                 }
429                             }
430                             resourceAction.apply(reader, name, location);
431                         }
432                     }
433                 }
434             } catch (IOException ioe) {
435                 throw TASK_HELPER.newBadArgs("err.invalid.jimage", file, ioe.getMessage());
436             }
437         }
438     }
439 
440     private boolean run() throws Exception, BadArgs {
441         switch (options.task) {
442             case EXTRACT:
443                 iterate(null, null, this::extract);
444                 break;
445             case INFO:
446                 iterate(this::info, null, null);
447                 break;
448             case LIST:
449                 iterate(this::listTitle, this::listModule, this::list);
450                 break;
451             case VERIFY:
452                 iterate(this::listTitle, null, this::verify);
453                 break;
454             default:
455                 throw TASK_HELPER.newBadArgs("err.not.a.task",
456                         options.task.name()).showUsage(true);
457         }
458         return true;
459     }
460 }