mirror of
https://github.com/openjdk/jdk.git
synced 2026-07-28 03:43:21 +00:00
8358723: jpackage signing issues: the main launcher doesn't have entitlements
Reviewed-by: almatvee
This commit is contained in:
parent
f0e1078c71
commit
bdb7d25ac1
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 -> {
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user