From bdb7d25ac11ca60a357b371c75544b346e523940 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 26 Sep 2025 13:59:39 +0000 Subject: [PATCH] 8358723: jpackage signing issues: the main launcher doesn't have entitlements Reviewed-by: almatvee --- .../jdk/jpackage/internal/AppImageSigner.java | 2 +- .../jpackage/internal/util/PListReader.java | 273 ++++++++++++++++- test/jdk/tools/jpackage/TEST.properties | 1 + .../jdk/jpackage/test/JPackageCommand.java | 4 +- .../test/LauncherAsServiceVerifier.java | 2 +- .../jdk/jpackage/test/LauncherVerifier.java | 44 ++- .../helpers/jdk/jpackage/test/MacHelper.java | 3 +- .../jdk/jpackage/test/MacSignVerify.java | 11 + .../internal/util/PListReaderTest.java | 290 ++++++++++++++++-- .../macosx/MacFileAssociationsTest.java | 35 ++- 10 files changed, 602 insertions(+), 63 deletions(-) diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageSigner.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageSigner.java index 7563dfa0ed3..571f163f682 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageSigner.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageSigner.java @@ -237,7 +237,7 @@ final class AppImageSigner { final var codesignExecutableFile = Codesign.build(signingCfg::toCodesignArgs).quiet(true).create().asConsumer(); final var codesignFile = Codesign.build(signingCfgWithoutEntitlements::toCodesignArgs).quiet(true).create().asConsumer(); - final var codesignDir = Codesign.build(signingCfgWithoutEntitlements::toCodesignArgs).force(true).create().asConsumer(); + final var codesignDir = Codesign.build(signingCfg::toCodesignArgs).force(true).create().asConsumer(); return new Codesigners(codesignFile, codesignExecutableFile, codesignDir); } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/PListReader.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/PListReader.java index 211a55897d0..6d7532bbd74 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/PListReader.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/PListReader.java @@ -24,21 +24,141 @@ */ package jdk.jpackage.internal.util; +import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier; + import java.io.ByteArrayInputStream; import java.io.IOException; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; +import java.util.stream.Stream; import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathFactory; -import jdk.jpackage.internal.util.function.ThrowingSupplier; +import org.w3c.dom.Element; import org.w3c.dom.Node; import org.xml.sax.SAXException; +/** + * Property list (plist) file reader. + */ public final class PListReader { + public record Raw(String value, Type type) { + + public enum Type { + STRING, + BOOLEAN, + REAL, + INTEGER, + DATE, + DATA; + + private static Optional fromElementName(String name) { + switch (name) { + case "string" -> { + return Optional.of(STRING); + } + case "true" -> { + return Optional.of(BOOLEAN); + } + case "false" -> { + return Optional.of(BOOLEAN); + } + case "real" -> { + return Optional.of(REAL); + } + case "integer" -> { + return Optional.of(INTEGER); + } + case "date" -> { + return Optional.of(DATE); + } + case "data" -> { + return Optional.of(DATA); + } + default -> { + return Optional.empty(); + } + } + } + } + + public Raw { + Objects.requireNonNull(value); + Objects.requireNonNull(type); + } + + private static Optional tryCreate(Element e) { + return Type.fromElementName(e.getNodeName()).map(type -> { + if (type == Type.BOOLEAN) { + if ("true".equals(e.getNodeName())) { + return new Raw(Boolean.TRUE.toString(), type); + } else { + return new Raw(Boolean.FALSE.toString(), type); + } + } else { + return new Raw(e.getTextContent(), type); + } + }); + } + } + + /** + * Returns the contents of the the underlying "dict" element as a Map. + *

+ * The keys in the returned map are names of the properties. + *

+ * Values of nested "dict" properties are stored as {@code Map} + * or {@code PListReader} objects depending on the value of the + * {@code fetchDictionaries} parameter. + *

+ * Values of "array" properties are stored as {@code List} objects. + *

