Skip to content
Permalink
Browse files
8266835: Add a --validate option to the jar tool
Reviewed-by: lancea
  • Loading branch information
JornVernee committed Jun 9, 2021
1 parent db45ff0 commit 79010f2254aee8459523800d6049f396b055f123
Showing 4 changed files with 146 additions and 33 deletions.
@@ -58,47 +58,54 @@ BadArgs showUsage(boolean b) {
// Main operations
new Option(false, OptionType.MAIN_OPERATION, "--create", "-c") {
void process(Main tool, String opt, String arg) throws BadArgs {
if (tool.iflag || tool.tflag || tool.uflag || tool.xflag || tool.dflag)
if (tool.iflag || tool.tflag || tool.uflag || tool.xflag || tool.dflag || tool.validate)
throw new BadArgs("error.multiple.main.operations").showUsage(true);
tool.cflag = true;
}
},
new Option(true, OptionType.MAIN_OPERATION, "--generate-index", "-i") {
void process(Main tool, String opt, String arg) throws BadArgs {
if (tool.cflag || tool.tflag || tool.uflag || tool.xflag || tool.dflag)
if (tool.cflag || tool.tflag || tool.uflag || tool.xflag || tool.dflag || tool.validate)
throw new BadArgs("error.multiple.main.operations").showUsage(true);
tool.iflag = true;
tool.rootjar = arg;
}
},
new Option(false, OptionType.MAIN_OPERATION, "--list", "-t") {
void process(Main tool, String opt, String arg) throws BadArgs {
if (tool.cflag || tool.iflag || tool.uflag || tool.xflag || tool.dflag)
if (tool.cflag || tool.iflag || tool.uflag || tool.xflag || tool.dflag || tool.validate)
throw new BadArgs("error.multiple.main.operations").showUsage(true);
tool.tflag = true;
}
},
new Option(false, OptionType.MAIN_OPERATION, "--update", "-u") {
void process(Main tool, String opt, String arg) throws BadArgs {
if (tool.cflag || tool.iflag || tool.tflag || tool.xflag || tool.dflag)
if (tool.cflag || tool.iflag || tool.tflag || tool.xflag || tool.dflag || tool.validate)
throw new BadArgs("error.multiple.main.operations").showUsage(true);
tool.uflag = true;
}
},
new Option(false, OptionType.MAIN_OPERATION, "--extract", "-x") {
void process(Main tool, String opt, String arg) throws BadArgs {
if (tool.cflag || tool.iflag || tool.tflag || tool.uflag || tool.dflag)
if (tool.cflag || tool.iflag || tool.tflag || tool.uflag || tool.dflag || tool.validate)
throw new BadArgs("error.multiple.main.operations").showUsage(true);
tool.xflag = true;
}
},
new Option(false, OptionType.MAIN_OPERATION, "--describe-module", "-d") {
void process(Main tool, String opt, String arg) throws BadArgs {
if (tool.cflag || tool.iflag || tool.tflag || tool.uflag || tool.xflag)
if (tool.cflag || tool.iflag || tool.tflag || tool.uflag || tool.xflag || tool.validate)
throw new BadArgs("error.multiple.main.operations").showUsage(true);
tool.dflag = true;
}
},
new Option(false, OptionType.MAIN_OPERATION, "--validate") {
void process(Main tool, String opt, String arg) throws BadArgs {
if (tool.cflag || tool.iflag || tool.tflag || tool.uflag || tool.xflag || tool.dflag)
throw new BadArgs("error.multiple.main.operations").showUsage(true);
tool.validate = true;
}
},

// Additional options
new Option(true, OptionType.ANY, "--file", "-f") {
@@ -150,7 +150,7 @@ public int hashCode() {
* pflag: preserve/don't strip leading slash and .. component from file name
* dflag: print module descriptor
*/
boolean cflag, uflag, xflag, tflag, vflag, flag0, Mflag, iflag, pflag, dflag;
boolean cflag, uflag, xflag, tflag, vflag, flag0, Mflag, iflag, pflag, dflag, validate;

boolean suppressDeprecateMsg = false;

@@ -193,7 +193,7 @@ static String getMsg(String key) {
try {
return (rsrc.getString(key));
} catch (MissingResourceException e) {
throw new Error("Error in message file");
throw new Error("Error in message file", e);
}
}

@@ -401,6 +401,17 @@ public synchronized boolean run(String args[]) {
}
if (!found)
error(getMsg("error.module.descriptor.not.found"));
} else if (validate) {
File file;
if (fname != null) {
file = new File(fname);
} else {
file = createTemporaryFile("tmpJar", ".jar");
try (InputStream in = new FileInputStream(FileDescriptor.in)) {
Files.copy(in, file.toPath());
}
}
ok = validateJar(file);
}
} catch (IOException e) {
fatalError(e);
@@ -420,15 +431,20 @@ public synchronized boolean run(String args[]) {
return ok;
}

private boolean validateJar(File file) throws IOException {
try (ZipFile zf = new ZipFile(file)) {
return Validator.validate(this, zf);
} catch (IOException e) {
error(formatMsg2("error.validator.jarfile.exception", fname, e.getMessage()));
return true;
}
}

private void validateAndClose(File tmpfile) throws IOException {
if (ok && isMultiRelease) {
try (ZipFile zf = new ZipFile(tmpfile)) {
ok = Validator.validate(this, zf);
if (!ok) {
error(formatMsg("error.validator.jarfile.invalid", fname));
}
} catch (IOException e) {
error(formatMsg2("error.validator.jarfile.exception", fname, e.getMessage()));
ok = validateJar(tmpfile);
if (!ok) {
error(formatMsg("error.validator.jarfile.invalid", fname));
}
}
Path path = tmpfile.toPath();
@@ -576,7 +592,7 @@ boolean parseArgs(String args[]) {
usageError(getMsg("main.usage.summary"));
return false;
}
if (!cflag && !tflag && !xflag && !uflag && !iflag && !dflag) {
if (!cflag && !tflag && !xflag && !uflag && !iflag && !dflag && !validate) {
usageError(getMsg("error.bad.option"));
return false;
}
@@ -36,7 +36,7 @@ error.missing.arg=\
error.bad.file.arg=\
Error parsing file arguments
error.bad.option=\
One of options -{ctxuid} must be specified.
One of options -{ctxuid} or --validate must be specified.
error.bad.cflag=\
'c' flag requires manifest or input files to be specified!
error.bad.uflag=\
@@ -235,6 +235,11 @@ main.help.opt.main.extract=\
\ -x, --extract Extract named (or all) files from the archive
main.help.opt.main.describe-module=\
\ -d, --describe-module Print the module descriptor, or automatic module name
main.help.opt.main.validate=\
\ --validate Validate the contents of the jar archive. This option\n\
\ will validate that the API exported by a multi-release\n\
\ jar archive is consistent across all different release\n\
\ versions.
main.help.opt.any=\
\ Operation modifiers valid in any mode:\n\
\n\
@@ -44,12 +44,17 @@
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

public class ApiValidatorTest extends MRTestBase {

@@ -85,13 +90,19 @@ public void changeMethodSignature(String sigBase, String sigV10,
"-C", classes.resolve("base").toString(), ".",
"--release", "10", "-C", classes.resolve("v10").toString(),
".");
if (isAcceptable) {
result.shouldHaveExitValue(SUCCESS)
.shouldBeEmptyIgnoreVMWarnings();
} else {
result.shouldNotHaveExitValue(SUCCESS)
.shouldContain("contains a class with different api from earlier version");
}

String failureMessage = "contains a class with different api from earlier version";
checkResult(result, isAcceptable, failureMessage);
if (isAcceptable) result.shouldBeEmptyIgnoreVMWarnings();


Path malformed = root.resolve("zip").resolve("test.jar");
zip(malformed,
Map.entry("", classes.resolve("base")),
Map.entry("META-INF/versions/10", classes.resolve("v10")));

result = validateJar(malformed.toString(), isAcceptable, failureMessage);
if (isAcceptable) result.shouldBeEmptyIgnoreVMWarnings();
}

@DataProvider
@@ -127,11 +138,20 @@ public void introducingPublicMembers(String publicAPI) throws Throwable {
compileTemplate(classes.resolve("base"), base);
compileTemplate(classes.resolve("v10"), v10);

String failureMessage = "contains a class with different api from earlier version";

String jarfile = root.resolve("test.jar").toString();
jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".",
"--release", "10", "-C", classes.resolve("v10").toString(), ".")
.shouldNotHaveExitValue(SUCCESS)
.shouldContain("contains a class with different api from earlier version");
.shouldContain(failureMessage);

Path malformed = root.resolve("zip").resolve("test.jar");
zip(malformed,
Map.entry("", classes.resolve("base")),
Map.entry("META-INF/versions/10", classes.resolve("v10")));

validateJar(malformed.toString(), false, failureMessage);
}

@DataProvider
@@ -163,14 +183,17 @@ public void introducingPrivateMembers(String privateAPI) throws Throwable {
jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".",
"--release", "10", "-C", classes.resolve("v10").toString(), ".")
.shouldHaveExitValue(SUCCESS);
validateJar(jarfile);
// add release
jar("uf", jarfile,
"--release", "11", "-C", classes.resolve("v10").toString(), ".")
.shouldHaveExitValue(SUCCESS);
validateJar(jarfile);
// replace release
jar("uf", jarfile,
"--release", "11", "-C", classes.resolve("v10").toString(), ".")
.shouldHaveExitValue(SUCCESS);
validateJar(jarfile);
}

@DataProvider
@@ -201,20 +224,31 @@ public void moduleNameHasChanged() throws Throwable {
compileModule(classes.resolve("base"), "module A { }");
compileModule(classes.resolve("v10"), "module B { }");

String failureMessage = "incorrect name";

String jarfile = root.resolve("test.jar").toString();
jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".",
"--release", "10", "-C", classes.resolve("v10").toString(), ".")
.shouldNotHaveExitValue(SUCCESS)
.shouldContain("incorrect name");
.shouldContain(failureMessage);

Path malformed = root.resolve("zip").resolve("test.jar");
zip(malformed,
Map.entry("", classes.resolve("base")),
Map.entry("META-INF/versions/10", classes.resolve("v10")));

validateJar(malformed.toString(), false, failureMessage);

// update module-info release
jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".",
"--release", "10", "-C", classes.resolve("base").toString(), ".")
.shouldHaveExitValue(SUCCESS);
validateJar(jarfile);

jar("uf", jarfile,
"--release", "10", "-C", classes.resolve("v10").toString(), ".")
.shouldNotHaveExitValue(SUCCESS)
.shouldContain("incorrect name");
.shouldContain(failureMessage);
}

// @Test @ignore 8173370
@@ -233,6 +267,7 @@ public void moduleBecomeOpen() throws Throwable {
jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".",
"--release", "10", "-C", classes.resolve("base").toString(), ".")
.shouldHaveExitValue(SUCCESS);
validateJar(jarfile);
jar("uf", jarfile,
"--release", "10", "-C", classes.resolve("v10").toString(), ".")
.shouldNotHaveExitValue(SUCCESS)
@@ -378,12 +413,14 @@ private void moduleDirectivesCase(String baseDirectives,
OutputAnalyzer output = jar("cf", jarfile,
"-C", classes.resolve("base").toString(), ".",
"--release", "10", "-C", classes.resolve("v10").toString(), ".");
if (expectSuccess) {
output.shouldHaveExitValue(SUCCESS);
} else {
output.shouldNotHaveExitValue(SUCCESS)
.shouldContain(expectedMessage);
}
checkResult(output, expectSuccess, expectedMessage);

Path malformed = root.resolve("zip").resolve("test.jar");
zip(malformed,
Map.entry("", classes.resolve("base")),
Map.entry("META-INF/versions/10", classes.resolve("v10")));

validateJar(malformed.toString(), expectSuccess, expectedMessage);
}

private void compileModule(Path classes, String moduleSource,
@@ -417,4 +454,52 @@ private void compileModule(Path classes, String moduleSource,

javac(classes, sourceFiles);
}

@SafeVarargs
private void zip(Path file, Map.Entry<String, Path>... copies) throws IOException {
Files.createDirectories(file.getParent());
Files.deleteIfExists(file);
try (FileSystem zipfs = FileSystems.newFileSystem(file, Map.of("create", "true"))) {
for (var entry : copies) {
Path dstDir = zipfs.getPath(entry.getKey());
Path srcDir = entry.getValue();

Files.createDirectories(dstDir);

try (Stream<Path> stream = Files.walk(srcDir)) {
stream.filter(Files::isRegularFile).forEach(srcFile -> {
try {
Path relativePath = srcDir.relativize(srcFile);
Path dst = dstDir.resolve(relativePath.toString());
Path dstParent = dst.getParent();
if (dstParent != null)
Files.createDirectories(dstParent);
Files.copy(srcFile, dst);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
}
}

private static OutputAnalyzer checkResult(OutputAnalyzer result, boolean isAcceptable, String failureMessage) {
if (isAcceptable) {
result.shouldHaveExitValue(SUCCESS);
} else {
result.shouldNotHaveExitValue(SUCCESS)
.shouldContain(failureMessage);
}

return result;
}

private OutputAnalyzer validateJar(String jarFile) throws Throwable {
return validateJar(jarFile, true, "");
}

private OutputAnalyzer validateJar(String jarFile, boolean shouldSucceed, String failureMessage) throws Throwable {
return checkResult(jar("--validate", "--file", jarFile), shouldSucceed, failureMessage);
}
}

1 comment on commit 79010f2

@openjdk-notifier
Copy link

@openjdk-notifier openjdk-notifier bot commented on 79010f2 Jun 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.