From cc3a366e2a616226b776f683dbfb7cddaf2270d3 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 9 May 2025 00:06:26 +0000 Subject: [PATCH] 8333568: Test that jpackage doesn't modify R/O files/directories Reviewed-by: almatvee --- .../jdk/jpackage/test/JPackageCommand.java | 152 +++++++++++++++++- .../helpers/jdk/jpackage/test/TKit.java | 42 +++++ 2 files changed, 189 insertions(+), 5 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java index c1f1bc05d51..476b6a3ef74 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java @@ -23,10 +23,18 @@ package jdk.jpackage.test; +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.mapping; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toMap; +import static java.util.stream.Collectors.toSet; +import static jdk.jpackage.test.AdditionalLauncher.forEachAdditionalLauncher; + import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.security.SecureRandom; import java.util.ArrayList; @@ -46,7 +54,6 @@ import java.util.function.Supplier; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; -import static jdk.jpackage.test.AdditionalLauncher.forEachAdditionalLauncher; import jdk.jpackage.internal.util.function.ThrowingConsumer; import jdk.jpackage.internal.util.function.ThrowingFunction; import jdk.jpackage.internal.util.function.ThrowingRunnable; @@ -78,6 +85,7 @@ public class JPackageCommand extends CommandArguments { prerequisiteActions = new Actions(cmd.prerequisiteActions); verifyActions = new Actions(cmd.verifyActions); appLayoutAsserts = cmd.appLayoutAsserts; + readOnlyPathAsserts = cmd.readOnlyPathAsserts; outputValidators = cmd.outputValidators; executeInDirectory = cmd.executeInDirectory; winMsiLogFile = cmd.winMsiLogFile; @@ -843,10 +851,13 @@ public class JPackageCommand extends CommandArguments { } } - Executor.Result result = new JPackageCommand(this) - .adjustArgumentsBeforeExecution() - .createExecutor() - .execute(expectedExitCode); + final var copy = new JPackageCommand(this).adjustArgumentsBeforeExecution(); + + final var directoriesAssert = new ReadOnlyPathsAssert(copy); + + Executor.Result result = copy.createExecutor().execute(expectedExitCode); + + directoriesAssert.updateAndAssert(); for (final var outputValidator: outputValidators) { outputValidator.accept(result.getOutput().iterator()); @@ -903,6 +914,136 @@ public class JPackageCommand extends CommandArguments { return macro.value(this); } + private static final class ReadOnlyPathsAssert { + ReadOnlyPathsAssert(JPackageCommand cmd) { + this.asserts = cmd.readOnlyPathAsserts.stream().map(a -> { + return a.getPaths(cmd).stream().map(dir -> { + return Map.entry(a, dir); + }); + }).flatMap(x -> x).collect(groupingBy(Map.Entry::getKey, mapping(Map.Entry::getValue, toList()))); + snapshots = createSnapshots(); + } + + void updateAndAssert() { + final var newSnapshots = createSnapshots(); + for (final var a : asserts.keySet().stream().sorted().toList()) { + final var snapshopGroup = snapshots.get(a); + final var newSnapshopGroup = newSnapshots.get(a); + for (int i = 0; i < snapshopGroup.size(); i++) { + TKit.PathSnapshot.assertEquals(snapshopGroup.get(i), newSnapshopGroup.get(i), + String.format("Check jpackage didn't modify ${%s}=[%s]", a, asserts.get(a).get(i))); + } + } + } + + private Map> createSnapshots() { + return asserts.entrySet().stream() + .map(e -> { + return Map.entry(e.getKey(), e.getValue().stream().map(TKit.PathSnapshot::new).toList()); + }).collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private final Map> asserts; + private final Map> snapshots; + } + + public static enum ReadOnlyPathAssert{ + APP_IMAGE(new Builder("--app-image").enable(cmd -> { + // External app image should be R/O unless it is an app image signing on macOS. + return !(TKit.isOSX() && MacHelper.signPredefinedAppImage(cmd)); + }).create()), + APP_CONTENT(new Builder("--app-content").multiple().create()), + RESOURCE_DIR(new Builder("--resource-dir").create()), + MAC_DMG_CONTENT(new Builder("--mac-dmg-content").multiple().create()), + RUNTIME_IMAGE(new Builder("--runtime-image").create()); + + ReadOnlyPathAssert(Function> getPaths) { + this.getPaths = getPaths; + } + + List getPaths(JPackageCommand cmd) { + return getPaths.apply(cmd).stream().toList(); + } + + private final static class Builder { + + Builder(String argName) { + this.argName = Objects.requireNonNull(argName); + } + + Builder multiple() { + multiple = true; + return this; + } + + Builder enable(Predicate v) { + enable = v; + return this; + } + + Function> create() { + return cmd -> { + if (enable != null && !enable.test(cmd)) { + return List.of(); + } else { + final List> dirs; + if (multiple) { + dirs = Stream.of(cmd.getAllArgumentValues(argName)) + .map(Builder::tokenizeValue) + .flatMap(x -> x) + .map(Builder::toExistingFile).toList(); + } else { + dirs = Optional.ofNullable(cmd.getArgumentValue(argName)) + .map(Builder::toExistingFile).map(List::of).orElseGet(List::of); + } + + final var mutablePaths = Stream.of("--temp", "--dest") + .map(cmd::getArgumentValue) + .filter(Objects::nonNull) + .map(Builder::toExistingFile) + .filter(Optional::isPresent).map(Optional::orElseThrow) + .collect(toSet()); + + return dirs.stream() + .filter(Optional::isPresent).map(Optional::orElseThrow) + .filter(Predicate.not(mutablePaths::contains)) + .toList(); + } + }; + } + + private static Optional toExistingFile(String path) { + Objects.requireNonNull(path); + try { + return Optional.of(Path.of(path)).filter(Files::exists).map(Path::toAbsolutePath); + } catch (InvalidPathException ex) { + return Optional.empty(); + } + } + + private static Stream tokenizeValue(String str) { + return Stream.of(str.split(",")); + } + + private Predicate enable; + private final String argName; + private boolean multiple; + } + + private final Function> getPaths; + } + + public JPackageCommand setReadOnlyPathAsserts(ReadOnlyPathAssert... asserts) { + readOnlyPathAsserts = Set.of(asserts); + return this; + } + + public JPackageCommand excludeReadOnlyPathAssert(ReadOnlyPathAssert... asserts) { + var asSet = Set.of(asserts); + return setReadOnlyPathAsserts(readOnlyPathAsserts.stream().filter(Predicate.not( + asSet::contains)).toArray(ReadOnlyPathAssert[]::new)); + } + public static enum AppLayoutAssert { APP_IMAGE_FILE(JPackageCommand::assertAppImageFile), PACKAGE_FILE(JPackageCommand::assertPackageFile), @@ -1320,6 +1461,7 @@ public class JPackageCommand extends CommandArguments { private final Actions verifyActions; private Path executeInDirectory; private Path winMsiLogFile; + private Set readOnlyPathAsserts = Set.of(ReadOnlyPathAssert.values()); private Set appLayoutAsserts = Set.of(AppLayoutAssert.values()); private List>> outputValidators = new ArrayList<>(); private static boolean defaultWithToolProvider; diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java index e2f35f4df35..cfed969f4b0 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java @@ -34,6 +34,7 @@ import java.io.UncheckedIOException; import java.lang.reflect.InvocationTargetException; import java.nio.file.FileSystems; import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.StandardWatchEventKinds; @@ -1130,6 +1131,47 @@ public final class TKit { new FileOutputStream(LOG_FILE.toFile(), true))).get(); } + public record PathSnapshot(List contentHashes) { + public PathSnapshot { + contentHashes.forEach(Objects::requireNonNull); + } + + public PathSnapshot(Path path) { + this(hashRecursive(path)); + } + + public static void assertEquals(PathSnapshot a, PathSnapshot b, String msg) { + assertStringListEquals(a.contentHashes(), b.contentHashes(), msg); + } + + private static List hashRecursive(Path path) { + try { + try (final var walk = Files.walk(path)) { + return walk.sorted().map(p -> { + final String hash; + if (Files.isDirectory(p, LinkOption.NOFOLLOW_LINKS)) { + hash = ""; + } else { + hash = hashFile(p); + } + return String.format("%s#%s", path.relativize(p), hash); + }).toList(); + } + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private static String hashFile(Path path) { + try { + final var time = Files.getLastModifiedTime(path, LinkOption.NOFOLLOW_LINKS); + return time.toString(); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + } + private static TestInstance currentTest; private static PrintStream extraLogStream;