8372301: Improve error message for jimage command line tool regarding version mismatch

Reviewed-by: alanb, rriggs
This commit is contained in:
David Beaumont 2026-02-17 19:14:17 +00:00 committed by Roger Riggs
parent 1b19261378
commit 92d0ea9ba8
4 changed files with 197 additions and 8 deletions

View File

@ -195,10 +195,9 @@ public class BasicImageReader implements AutoCloseable {
}
if (result.getMajorVersion() != ImageHeader.MAJOR_VERSION ||
result.getMinorVersion() != ImageHeader.MINOR_VERSION) {
throw new IOException("The image file \"" + name + "\" is not " +
"the correct version. Major: " + result.getMajorVersion() +
". Minor: " + result.getMinorVersion());
result.getMinorVersion() != ImageHeader.MINOR_VERSION) {
throw new ImageVersionMismatchException(
name, result.getMajorVersion(), result.getMinorVersion());
}
return result;
@ -447,4 +446,14 @@ public class BasicImageReader implements AutoCloseable {
return new ByteArrayInputStream(bytes);
}
public static final class ImageVersionMismatchException extends IOException {
@Deprecated
private static final long serialVersionUID = 1L;
// If needed we could capture major/minor version for use by JImageTask.
ImageVersionMismatchException(String name, int majorVersion, int minorVersion) {
super("The image file \"" + name + "\" is not the correct version. " +
"Major: " + majorVersion + ". Minor: " + minorVersion);
}
}
}

View File

@ -435,7 +435,10 @@ class JImageTask {
}
}
} catch (IOException ioe) {
throw TASK_HELPER.newBadArgs("err.invalid.jimage", file, ioe.getMessage());
boolean isVersionMismatch = ioe instanceof BasicImageReader.ImageVersionMismatchException;
// Both messages take the file name and underlying message.
String msgKey = isVersionMismatch ? "err.wrong.version" : "err.invalid.jimage";
throw TASK_HELPER.newBadArgs(msgKey, file, ioe.getMessage());
}
}
}

View File

@ -89,15 +89,20 @@ main.opt.footer=\
\ glob:<glob-pattern>\n\
\ regex:<regex-pattern>
err.not.a.task=task must be one of <extract | info | list | verify>: {0}
err.missing.arg=no value given for {0}
err.ambiguous.arg=value for option {0} starts with \"--\" should use {0}=<value> format
err.not.a.dir=not a directory: {0}
err.not.a.jimage=not a jimage file: {0}
err.invalid.jimage=Unable to open {0}: {1}
err.no.jimage=no jimage provided
err.option.unsupported={0} not supported: {1}
err.unknown.option=unknown option: {0}
err.cannot.create.dir=cannot create directory {0}
# General failure to open a jimage file.
# {0} = path of jimage file, {1} = underlying error message
err.invalid.jimage=Unable to open {0}: {1}
# More specific alternative for cases of version mismatch
err.wrong.version=Unable to open {0}: mismatched file and tool version\n\
Use ''<JAVA_HOME>/bin/jimage'' for the JDK associated with the jimage file:\n\
{1}

View File