+ * Values of other properties are stored as {@code Raw} objects. + * + * @param fetchDictionaries controls the type of objects of nested "dict" + * elements. If the value is {@code true}, + * {@code Map} type is used, and + * {@code PListReader} type otherwise. + * @return the contents of the the underlying "dict" element as a Map + */ + public Map toMap(boolean fetchDictionaries) { + Map reply = new HashMap<>(); + var nodes = root.getChildNodes(); + for (int i = 0; i != nodes.getLength(); i++) { + if (nodes.item(i) instanceof Element e) { + tryCreateValue(e, fetchDictionaries).ifPresent(value -> { + final var query = "preceding-sibling::*[1]"; + Optional.ofNullable(toSupplier(() -> { + return (Node) XPathSingleton.INSTANCE.evaluate(query, e, XPathConstants.NODE); + }).get()).ifPresent(n -> { + if ("key".equals(n.getNodeName())) { + var keyName = n.getTextContent(); + reply.putIfAbsent(keyName, value); + } + }); + }); + } + } + + return reply; + } + + /** + * Returns the value of the given string property in the underlying "dict" + * element. + * + * @param keyName the name of a string property whose value to query + * @return the value of the string property with the specified name in the + * underlying "dict" element + * @throws NoSuchElementException if there is no string property with the given + * name in the underlying "dict" element + */ public String queryValue(String keyName) { final var node = getNode(keyName); switch (node.getNodeName()) { @@ -51,6 +171,38 @@ public final class PListReader { } } + /** + * Returns the value of the given "dict" property in the underlying "dict" + * element. + * + * @param keyName the name of a "dict" property whose value to query + * @return the value of the "dict" property with the specified name in the + * underlying "dict" element + * @throws NoSuchElementException if there is no "dict" property with the given + * name in the underlying "dict" element + */ + public PListReader queryDictValue(String keyName) { + final var node = getNode(keyName); + switch (node.getNodeName()) { + case "dict" -> { + return new PListReader(node); + } + default -> { + throw new NoSuchElementException(); + } + } + } + + /** + * Returns the value of the given boolean property in the underlying "dict" + * element. + * + * @param keyName the name of a boolean property whose value to query + * @return the value of the boolean property with the specified name in the + * underlying "dict" element + * @throws NoSuchElementException if there is no string property with the given + * name in the underlying "dict" element + */ public boolean queryBoolValue(String keyName) { final var node = getNode(keyName); switch (node.getNodeName()) { @@ -66,13 +218,58 @@ public final class PListReader { } } - public List queryArrayValue(String keyName) { + /** + * Returns the value of the given array property in the underlying "dict" + * element as a list of strings. + *

+ * Processes the result of calling {@link #queryArrayValue(String)} on the + * specified property name by filtering {@link Raw} instances of type + * {@link Raw.Type#STRING}. + * + * @param keyName the name of an array property whose value to query + * @return the value of the array property with the specified name in the + * underlying "dict" element + * @throws NoSuchElementException if there is no array property with the given + * name in the underlying "dict" element + */ + public List queryStringArrayValue(String keyName) { + return queryArrayValue(keyName, false).map(v -> { + if (v instanceof Raw r) { + if (r.type() == Raw.Type.STRING) { + return r.value(); + } + } + return (String)null; + }).filter(Objects::nonNull).toList(); + } + + /** + * Returns the value of the given array property in the underlying "dict" + * element as a stream of {@link Object}-s. + *

+ * Values of "dict" array items are stored as {@code Map} or + * {@code PListReader} objects depending on the value of the + * {@code fetchDictionaries} parameter. + *

+ * Values of "array" array items are stored as {@code List} objects. + *

+ * Values of other types are stored as {@code Raw} objects. + * + * @param keyName the name of an array property whose value to query + * @param fetchDictionaries controls the type of objects of "dict" elements. If + * the value is {@code true}, + * {@code Map} type is used, and + * {@code PListReader} type otherwise. + * @return the value of the array property with the specified name in the + * underlying "dict" element + * @throws NoSuchElementException if there is no array key with the given name + * in the underlying "dict" element + */ + public Stream queryArrayValue(String keyName, boolean fetchDictionaries) { final var node = getNode(keyName); switch (node.getNodeName()) { case "array" -> { - return XmlUtils.toStream(node.getChildNodes()).filter(n -> { - return n.getNodeName().equals("string"); - }).map(Node::getTextContent).toList(); + return readArray(node, fetchDictionaries); } default -> { throw new NoSuchElementException(); @@ -80,21 +277,75 @@ public final class PListReader { } } - public PListReader(Node doc) { - this.root = Objects.requireNonNull(doc); + /** + * Creates plist reader from the given node. + *

+ * If the specified node is an element with the name "dict", the reader is bound + * to the specified node; otherwise, it is bound to the {@code /plist/dict} + * element in the document. + * + * @param node the node + * @throws NoSuchElementException if the specified node is not an element with + * name "dict" and there is no + * {@code /plist/dict} node in the document + */ + public PListReader(Node node) { + Objects.requireNonNull(node); + if (node.getNodeName().equals("dict")) { + this.root = node; + } else { + this.root = Optional.ofNullable(toSupplier(() -> { + return (Node) XPathSingleton.INSTANCE.evaluate("/plist[1]/dict[1]", node, XPathConstants.NODE); + }).get()).orElseThrow(NoSuchElementException::new); + } } public PListReader(byte[] xmlData) throws ParserConfigurationException, SAXException, IOException { this(XmlUtils.initDocumentBuilder().parse(new ByteArrayInputStream(xmlData))); } + private Optional tryCreateValue(Element e, boolean fetchDictionaries) { + switch (e.getNodeName()) { + case "dict" -> { + var plistReader = new PListReader(e); + if (fetchDictionaries) { + return Optional.of(plistReader.toMap(fetchDictionaries)); + } else { + return Optional.of(plistReader); + } + } + case "array" -> { + return Optional.of(readArray(e, fetchDictionaries).toList()); + } + default -> { + return Raw.tryCreate(e); + } + } + } + + private Stream readArray(Node node, boolean fetchDictionaries) { + return XmlUtils.toStream(node.getChildNodes()).map(n -> { + if (n instanceof Element e) { + return tryCreateValue(e, fetchDictionaries); + } else { + return Optional.empty(); + } + }).filter(Optional::isPresent).map(Optional::get); + } + private Node getNode(String keyName) { - final var xPath = XPathFactory.newInstance().newXPath(); - final var query = String.format("//*[preceding-sibling::key = \"%s\"][1]", keyName); - return Optional.ofNullable(ThrowingSupplier.toSupplier(() -> { - return (Node) xPath.evaluate(query, root, XPathConstants.NODE); + Objects.requireNonNull(keyName); + final var query = String.format("*[preceding-sibling::key = \"%s\"][1]", keyName); + return Optional.ofNullable(toSupplier(() -> { + return (Node) XPathSingleton.INSTANCE.evaluate(query, root, XPathConstants.NODE); }).get()).orElseThrow(NoSuchElementException::new); } + + private static final class XPathSingleton { + private static final XPath INSTANCE = XPathFactory.newInstance().newXPath(); + } + + private final Node root; } diff --git a/test/jdk/tools/jpackage/TEST.properties b/test/jdk/tools/jpackage/TEST.properties index 58a82a9444b..7c799e4d320 100644 --- a/test/jdk/tools/jpackage/TEST.properties +++ b/test/jdk/tools/jpackage/TEST.properties @@ -22,5 +22,6 @@ modules = \ jdk.jpackage/jdk.jpackage.internal:+open \ jdk.jpackage/jdk.jpackage.internal.util \ jdk.jpackage/jdk.jpackage.internal.util.function \ + jdk.jpackage/jdk.jpackage.internal.resources \ java.base/jdk.internal.util \ jdk.jlink/jdk.tools.jlink.internal 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 49f565e27e9..6aacc261fb6 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java @@ -1085,7 +1085,9 @@ public class JPackageCommand extends CommandArguments { }), MAIN_LAUNCHER_FILES(cmd -> { if (!cmd.isRuntime()) { - new LauncherVerifier(cmd).verify(cmd, LauncherVerifier.Action.VERIFY_INSTALLED); + new LauncherVerifier(cmd).verify(cmd, + LauncherVerifier.Action.VERIFY_INSTALLED, + LauncherVerifier.Action.VERIFY_MAC_ENTITLEMENTS); } }), MAIN_JAR_FILE(cmd -> { diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherAsServiceVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherAsServiceVerifier.java index 42821822894..e1cd37fe8b4 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherAsServiceVerifier.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherAsServiceVerifier.java @@ -311,7 +311,7 @@ public final class LauncherAsServiceVerifier { var servicePlist = MacHelper.readPList(servicePlistFile); - var args = servicePlist.queryArrayValue("ProgramArguments"); + var args = servicePlist.queryStringArrayValue("ProgramArguments"); TKit.assertEquals(1, args.size(), "Check number of array elements in 'ProgramArguments' property in the property file"); TKit.assertEquals(installedLauncherPath.toString(), args.get(0), diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java index b3ed030c69d..7bf01fdde05 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java @@ -37,9 +37,14 @@ import java.util.function.BiConsumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; +import javax.xml.parsers.ParserConfigurationException; +import jdk.jpackage.internal.resources.ResourceLocator; +import jdk.jpackage.internal.util.PListReader; import jdk.jpackage.internal.util.function.ThrowingBiConsumer; +import jdk.jpackage.internal.util.function.ThrowingSupplier; import jdk.jpackage.test.AdditionalLauncher.PropertyFile; import jdk.jpackage.test.LauncherShortcut.StartupDirectory; +import org.xml.sax.SAXException; public final class LauncherVerifier { @@ -82,6 +87,11 @@ public final class LauncherVerifier { verifier.verifyInAppImageFile(cmd); } }), + VERIFY_MAC_ENTITLEMENTS((verifier, cmd) -> { + if (TKit.isOSX() && MacHelper.appImageSigned(cmd)) { + verifier.verifyMacEntitlements(cmd); + } + }), EXECUTE_LAUNCHER(LauncherVerifier::executeLauncher), ; @@ -96,7 +106,7 @@ public final class LauncherVerifier { private final BiConsumer action; static final List VERIFY_APP_IMAGE = List.of( - VERIFY_ICON, VERIFY_DESCRIPTION, VERIFY_INSTALLED, VERIFY_APP_IMAGE_FILE + VERIFY_ICON, VERIFY_DESCRIPTION, VERIFY_INSTALLED, VERIFY_APP_IMAGE_FILE, VERIFY_MAC_ENTITLEMENTS ); static final List VERIFY_DEFAULTS = Stream.concat( @@ -323,6 +333,24 @@ public final class LauncherVerifier { } } + private void verifyMacEntitlements(JPackageCommand cmd) throws ParserConfigurationException, SAXException, IOException { + Path launcherPath = cmd.appLauncherPath(name); + var entitlements = MacSignVerify.findEntitlements(launcherPath); + + TKit.assertTrue(entitlements.isPresent(), String.format("Check [%s] launcher is signed with entitlements", name)); + + Map expected; + if (cmd.hasArgument("--mac-entitlements")) { + expected = new PListReader(Files.readAllBytes(Path.of(cmd.getArgumentValue("--mac-entitlements")))).toMap(true); + } else if (cmd.hasArgument("--mac-app-store")) { + expected = DefaultEntitlements.APP_STORE; + } else { + expected = DefaultEntitlements.STANDARD; + } + + TKit.assertEquals(expected, entitlements.orElseThrow().toMap(true), String.format("Check [%s] launcher is signed with expected entitlements", name)); + } + private void executeLauncher(JPackageCommand cmd) throws IOException { Path launcherPath = cmd.appLauncherPath(name); @@ -362,6 +390,20 @@ public final class LauncherVerifier { }); } + + private static final class DefaultEntitlements { + private static Map loadFromResources(String resourceName) { + return ThrowingSupplier.toSupplier(() -> { + var bytes = ResourceLocator.class.getResourceAsStream("entitlements.plist").readAllBytes(); + return new PListReader(bytes).toMap(true); + }).get(); + } + + static final Map STANDARD = loadFromResources("entitlements.plist"); + static final Map APP_STORE = loadFromResources("sandbox.plist"); + } + + private final String name; private final Optional> javaOptions; private final Optional> arguments; diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java index f4feb3e2fde..88e2819ce15 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java @@ -74,7 +74,8 @@ public final class MacHelper { Path mountPoint = null; try { - var plist = readPList(attachExecutor.getOutput()); + // The first "dict" item of "system-entities" array property contains "mount-point" string property. + var plist = readPList(attachExecutor.getOutput()).queryArrayValue("system-entities", false).findFirst().map(PListReader.class::cast).orElseThrow(); mountPoint = Path.of(plist.queryValue("mount-point")); // code here used to copy just or .app diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacSignVerify.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacSignVerify.java index 432433b4fd0..ae27e292bf6 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacSignVerify.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacSignVerify.java @@ -56,6 +56,17 @@ public final class MacSignVerify { String.format("Check [%s] signed with adhoc signature", path)); } + public static Optional findEntitlements(Path path) { + final var exec = Executor.of("/usr/bin/codesign", "-d", "--entitlements", "-", "--xml", path.toString()).saveOutput().dumpOutput(); + final var result = exec.execute(); + var xml = result.stdout().getOutput(); + if (xml.isEmpty()) { + return Optional.empty(); + } else { + return Optional.of(MacHelper.readPList(xml)); + } + } + public static void assertUnsigned(Path path) { TKit.assertTrue(findSpctlSignOrigin(SpctlType.EXEC, path).isEmpty(), String.format("Check [%s] unsigned", path)); diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/PListReaderTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/PListReaderTest.java index d766505cf14..fe7d0c63870 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/PListReaderTest.java +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/PListReaderTest.java @@ -32,14 +32,18 @@ import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; import java.util.function.BiFunction; +import java.util.stream.Stream; import javax.xml.parsers.ParserConfigurationException; +import jdk.jpackage.internal.util.PListReader.Raw; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.EnumSource.Mode; import org.junit.jupiter.params.provider.MethodSource; import org.w3c.dom.Node; import org.xml.sax.InputSource; @@ -51,7 +55,23 @@ public class PListReaderTest { enum QueryType { STRING(PListReader::queryValue), BOOLEAN(PListReader::queryBoolValue), - STRING_ARRAY(PListReader::queryArrayValue); + DICT((plistReader, keyName) -> { + return plistReader.queryDictValue(keyName).toMap(true); + }), + STRING_ARRAY(PListReader::queryStringArrayValue), + RAW_ARRAY((plistReader, keyName) -> { + return plistReader.queryArrayValue(keyName, false).toList(); + }), + RAW_ARRAY_RECURSIVE((plistReader, keyName) -> { + return plistReader.queryArrayValue(keyName, true).toList(); + }), + TO_MAP((plistReader, _) -> { + return plistReader.toMap(false); + }), + TO_MAP_RECURSIVE((plistReader, _) -> { + return plistReader.toMap(true); + }), + ; QueryType(BiFunction queryMethod) { this.queryMethod = Objects.requireNonNull(queryMethod); @@ -65,10 +85,10 @@ public class PListReaderTest { private final BiFunction queryMethod; } - public record QueryValueTestSpec(QueryType queryType, String keyName, Optional expectedValue, + public record TestSpec(QueryType queryType, Optional keyName, Optional expectedValue, Optional> expectedException, String... xml) { - public QueryValueTestSpec { + public TestSpec { Objects.requireNonNull(queryType); Objects.requireNonNull(keyName); Objects.requireNonNull(expectedValue); @@ -77,6 +97,14 @@ public class PListReaderTest { if (expectedValue.isEmpty() == expectedException.isEmpty()) { throw new IllegalArgumentException(); } + if (keyName.isEmpty()) { + switch (queryType) { + case TO_MAP, TO_MAP_RECURSIVE -> {} + default -> { + throw new IllegalArgumentException(); + } + } + } } static final class Builder { @@ -91,7 +119,7 @@ public class PListReaderTest { return this; } - Builder expectedValue(Object v) { + Builder expect(Object v) { expectedValue = v; if (v instanceof String) { queryType(QueryType.STRING); @@ -99,11 +127,13 @@ public class PListReaderTest { queryType(QueryType.BOOLEAN); } else if (v instanceof List) { queryType(QueryType.STRING_ARRAY); + } else if (v instanceof Map) { + queryType(QueryType.DICT); } return this; } - Builder expectedException(Class v) { + Builder expectException(Class v) { expectedException = v; return this; } @@ -113,8 +143,11 @@ public class PListReaderTest { return this; } - QueryValueTestSpec create() { - return new QueryValueTestSpec(queryType, keyName, Optional.ofNullable(expectedValue), + TestSpec create() { + return new TestSpec( + queryType, + Optional.ofNullable(keyName), + Optional.ofNullable(expectedValue), validatedExpectedException(), xml); } @@ -137,12 +170,12 @@ public class PListReaderTest { final var plistReader = new PListReader(createXml(xml)); expectedValue.ifPresent(v -> { - final var actualValue = queryType.queryValue(plistReader, keyName); + final var actualValue = queryType.queryValue(plistReader, keyName.orElse(null)); assertEquals(v, actualValue); }); expectedException.ifPresent(v -> { - assertThrows(v, () -> queryType.queryValue(plistReader, keyName)); + assertThrows(v, () -> queryType.queryValue(plistReader, keyName.orElse(null))); }); } @@ -150,7 +183,9 @@ public class PListReaderTest { public String toString() { final var sb = new StringBuilder(); sb.append(queryType); - sb.append("; key=").append(keyName); + if (keyName != null) { + sb.append("; key=").append(keyName); + } expectedValue.ifPresent(v -> { sb.append("; expected="); sb.append(v); @@ -166,13 +201,13 @@ public class PListReaderTest { } @ParameterizedTest - @EnumSource(QueryType.class) + @EnumSource(mode = Mode.MATCH_NONE, names = {"TO_MAP.*"}) public void testNoSuchElement(QueryType queryType) { testSpec(queryType).create().test(); } @ParameterizedTest - @EnumSource(QueryType.class) + @EnumSource(mode = Mode.MATCH_NONE, names = {"TO_MAP.*"}) public void testWrongValueType(QueryType queryType) { final var builder = testSpec(queryType).xml( "string-key", @@ -182,33 +217,56 @@ public class PListReaderTest { "boolean-false-key", "", "array-key", - "b"); + "b", + "dict-key", + "nested-dict-key345"); - List testSpecs = new ArrayList<>(); + List testSpecs = new ArrayList<>(); switch (queryType) { case STRING -> { testSpecs.add(builder.keyName("boolean-true-key").create()); testSpecs.add(builder.keyName("boolean-false-key").create()); testSpecs.add(builder.keyName("array-key").create()); + testSpecs.add(builder.keyName("dict-key").create()); } case BOOLEAN -> { testSpecs.add(builder.keyName("string-key").create()); testSpecs.add(builder.keyName("array-key").create()); + testSpecs.add(builder.keyName("dict-key").create()); } - case STRING_ARRAY -> { + case STRING_ARRAY, RAW_ARRAY, RAW_ARRAY_RECURSIVE -> { testSpecs.add(builder.keyName("string-key").create()); testSpecs.add(builder.keyName("boolean-true-key").create()); testSpecs.add(builder.keyName("boolean-false-key").create()); + testSpecs.add(builder.keyName("dict-key").create()); + } + case DICT -> { + testSpecs.add(builder.keyName("string-key").create()); + testSpecs.add(builder.keyName("boolean-true-key").create()); + testSpecs.add(builder.keyName("boolean-false-key").create()); + testSpecs.add(builder.keyName("array-key").create()); + } + case TO_MAP, TO_MAP_RECURSIVE -> { + throw new UnsupportedOperationException(); } } - testSpecs.forEach(QueryValueTestSpec::test); + testSpecs.forEach(TestSpec::test); + + builder.keyName(null).expect(Map.of( + "string-key", new Raw("a", Raw.Type.STRING), + "boolean-true-key", new Raw(Boolean.TRUE.toString(), Raw.Type.BOOLEAN), + "boolean-false-key", new Raw(Boolean.FALSE.toString(), Raw.Type.BOOLEAN), + "array-key", List.of(new Raw("b", Raw.Type.STRING)), + "dict-key", Map.of("nested-dict-key", new Raw("345", Raw.Type.INTEGER)) + )).queryType(QueryType.TO_MAP_RECURSIVE).create().test(); + } @ParameterizedTest @MethodSource - public void testQueryValue(QueryValueTestSpec testSpec) { + public void test(TestSpec testSpec) { testSpec.test(); } @@ -219,28 +277,190 @@ public class PListReaderTest { assertEquals("A", actualValue); } - private static List testQueryValue() { - return List.of( - testSpec().expectedValue("A").xml("fooA").create(), - testSpec().expectedValue("").xml("foo").create(), - testSpec().xml("foo").create(), - testSpec().expectedValue(Boolean.TRUE).xml("foo").create(), - testSpec().expectedValue(Boolean.FALSE).xml("foo").create(), - testSpec(QueryType.BOOLEAN).xml("foo").create(), - testSpec(QueryType.BOOLEAN).xml("foo").create(), - testSpec().expectedValue(List.of("foo", "bar")).xml("foofoobar").create(), - testSpec().expectedValue(List.of()).xml("foo").create(), - testSpec(QueryType.STRING_ARRAY).xml("foo").create(), - testSpec().expectedValue("A").xml("fooAB").create(), - testSpec().expectedValue("A").xml("fooAfooB").create() + @Test + public void test_toMap() { + + var builder = testSpec(); + + builder.xml( + "AppName", + "Hello", + "", + "AppVersion", + "1.0", + "Release", + "", + "Debug", + "", + "ReleaseDate", + "2025-09-24T09:23:00Z", + "UserData", + "", + "", + " Foo", + " ", + " Str", + " ", + " Another Str", + " ", + " ", + " ", + " ", + "", + "Checksum", + "7841ff0076cdde93bdca02cfd332748c40620ce4", + "Plugins", + "", + " ", + " PluginName", + " Foo", + " Priority", + " 13", + " History", + " ", + " New File", + " Another New File", + " ", + " ", + " ", + " PluginName", + " Bar", + " Priority", + " 23", + " History", + " ", + " ", + "" ); + + var expected = Map.of( + "AppName", new Raw("Hello", Raw.Type.STRING), + "AppVersion", new Raw("1.0", Raw.Type.REAL), + "Release", new Raw(Boolean.TRUE.toString(), Raw.Type.BOOLEAN), + "Debug", new Raw(Boolean.FALSE.toString(), Raw.Type.BOOLEAN), + "ReleaseDate", new Raw("2025-09-24T09:23:00Z", Raw.Type.DATE), + "Checksum", new Raw("7841ff0076cdde93bdca02cfd332748c40620ce4", Raw.Type.DATA), + "UserData", Map.of( + "Foo", List.of( + new Raw("Str", Raw.Type.STRING), + List.of( + new Raw("Another Str", Raw.Type.STRING), + new Raw(Boolean.TRUE.toString(), Raw.Type.BOOLEAN), + new Raw(Boolean.FALSE.toString(), Raw.Type.BOOLEAN) + ) + ) + ), + "Plugins", List.of( + Map.of( + "PluginName", new Raw("Foo", Raw.Type.STRING), + "Priority", new Raw("13", Raw.Type.INTEGER), + "History", List.of( + new Raw("New File", Raw.Type.STRING), + new Raw("Another New File", Raw.Type.STRING) + ) + ), + Map.of( + "PluginName", new Raw("Bar", Raw.Type.STRING), + "Priority", new Raw("23", Raw.Type.REAL), + "History", List.of() + ) + ) + ); + + builder.expect(expected).queryType(QueryType.TO_MAP_RECURSIVE).create().test(); } - private static QueryValueTestSpec.Builder testSpec() { - return new QueryValueTestSpec.Builder(); + private static List test() { + + List data = new ArrayList<>(); + + Stream.of( + testSpec().expect("A").xml("fooA"), + testSpec().expect("A").xml("BfooA"), + testSpec().expect("").xml("foo some text "), + testSpec().xml("foo"), + testSpec().xml("foo"), + testSpec().xml("fooA"), + testSpec().expect(Boolean.TRUE).xml("foo"), + testSpec().expect(Boolean.FALSE).xml("foo"), + testSpec(QueryType.BOOLEAN).xml("foo"), + testSpec(QueryType.BOOLEAN).xml("foo"), + testSpec().expect(List.of("foo", "bar")).xml("foofoobar"), + testSpec().expect(List.of()).xml("foo"), + testSpec(QueryType.STRING_ARRAY).xml("foo"), + testSpec().expect("A").xml("fooAB"), + testSpec().expect("A").xml("fooAfooB"), + + testSpec().expect(Map.of()).xml("foo"), + + // + // Test that if there are multiple keys with the same name, all but the first are ignored. + // + testSpec().expect("A").xml("fooAfooBfooC"), + testSpec().expect("A").xml("fooAfooB"), + testSpec(QueryType.STRING).xml("fooBfooA"), + testSpec().expect(Boolean.TRUE).xml("foofoo"), + testSpec().expect(Boolean.TRUE).xml("foofoo"), + testSpec(QueryType.BOOLEAN).xml("foofoo"), + + // + // Test that it doesn't look up keys in nested "dict" or "array" elements. + // + testSpec().xml("foofooA"), + testSpec().expect("B").xml("barfooAfooB"), + testSpec().xml("foofooA"), + testSpec().expect("B").xml("barfooAfooB"), + + // + // Test empty arrays. + // + testSpec().expect(List.of()).queryType(QueryType.RAW_ARRAY_RECURSIVE).xml("foo"), + testSpec().expect(List.of()).queryType(QueryType.RAW_ARRAY).xml("foo") + + ).map(TestSpec.Builder::create).forEach(data::add); + + // + // Test toMap() method. + // + Stream.of( + testSpec().expect(Map.of()).xml(), + testSpec().expect(Map.of()).xml("foobar"), + testSpec().expect(Map.of()).xml("Abar"), + testSpec().expect(Map.of()).xml("A"), + testSpec().expect(Map.of()).xml("fooA"), + testSpec().expect(Map.of("foo", new Raw("A", Raw.Type.STRING))).xml("fooAB"), + testSpec().expect(Map.of("foo", new Raw("A", Raw.Type.STRING))).xml("fooA hello foo bye B"), + testSpec().expect(Map.of("foo", new Raw("A", Raw.Type.STRING), "Foo", new Raw("B", Raw.Type.STRING))).xml("fooAFooB") + ).map(builder -> { + return builder.queryType(QueryType.TO_MAP_RECURSIVE); + }).map(TestSpec.Builder::create).forEach(data::add); + + var arrayTestSpec = testSpec().expect(List.of( + new Raw("Hello", Raw.Type.STRING), + Map.of("foo", new Raw("Bye", Raw.Type.STRING)), + new Raw("integer", Raw.Type.INTEGER), + Map.of(), + new Raw(Boolean.TRUE.toString(), Raw.Type.BOOLEAN) + )).queryType(QueryType.RAW_ARRAY_RECURSIVE); + + Stream.of( + "HellofooByeinteger", + "HelloBingofooByeinteger", + "BHellofooByeByeeeinteger", + "HellobarfooByeinteger", + "HellofooByefooByeByeinteger" + ).map(xml -> { + return "foo" + xml + ""; + }).map(arrayTestSpec::xml).map(TestSpec.Builder::create).forEach(data::add); + + return data; } - private static QueryValueTestSpec.Builder testSpec(QueryType queryType) { + private static TestSpec.Builder testSpec() { + return new TestSpec.Builder(); + } + + private static TestSpec.Builder testSpec(QueryType queryType) { return testSpec().queryType(queryType); } @@ -248,7 +468,9 @@ public class PListReaderTest { final List content = new ArrayList<>(); content.add(""); content.add(""); + content.add(""); content.addAll(List.of(xml)); + content.add(""); content.add(""); return String.join("", content.toArray(String[]::new)); } diff --git a/test/jdk/tools/jpackage/macosx/MacFileAssociationsTest.java b/test/jdk/tools/jpackage/macosx/MacFileAssociationsTest.java index 81831f59da3..e6d8b41f05f 100644 --- a/test/jdk/tools/jpackage/macosx/MacFileAssociationsTest.java +++ b/test/jdk/tools/jpackage/macosx/MacFileAssociationsTest.java @@ -21,15 +21,16 @@ * questions. */ +import static java.util.Map.entry; + import java.nio.file.Path; import java.util.List; import java.util.Map; -import static java.util.Map.entry; -import jdk.jpackage.test.JPackageCommand; -import jdk.jpackage.test.TKit; -import jdk.jpackage.test.MacHelper; import jdk.jpackage.internal.util.PListReader; import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.MacHelper; +import jdk.jpackage.test.TKit; /** * Tests generation of app image with --file-associations and mac additional file @@ -80,21 +81,26 @@ public class MacFileAssociationsTest { "Check value of %s plist key", key)); } - private static void checkBoolValue(PListReader plist, String key, Boolean value) { - Boolean result = plist.queryBoolValue(key); - TKit.assertEquals(value.toString(), result.toString(), String.format( + private static void checkBoolValue(PListReader plist, String key, boolean value) { + boolean result = plist.queryBoolValue(key); + TKit.assertEquals(value, result, String.format( "Check value of %s plist key", key)); } private static void checkArrayValue(PListReader plist, String key, List values) { - List result = plist.queryArrayValue(key); + List result = plist.queryStringArrayValue(key); TKit.assertStringListEquals(values, result, String.format( "Check value of %s plist key", key)); } private static void verifyPList(Path appImage) throws Exception { - var plist = MacHelper.readPListFromAppImage(appImage); + final var rootPlist = MacHelper.readPListFromAppImage(appImage); + + TKit.traceFileContents(appImage.resolve("Contents/Info.plist"), "Info.plist"); + + var plist = rootPlist.queryArrayValue("CFBundleDocumentTypes", false).findFirst().map(PListReader.class::cast).orElseThrow(); + checkStringValue(plist, "CFBundleTypeRole", "Viewer"); checkStringValue(plist, "LSHandlerRank", "Default"); checkStringValue(plist, "NSDocumentClass", "SomeClass"); @@ -103,10 +109,13 @@ public class MacFileAssociationsTest { checkBoolValue(plist, "LSSupportsOpeningDocumentsInPlace", false); checkBoolValue(plist, "UISupportsDocumentBrowser", false); - checkArrayValue(plist, "NSExportableTypes", List.of("public.png", - "public.jpg")); + plist = rootPlist.queryArrayValue("UTExportedTypeDeclarations", false).findFirst().map(PListReader.class::cast).orElseThrow(); + + checkArrayValue(plist, "UTTypeConformsTo", List.of("public.image", "public.data")); + + plist = plist.queryDictValue("UTTypeTagSpecification"); + + checkArrayValue(plist, "NSExportableTypes", List.of("public.png", "public.jpg")); - checkArrayValue(plist, "UTTypeConformsTo", List.of("public.image", - "public.data")); } }