8358723: jpackage signing issues: the main launcher doesn't have entitlements

Reviewed-by: almatvee
This commit is contained in:
Alexey Semenyuk 2025-09-26 13:59:39 +00:00
parent f0e1078c71
commit bdb7d25ac1
10 changed files with 602 additions and 63 deletions

View File

@ -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);
}

View File

@ -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<Type> 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<Raw> 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.
* <p>
* The keys in the returned map are names of the properties.
* <p>
* Values of nested "dict" properties are stored as {@code Map<String, Object>}
* or {@code PListReader} objects depending on the value of the
* {@code fetchDictionaries} parameter.
* <p>
* Values of "array" properties are stored as {@code List<Object>} objects.
* <p>
* 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<String, Object>} type is used, and
* {@code PListReader} type otherwise.
* @return the contents of the the underlying "dict" element as a Map
*/
public Map<String, Object> toMap(boolean fetchDictionaries) {
Map<String, Object> 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<String> queryArrayValue(String keyName) {
/**
* Returns the value of the given array property in the underlying "dict"
* element as a list of strings.
* <p>
* 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<String> 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.
* <p>
* Values of "dict" array items are stored as {@code Map<String, Object>} or
* {@code PListReader} objects depending on the value of the
* {@code fetchDictionaries} parameter.
* <p>
* Values of "array" array items are stored as {@code List<Object>} objects.
* <p>
* 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<String, Object>} 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<Object> 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.
* <p>
* 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<Object> readArray(Node node, boolean fetchDictionaries) {
return XmlUtils.toStream(node.getChildNodes()).map(n -> {
if (n instanceof Element e) {
return tryCreateValue(e, fetchDictionaries);
} else {
return Optional.<Raw>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;
}

View File

@ -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

View File

@ -1085,7 +1085,9 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
}),
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 -> {

View File

@ -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),

View File

@ -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<LauncherVerifier, JPackageCommand> action;
static final List<Action> 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<Action> 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<String, Object> 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<String, Object> loadFromResources(String resourceName) {
return ThrowingSupplier.toSupplier(() -> {
var bytes = ResourceLocator.class.getResourceAsStream("entitlements.plist").readAllBytes();
return new PListReader(bytes).toMap(true);
}).get();
}
static final Map<String, Object> STANDARD = loadFromResources("entitlements.plist");
static final Map<String, Object> APP_STORE = loadFromResources("sandbox.plist");
}
private final String name;
private final Optional<List<String>> javaOptions;
private final Optional<List<String>> arguments;

View File

@ -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 <runtime name> or <app name>.app

View File

@ -56,6 +56,17 @@ public final class MacSignVerify {
String.format("Check [%s] signed with adhoc signature", path));
}
public static Optional<PListReader> 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));

View File

@ -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<PListReader, String, ?> queryMethod) {
this.queryMethod = Objects.requireNonNull(queryMethod);
@ -65,10 +85,10 @@ public class PListReaderTest {
private final BiFunction<PListReader, String, ?> queryMethod;
}
public record QueryValueTestSpec(QueryType queryType, String keyName, Optional<Object> expectedValue,
public record TestSpec(QueryType queryType, Optional<String> keyName, Optional<Object> expectedValue,
Optional<Class<? extends RuntimeException>> 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<? extends RuntimeException> v) {
Builder expectException(Class<? extends RuntimeException> 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(
"<key>string-key</key>",
@ -182,33 +217,56 @@ public class PListReaderTest {
"<key>boolean-false-key</key>",
"<false/>",
"<key>array-key</key>",
"<array><string>b</string></array>");
"<array><string>b</string></array>",
"<key>dict-key</key>",
"<dict><key>nested-dict-key</key><integer>345</integer></dict>");
List<QueryValueTestSpec> testSpecs = new ArrayList<>();
List<TestSpec> 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<QueryValueTestSpec> testQueryValue() {
return List.of(
testSpec().expectedValue("A").xml("<key>foo</key><string>A</string>").create(),
testSpec().expectedValue("").xml("<key>foo</key><string/>").create(),
testSpec().xml("<key>foo</key><String/>").create(),
testSpec().expectedValue(Boolean.TRUE).xml("<key>foo</key><true/>").create(),
testSpec().expectedValue(Boolean.FALSE).xml("<key>foo</key><false/>").create(),
testSpec(QueryType.BOOLEAN).xml("<key>foo</key><True/>").create(),
testSpec(QueryType.BOOLEAN).xml("<key>foo</key><False/>").create(),
testSpec().expectedValue(List.of("foo", "bar")).xml("<key>foo</key><array><string>foo</string><string>bar</string></array>").create(),
testSpec().expectedValue(List.of()).xml("<key>foo</key><array/>").create(),
testSpec(QueryType.STRING_ARRAY).xml("<key>foo</key><Array/>").create(),
testSpec().expectedValue("A").xml("<key>foo</key><string>A</string><string>B</string>").create(),
testSpec().expectedValue("A").xml("<key>foo</key><string>A</string><key>foo</key><string>B</string>").create()
@Test
public void test_toMap() {
var builder = testSpec();
builder.xml(
"<key>AppName</key>",
"<string>Hello</string>",
"<!-- Application version -->",
"<key>AppVersion</key>",
"<real>1.0</real>",
"<key>Release</key>",
"<true/>",
"<key>Debug</key>",
"<false/>",
"<key>ReleaseDate</key>",
"<date>2025-09-24T09:23:00Z</date>",
"<key>UserData</key>",
"<!-- User data -->",
"<dict>",
" <key>Foo</key>",
" <array>",
" <string>Str</string>",
" <array>",
" <string>Another Str</string>",
" <true/>",
" <false/>",
" </array>",
" </array>",
"</dict>",
"<key>Checksum</key>",
"<data>7841ff0076cdde93bdca02cfd332748c40620ce4</data>",
"<key>Plugins</key>",
"<array>",
" <dict>",
" <key>PluginName</key>",
" <string>Foo</string>",
" <key>Priority</key>",
" <integer>13</integer>",
" <key>History</key>",
" <array>",
" <string>New File</string>",
" <string>Another New File</string>",
" </array>",
" </dict>",
" <dict>",
" <key>PluginName</key>",
" <string>Bar</string>",
" <key>Priority</key>",
" <real>23</real>",
" <key>History</key>",
" <array/>",
" </dict>",
"</array>"
);
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<TestSpec> test() {
List<TestSpec> data = new ArrayList<>();
Stream.of(
testSpec().expect("A").xml("<key>foo</key><string>A</string>"),
testSpec().expect("A").xml("<a><string>B</string></a><key>foo</key><string>A</string>"),
testSpec().expect("").xml("<key>foo</key> some text <string/>"),
testSpec().xml("<key>foo</key><String/>"),
testSpec().xml("<key>foo</key>"),
testSpec().xml("<key>foo</key><foo/><string>A</string>"),
testSpec().expect(Boolean.TRUE).xml("<key>foo</key><true/>"),
testSpec().expect(Boolean.FALSE).xml("<key>foo</key><false/>"),
testSpec(QueryType.BOOLEAN).xml("<key>foo</key><True/>"),
testSpec(QueryType.BOOLEAN).xml("<key>foo</key><False/>"),
testSpec().expect(List.of("foo", "bar")).xml("<key>foo</key><array><string>foo</string><random/><dict/><string>bar</string><true/></array>"),
testSpec().expect(List.of()).xml("<key>foo</key><array/>"),
testSpec(QueryType.STRING_ARRAY).xml("<key>foo</key><Array/>"),
testSpec().expect("A").xml("<key>foo</key><string>A</string><string>B</string>"),
testSpec().expect("A").xml("<key>foo</key><string>A</string><key>foo</key><string>B</string>"),
testSpec().expect(Map.of()).xml("<key>foo</key><dict/>"),
//
// Test that if there are multiple keys with the same name, all but the first are ignored.
//
testSpec().expect("A").xml("<key>foo</key><string>A</string><key>foo</key><string>B</string><key>foo</key><string>C</string>"),
testSpec().expect("A").xml("<key>foo</key><string>A</string><key>foo</key><String>B</String>"),
testSpec(QueryType.STRING).xml("<key>foo</key><String>B</String><key>foo</key><string>A</string>"),
testSpec().expect(Boolean.TRUE).xml("<key>foo</key><true/><key>foo</key><false/>"),
testSpec().expect(Boolean.TRUE).xml("<key>foo</key><true/><key>foo</key><False/>"),
testSpec(QueryType.BOOLEAN).xml("<key>foo</key><False/><key>foo</key><true/>"),
//
// Test that it doesn't look up keys in nested "dict" or "array" elements.
//
testSpec().xml("<key>foo</key><dict><key>foo</key><string>A</string></dict>"),
testSpec().expect("B").xml("<key>bar</key><dict><key>foo</key><string>A</string></dict><key>foo</key><string>B</string>"),
testSpec().xml("<key>foo</key><array><dict><key>foo</key><string>A</string></dict></array>"),
testSpec().expect("B").xml("<key>bar</key><array><dict><key>foo</key><string>A</string></dict></array><key>foo</key><string>B</string>"),
//
// Test empty arrays.
//
testSpec().expect(List.of()).queryType(QueryType.RAW_ARRAY_RECURSIVE).xml("<key>foo</key><array/>"),
testSpec().expect(List.of()).queryType(QueryType.RAW_ARRAY).xml("<key>foo</key><array/>")
).map(TestSpec.Builder::create).forEach(data::add);
//
// Test toMap() method.
//
Stream.of(
testSpec().expect(Map.of()).xml(),
testSpec().expect(Map.of()).xml("<key>foo</key><key>bar</key>"),
testSpec().expect(Map.of()).xml("<string>A</string><key>bar</key>"),
testSpec().expect(Map.of()).xml("<string>A</string>"),
testSpec().expect(Map.of()).xml("<key>foo</key><a/><string>A</string>"),
testSpec().expect(Map.of("foo", new Raw("A", Raw.Type.STRING))).xml("<key>foo</key><string>A</string><string>B</string>"),
testSpec().expect(Map.of("foo", new Raw("A", Raw.Type.STRING))).xml("<key>foo</key><string>A</string> hello <key>foo</key> bye <string>B</string>"),
testSpec().expect(Map.of("foo", new Raw("A", Raw.Type.STRING), "Foo", new Raw("B", Raw.Type.STRING))).xml("<key>foo</key><string>A</string><key>Foo</key><string>B</string>")
).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(
"<string>Hello</string><random/><dict><key>foo</key><string>Bye</string></dict><integer>integer</integer><dict/><true/>",
"<string>Hello</string><dict><data>Bingo</data><key>foo</key><string>Bye</string></dict><integer>integer</integer><dict/><true/>",
"<a><string>B</string></a><string>Hello</string><random/><dict><key>foo</key><string>Bye</string><string>Byeee</string></dict><integer>integer</integer><dict/><true/>",
"<string>Hello</string><random/><dict><key>bar</key><key>foo</key><string>Bye</string></dict><integer>integer</integer><dict/><true/>",
"<string>Hello</string><random/><dict><key>foo</key><string>Bye</string><key>foo</key><string>ByeBye</string></dict><integer>integer</integer><dict/><true/>"
).map(xml -> {
return "<key>foo</key><array>" + xml + "</array>";
}).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<String> content = new ArrayList<>();
content.add("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
content.add("<plist version=\"1.0\">");
content.add("<dict>");
content.addAll(List.of(xml));
content.add("</dict>");
content.add("</plist>");
return String.join("", content.toArray(String[]::new));
}

View File

@ -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<String> values) {
List<String> result = plist.queryArrayValue(key);
List<String> 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"));
}
}