@ -0,0 +1,172 @@
/*
* Copyright (c) 2026, 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.
*
* 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.
*/
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.function.Consumer;
import static java.nio.file.StandardOpenOption.READ;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static java.util.regex.Pattern.quote;
/*
* @test
* @summary Tests to verify behavior for "invalid" jimage files
* @library /test/lib
* @modules jdk.jlink/jdk.tools.jimage
* @build jdk.test.lib.Asserts
* @run main JImageBadFileTest
*/
public class JImageBadFileTest extends JImageCliTest {
// src/java.base/share/native/libjimage/imageFile.hpp
//
// 31 -------- bits -------- 0
// IDX +-------------------------+
// 0 | Magic (0xCAFEDADA) |
// +------------+------------+
// 1 | Major Vers | Minor Vers |
// +------------+------------+
// 2 | Flags |
// +-------------------------+
// 3 | Resource Count |
// +-------------------------+
// 4 | Table Length |
// +-------------------------+
// 5 | Attributes Size |
// +-------------------------+
// 6 | Strings Size |
// +-------------------------+
private static final int HEADER_SIZE_BYTES = 7 * 4;
/**
* Helper to copy the default jimage file for the runtime under test and
* allow it to be corrupted in various ways.
*
* @param label label for the temporary file (arbitrary debug name)
* @param maxLen maximum number of bytes to copy (-1 to copy all)
* @param headerFn function which may corrupt specific header values
* @return the path of a temporary jimage file in the test directory containing
* the possibly corrupted jimage file (caller should delete)
*/
private Path writeModifiedJimage(String label, int maxLen, Consumer<IntBuffer> headerFn)
throws IOException {
int remaining = maxLen >= 0 ? maxLen : Integer.MAX_VALUE;
Path dst = Files.createTempFile(Path.of("."), "modules-" + label, "");
try (InputStream rest = Files.newInputStream(Path.of(getImagePath()), READ);
OutputStream out = Files.newOutputStream(dst, TRUNCATE_EXISTING)) {
ByteBuffer bytes = ByteBuffer.wrap(rest.readNBytes(HEADER_SIZE_BYTES));
bytes.order(ByteOrder.nativeOrder());
headerFn.accept(bytes.asIntBuffer());
int headerSize = Math.min(remaining, HEADER_SIZE_BYTES);
out.write(bytes.array(), 0, headerSize);
remaining -= headerSize;
if (remaining > 0) {
byte[] block = new byte[8192];
do {
int copySize = Math.min(remaining, block.length);
out.write(block, 0, rest.readNBytes(block, 0, copySize));
remaining -= copySize;
} while (rest.available() > 0 && remaining > 0);
}
return dst.toAbsolutePath();
} catch (IOException e) {
Files.deleteIfExists(dst);
throw e;
}
}
public void testBadMagicNumber() throws IOException {
// Flip some bits in the magic number.
Path tempJimage = writeModifiedJimage("bad_magic", -1, b -> b.put(0, b.get(1) ^ 0x1010));
try {
JImageResult result = jimage("info", tempJimage.toString());
result.assertShowsError();
assertMatches(quote("Unable to open"), result.output);
assertMatches(quote("is not an image file"), result.output);
} finally {
Files.delete(tempJimage);
}
}
public void testMismatchedVersion() throws IOException {
// Add one to minor version (lowest bits).
Path tempJimage = writeModifiedJimage("bad_version", -1, b -> b.put(1, b.get(1) + 1));
try {
JImageResult result = jimage("info", tempJimage.toString());
result.assertShowsError();
assertMatches(quote("Unable to open"), result.output);
assertMatches(quote("<JAVA_HOME>/bin/jimage"), result.output);
assertMatches(quote("not the correct version"), result.output);
assertMatches("Major: \\d+", result.output);
assertMatches("Minor: \\d+", result.output);
} finally {
Files.delete(tempJimage);
}
}
public void testTruncatedHeader() throws IOException {
// Copy less than the header.
Path tempJimage = writeModifiedJimage("truncated_header", HEADER_SIZE_BYTES - 4, b -> {});
try {
JImageResult result = jimage("info", tempJimage.toString());
result.assertShowsError();
assertMatches(quote("Unable to open"), result.output);
assertMatches(quote("is not an image file"), result.output);
} finally {
Files.delete(tempJimage);
}
}
public void testTruncatedData() throws IOException {
// Copy more than the header, but definitely less than the whole file.
Path tempJimage = writeModifiedJimage("truncated_data", HEADER_SIZE_BYTES + 1024, b -> {});
try {
JImageResult result = jimage("info", tempJimage.toString());
result.assertShowsError();
assertMatches(quote("Unable to open"), result.output);
assertMatches("image file \".*\" is corrupted", result.output);
} finally {
Files.delete(tempJimage);
}
}
public void testGoodFileCopy() throws IOException {
// Self test that the file copying isn't itself corrupting anything.
Path tempJimage = writeModifiedJimage("good_file", -1, b -> {});
try {
jimage("info", tempJimage.toString()).assertSuccess();
} finally {
Files.delete(tempJimage);
}
}
public static void main(String[] args) throws Throwable {
new JImageBadFileTest().runTests();
}
}