| /* | |
| * Copyright (c) 2014, 2024, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. Oracle designates this | |
| * particular file as subject to the "Classpath" exception as provided | |
| * by Oracle in the LICENSE file that accompanied this code. | |
| * | |
| * This code is distributed in the hope that it will be useful, but WITHOUT | |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| */ | |
| package jdk.tools.jimage; | |
| import java.io.File; | |
| import java.io.IOException; | |
| import java.io.PrintWriter; | |
| import java.nio.file.FileSystem; | |
| import java.nio.file.Files; | |
| import java.nio.file.PathMatcher; | |
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| import java.util.List; | |
| import java.util.Locale; | |
| import java.util.MissingResourceException; | |
| import java.util.function.Predicate; | |
| import java.util.stream.Collectors; | |
| import java.util.stream.Stream; | |
| import java.lang.classfile.ClassFile; | |
| import java.lang.classfile.CodeModel; | |
| import java.lang.classfile.MethodModel; | |
| import jdk.internal.jimage.BasicImageReader; | |
| import jdk.internal.jimage.ImageHeader; | |
| import jdk.internal.jimage.ImageLocation; | |
| import jdk.tools.jlink.internal.ImageResourcesTree; | |
| import jdk.tools.jlink.internal.TaskHelper; | |
| import jdk.tools.jlink.internal.TaskHelper.BadArgs; | |
| import static jdk.tools.jlink.internal.TaskHelper.JIMAGE_BUNDLE; | |
| import jdk.tools.jlink.internal.TaskHelper.Option; | |
| import jdk.tools.jlink.internal.TaskHelper.OptionsHelper; | |
| import jdk.tools.jlink.internal.Utils; | |
| class JImageTask { | |
| private static final Option<?>[] RECOGNIZED_OPTIONS = { | |
| new Option<JImageTask>(true, (task, option, arg) -> { | |
| task.options.directory = arg; | |
| }, "--dir"), | |
| new Option<JImageTask>(true, (task, option, arg) -> { | |
| task.options.include = arg; | |
| }, "--include"), | |
| new Option<JImageTask>(false, (task, option, arg) -> { | |
| task.options.fullVersion = true; | |
| }, true, "--full-version"), | |
| new Option<JImageTask>(false, (task, option, arg) -> { | |
| task.options.help = true; | |
| }, "--help", "-h", "-?"), | |
| new Option<JImageTask>(false, (task, option, arg) -> { | |
| task.options.verbose = true; | |
| }, "--verbose"), | |
| new Option<JImageTask>(false, (task, option, arg) -> { | |
| task.options.version = true; | |
| }, "--version") | |
| }; | |
| private static final TaskHelper TASK_HELPER | |
| = new TaskHelper(JIMAGE_BUNDLE); | |
| private static final OptionsHelper<JImageTask> OPTION_HELPER | |
| = TASK_HELPER.newOptionsHelper(JImageTask.class, RECOGNIZED_OPTIONS); | |
| private static final String PROGNAME = "jimage"; | |
| private static final FileSystem JRT_FILE_SYSTEM = Utils.jrtFileSystem(); | |
| private final OptionsValues options; | |
| private final List<Predicate<String>> includePredicates; | |
| private PrintWriter log; | |
| JImageTask() { | |
| this.options = new OptionsValues(); | |
| this.includePredicates = new ArrayList<>(); | |
| log = null; | |
| } | |
| void setLog(PrintWriter out) { | |
| log = out; | |
| TASK_HELPER.setLog(log); | |
| } | |
| static class OptionsValues { | |
| Task task = null; | |
| String directory = "."; | |
| String include = ""; | |
| boolean fullVersion; | |
| boolean help; | |
| boolean verbose; | |
| boolean version; | |
| List<File> jimages = new ArrayList<>(); | |
| } | |
| enum Task { | |
| EXTRACT, | |
| INFO, | |
| LIST, | |
| VERIFY | |
| }; | |
| private String pad(String string, int width, boolean justifyRight) { | |
| int length = string.length(); | |
| if (length == width) { | |
| return string; | |
| } | |
| if (length > width) { | |
| return string.substring(0, width); | |
| } | |
| int padding = width - length; | |
| StringBuilder sb = new StringBuilder(width); | |
| if (justifyRight) { | |
| for (int i = 0; i < padding; i++) { | |
| sb.append(' '); | |
| } | |
| } | |
| sb.append(string); | |
| if (!justifyRight) { | |
| for (int i = 0; i < padding; i++) { | |
| sb.append(' '); | |
| } | |
| } | |
| return sb.toString(); | |
| } | |
| private String pad(String string, int width) { | |
| return pad(string, width, false); | |
| } | |
| private String pad(long value, int width) { | |
| return pad(Long.toString(value), width, true); | |
| } | |
| private static final int EXIT_OK = 0; // No errors. | |
| private static final int EXIT_ERROR = 1; // Completed but reported errors. | |
| private static final int EXIT_CMDERR = 2; // Bad command-line arguments and/or switches. | |
| private static final int EXIT_ABNORMAL = 4; // Terminated abnormally. | |
| int run(String[] args) { | |
| if (log == null) { | |
| setLog(new PrintWriter(System.out, true)); | |
| } | |
| if (args.length == 0) { | |
| log.println(TASK_HELPER.getMessage("main.usage.summary", PROGNAME)); | |
| return EXIT_ABNORMAL; | |
| } | |
| try { | |
| String command; | |
| String[] remaining = args; | |
| try { | |
| command = args[0]; | |
| options.task = Enum.valueOf(Task.class, args[0].toUpperCase(Locale.ROOT)); | |
| remaining = args.length > 1 ? Arrays.copyOfRange(args, 1, args.length) | |
| : new String[0]; | |
| } catch (IllegalArgumentException ex) { | |
| command = null; | |
| options.task = null; | |
| } | |
| // process arguments | |
| List<String> unhandled = OPTION_HELPER.handleOptions(this, remaining); | |
| for (String f : unhandled) { | |
| options.jimages.add(new File(f)); | |
| } | |
| if (options.task == null && !options.help && !options.version && !options.fullVersion) { | |
| throw TASK_HELPER.newBadArgs("err.not.a.task", | |
| command != null ? command : "<unspecified>"); | |
| } | |
| if (options.help) { | |
| if (options.task == null) { | |
| log.println(TASK_HELPER.getMessage("main.usage", PROGNAME)); | |
| Arrays.asList(RECOGNIZED_OPTIONS).stream() | |
| .filter(option -> !option.isHidden()) | |
| .sorted() | |
| .forEach(option -> { | |
| log.println(TASK_HELPER.getMessage(option.resourceName())); | |
| }); | |
| log.println(TASK_HELPER.getMessage("main.opt.footer")); | |
| } else { | |
| try { | |
| log.println(TASK_HELPER.getMessage("main.usage." + | |
| options.task.toString().toLowerCase(Locale.ROOT))); | |
| } catch (MissingResourceException ex) { | |
| throw TASK_HELPER.newBadArgs("err.not.a.task", command); | |
| } | |
| } | |
| return EXIT_OK; | |
| } | |
| if (options.version || options.fullVersion) { | |
| if (options.task == null && !unhandled.isEmpty()) { | |
| throw TASK_HELPER.newBadArgs("err.not.a.task", | |
| Stream.of(args).collect(Collectors.joining(" "))); | |
| } | |
| TASK_HELPER.showVersion(options.fullVersion); | |
| if (unhandled.isEmpty()) { | |
| return EXIT_OK; | |
| } | |
| } | |
| processInclude(options.include); | |
| return run() ? EXIT_OK : EXIT_ERROR; | |
| } catch (BadArgs e) { | |
| TASK_HELPER.reportError(e.key, e.args); | |
| if (e.showUsage) { | |
| log.println(TASK_HELPER.getMessage("main.usage.summary", PROGNAME)); | |
| } | |
| return EXIT_CMDERR; | |
| } catch (Exception x) { | |
| x.printStackTrace(); | |
| return EXIT_ABNORMAL; | |
| } finally { | |
| log.flush(); | |
| } | |
| } | |
| private void processInclude(String include) { | |
| if (include.isEmpty()) { | |
| return; | |
| } | |
| for (String filter : include.split(",")) { | |
| final PathMatcher matcher = Utils.getPathMatcher(JRT_FILE_SYSTEM, filter); | |
| Predicate<String> predicate = (path) -> matcher.matches(JRT_FILE_SYSTEM.getPath(path)); | |
| includePredicates.add(predicate); | |
| } | |
| } | |
| private void listTitle(File file, BasicImageReader reader) { | |
| log.println("jimage: " + file); | |
| } | |
| private interface JImageAction { | |
| public void apply(File file, BasicImageReader reader) throws IOException, BadArgs; | |
| } | |
| private interface ModuleAction { | |
| public void apply(BasicImageReader reader, | |
| String oldModule, String newModule) throws IOException, BadArgs; | |
| } | |
| private interface ResourceAction { | |
| public void apply(BasicImageReader reader, String name, | |
| ImageLocation location) throws IOException, BadArgs; | |
| } | |
| private void extract(BasicImageReader reader, String name, | |
| ImageLocation location) throws IOException, BadArgs { | |
| File directory = new File(options.directory); | |
| byte[] bytes = reader.getResource(location); | |
| File resource = new File(directory, name); | |
| File parent = resource.getParentFile(); | |
| if (parent.exists()) { | |
| if (!parent.isDirectory()) { | |
| throw TASK_HELPER.newBadArgs("err.cannot.create.dir", | |
| parent.getAbsolutePath()); | |
| } | |
| } else if (!parent.mkdirs()) { | |
| throw TASK_HELPER.newBadArgs("err.cannot.create.dir", | |
| parent.getAbsolutePath()); | |
| } | |
| if (!ImageResourcesTree.isTreeInfoResource(name)) { | |
| Files.write(resource.toPath(), bytes); | |
| } | |
| } | |
| private static final int OFFSET_WIDTH = 12; | |
| private static final int SIZE_WIDTH = 10; | |
| private static final int COMPRESSEDSIZE_WIDTH = 10; | |
| private String trimModule(String name) { | |
| int offset = name.indexOf('/', 1); | |
| if (offset != -1 && offset + 1 < name.length()) { | |
| return name.substring(offset + 1); | |
| } | |
| return name; | |
| } | |
| private void print(String name, ImageLocation location) { | |
| log.print(pad(location.getContentOffset(), OFFSET_WIDTH) + " "); | |
| log.print(pad(location.getUncompressedSize(), SIZE_WIDTH) + " "); | |
| log.print(pad(location.getCompressedSize(), COMPRESSEDSIZE_WIDTH) + " "); | |
| log.println(trimModule(name)); | |
| } | |
| private void print(BasicImageReader reader, String name) { | |
| if (options.verbose) { | |
| print(name, reader.findLocation(name)); | |
| } else { | |
| log.println(" " + trimModule(name)); | |
| } | |
| } | |
| private void info(File file, BasicImageReader reader) throws IOException { | |
| ImageHeader header = reader.getHeader(); | |
| log.println(" Major Version: " + header.getMajorVersion()); | |
| log.println(" Minor Version: " + header.getMinorVersion()); | |
| log.println(" Flags: " + Integer.toHexString(header.getFlags())); | |
| log.println(" Resource Count: " + header.getResourceCount()); | |
| log.println(" Table Length: " + header.getTableLength()); | |
| log.println(" Offsets Size: " + header.getOffsetsSize()); | |
| log.println(" Redirects Size: " + header.getRedirectSize()); | |
| log.println(" Locations Size: " + header.getLocationsSize()); | |
| log.println(" Strings Size: " + header.getStringsSize()); | |
| log.println(" Index Size: " + header.getIndexSize()); | |
| } | |
| private void listModule(BasicImageReader reader, String oldModule, String newModule) { | |
| log.println(); | |
| log.println("Module: " + newModule); | |
| if (options.verbose) { | |
| log.print(pad("Offset", OFFSET_WIDTH) + " "); | |
| log.print(pad("Size", SIZE_WIDTH) + " "); | |
| log.print(pad("Compressed", COMPRESSEDSIZE_WIDTH) + " "); | |
| log.println("Entry"); | |
| } | |
| } | |
| private void list(BasicImageReader reader, String name, ImageLocation location) { | |
| print(reader, name); | |
| } | |
| void verify(BasicImageReader reader, String name, ImageLocation location) { | |
| if (name.endsWith(".class") && !name.endsWith("module-info.class")) { | |
| try { | |
| byte[] bytes = reader.getResource(location); | |
| ClassFile.of().parse(bytes).forEach(cle -> { | |
| if (cle instanceof MethodModel mm) mm.forEach(me -> { | |
| if (me instanceof CodeModel com) com.forEach(coe -> { | |
| //do nothing here, just visit each model element | |
| }); | |
| }); | |
| }); | |
| } catch (Exception ex) { | |
| log.println("Error(s) in Class: " + name); | |
| } | |
| } | |
| } | |
| private void iterate(JImageAction jimageAction, | |
| ModuleAction moduleAction, | |
| ResourceAction resourceAction) throws IOException, BadArgs { | |
| if (options.jimages.isEmpty()) { | |
| throw TASK_HELPER.newBadArgs("err.no.jimage"); | |
| } | |
| for (File file : options.jimages) { | |
| if (!file.exists() || !file.isFile()) { | |
| throw TASK_HELPER.newBadArgs("err.not.a.jimage", file); | |
| } | |
| try (BasicImageReader reader = BasicImageReader.open(file.toPath())) { | |
| if (jimageAction != null) { | |
| jimageAction.apply(file, reader); | |
| } | |
| if (resourceAction != null) { | |
| String[] entryNames = reader.getEntryNames(); | |
| String oldModule = ""; | |
| for (String name : entryNames) { | |
| boolean match = includePredicates.isEmpty(); | |
| for (Predicate<String> predicate : includePredicates) { | |
| if (predicate.test(name)) { | |
| match = true; | |
| break; | |
| } | |
| } | |
| if (!match) { | |
| continue; | |
| } | |
| if (!ImageResourcesTree.isTreeInfoResource(name)) { | |
| if (moduleAction != null) { | |
| int offset = name.indexOf('/', 1); | |
| String newModule = offset != -1 ? | |
| name.substring(1, offset) : | |
| "<unknown>"; | |
| if (!oldModule.equals(newModule)) { | |
| moduleAction.apply(reader, oldModule, newModule); | |
| oldModule = newModule; | |
| } | |
| } | |
| ImageLocation location = reader.findLocation(name); | |
| resourceAction.apply(reader, name, location); | |
| } | |
| } | |
| } | |
| } catch (IOException ioe) { | |
| throw TASK_HELPER.newBadArgs("err.invalid.jimage", file, ioe.getMessage()); | |
| } | |
| } | |
| } | |
| private boolean run() throws Exception, BadArgs { | |
| switch (options.task) { | |
| case EXTRACT: | |
| iterate(null, null, this::extract); | |
| break; | |
| case INFO: | |
| iterate(this::info, null, null); | |
| break; | |
| case LIST: | |
| iterate(this::listTitle, this::listModule, this::list); | |
| break; | |
| case VERIFY: | |
| iterate(this::listTitle, null, this::verify); | |
| break; | |
| default: | |
| throw TASK_HELPER.newBadArgs("err.not.a.task", | |
| options.task.name()).showUsage(true); | |
| } | |
| return true; | |
| } | |
| } |