From 6e05b5ee58e67cd40e58c6f6002f3599897ca358 Mon Sep 17 00:00:00 2001 From: Glavo Date: Wed, 14 May 2025 22:18:02 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BD=BF=E7=94=A8=20JNA=20=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E8=A1=A8=20(#3913)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/java/JavaManager.java | 76 ++------ HMCLCore/build.gradle.kts | 2 + .../hmcl/util/platform/windows/Advapi32.java | 59 ++++++ .../util/platform/windows/WinConstants.java | 44 +++++ .../hmcl/util/platform/windows/WinReg.java | 169 +++++++++++++++++- .../hmcl/util/platform/windows/WinTypes.java | 2 - .../util/platform/windows/WinRegTest.java | 113 ++++++++++++ gradle/libs.versions.toml | 1 + 8 files changed, 402 insertions(+), 64 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/Advapi32.java create mode 100644 HMCLCore/src/test/java/org/jackhuang/hmcl/util/platform/windows/WinRegTest.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/java/JavaManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/java/JavaManager.java index fb5af2026..e885e637c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/java/JavaManager.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/java/JavaManager.java @@ -31,18 +31,17 @@ import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.util.CacheRepository; import org.jackhuang.hmcl.util.Lang; -import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.platform.Architecture; import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jackhuang.hmcl.util.platform.Platform; import org.jackhuang.hmcl.util.platform.UnsupportedPlatformException; +import org.jackhuang.hmcl.util.platform.windows.WinReg; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.jetbrains.annotations.Nullable; import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStreamReader; import java.nio.file.*; import java.util.*; import java.util.concurrent.CountDownLatch; @@ -357,10 +356,10 @@ public final class JavaManager { switch (OperatingSystem.CURRENT_OS) { case WINDOWS: - queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Runtime Environment\\"); - queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Development Kit\\"); - queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JRE\\"); - queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JDK\\"); + queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Runtime Environment"); + queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Development Kit"); + queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JRE"); + queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JDK"); searchJavaInProgramFiles(javaRuntimes, "ProgramFiles", "C:\\Program Files"); searchJavaInProgramFiles(javaRuntimes, "ProgramFiles(x86)", "C:\\Program Files (x86)"); @@ -646,67 +645,22 @@ public final class JavaManager { } // ==== Windows Registry Support ==== - private static void queryJavaInRegistryKey(Map javaRuntimes, String location) { - for (String java : querySubFolders(location)) { - if (!querySubFolders(java).contains(java + "\\MSI")) + private static void queryJavaInRegistryKey(Map javaRuntimes, WinReg.HKEY hkey, String location) { + WinReg reg = WinReg.INSTANCE; + if (reg == null) + return; + + for (String java : reg.queryKeys(hkey, location)) { + if (!reg.queryKeys(hkey, java).contains(java + "\\MSI")) continue; - String home = queryRegisterValue(java, "JavaHome"); - if (home != null) { + Object home = reg.queryValue(hkey, java, "JavaHome"); + if (home instanceof String) { try { - tryAddJavaHome(javaRuntimes, Paths.get(home)); + tryAddJavaHome(javaRuntimes, Paths.get((String) home)); } catch (InvalidPathException e) { LOG.warning("Invalid Java path in system registry: " + home); } } } } - - private static List querySubFolders(String location) { - List res = new ArrayList<>(); - - try { - Process process = Runtime.getRuntime().exec(new String[]{"cmd", "/c", "reg", "query", location}); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), OperatingSystem.NATIVE_CHARSET))) { - for (String line; (line = reader.readLine()) != null; ) { - if (line.startsWith(location) && !line.equals(location)) { - res.add(line); - } - } - } - } catch (IOException e) { - LOG.warning("Failed to query sub folders of " + location, e); - } - return res; - } - - private static String queryRegisterValue(String location, String name) { - boolean last = false; - - try { - Process process = Runtime.getRuntime().exec(new String[]{"cmd", "/c", "reg", "query", location, "/v", name}); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), OperatingSystem.NATIVE_CHARSET))) { - for (String line; (line = reader.readLine()) != null; ) { - if (StringUtils.isNotBlank(line)) { - if (last && line.trim().startsWith(name)) { - int begins = line.indexOf(name); - if (begins > 0) { - String s2 = line.substring(begins + name.length()); - begins = s2.indexOf("REG_SZ"); - if (begins > 0) { - return s2.substring(begins + "REG_SZ".length()).trim(); - } - } - } - if (location.equals(line.trim())) { - last = true; - } - } - } - } - } catch (IOException e) { - LOG.warning("Failed to query register value of " + location, e); - } - - return null; - } } diff --git a/HMCLCore/build.gradle.kts b/HMCLCore/build.gradle.kts index 18b2c6c45..a7e92aa9d 100644 --- a/HMCLCore/build.gradle.kts +++ b/HMCLCore/build.gradle.kts @@ -19,4 +19,6 @@ dependencies { api(libs.pci.ids) compileOnlyApi(libs.jetbrains.annotations) + + testImplementation(libs.jna.platform) } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/Advapi32.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/Advapi32.java new file mode 100644 index 000000000..274826cd6 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/Advapi32.java @@ -0,0 +1,59 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.util.platform.windows; + +import com.sun.jna.Pointer; +import com.sun.jna.WString; +import com.sun.jna.ptr.IntByReference; +import com.sun.jna.ptr.PointerByReference; +import com.sun.jna.win32.StdCallLibrary; +import org.jackhuang.hmcl.util.platform.NativeUtils; + +/** + * @author Glavo + */ +public interface Advapi32 extends StdCallLibrary { + + Advapi32 INSTANCE = NativeUtils.USE_JNA && com.sun.jna.Platform.isWindows() + ? NativeUtils.load("advapi32", Advapi32.class) + : null; + + /** + * @see RegOpenKeyExW function + */ + int RegOpenKeyExW(Pointer hKey, WString lpSubKey, int ulOptions, int samDesired, PointerByReference phkResult); + + /** + * @see RegCloseKey function + */ + int RegCloseKey(Pointer hKey); + + /** + * @see RegQueryValueExW function + */ + int RegQueryValueExW(Pointer hKey, WString lpValueName, Pointer lpReserved, IntByReference lpType, Pointer lpData, IntByReference lpcbData); + + /** + * @see RegEnumKeyExW function + */ + int RegEnumKeyExW(Pointer hKey, int dwIndex, + Pointer lpName, IntByReference lpcchName, + IntByReference lpReserved, + Pointer lpClass, IntByReference lpcchClass, + Pointer lpftLastWriteTime); +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/WinConstants.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/WinConstants.java index 8645ffc95..c44b929a4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/WinConstants.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/WinConstants.java @@ -22,9 +22,53 @@ package org.jackhuang.hmcl.util.platform.windows; */ public interface WinConstants { + // https://learn.microsoft.com/windows/win32/debug/system-error-codes--0-499- + int ERROR_SUCCESS = 0; + int ERROR_FILE_NOT_FOUND = 2; + int ERROR_PATH_NOT_FOUND = 3; + int ERROR_ACCESS_DENIED = 5; + int ERROR_INVALID_HANDLE = 6; + int ERROR_INVALID_DATA = 13; + int ERROR_NOT_SAME_DEVICE = 17; + int ERROR_NOT_READY = 21; + int ERROR_SHARING_VIOLATION = 32; + int ERROR_FILE_EXISTS = 80; + int ERROR_INVALID_PARAMETER = 87; + int ERROR_DISK_FULL = 112; + int ERROR_INSUFFICIENT_BUFFER = 122; + int ERROR_INVALID_LEVEL = 124; + int ERROR_DIR_NOT_ROOT = 144; + int ERROR_DIR_NOT_EMPTY = 145; + int ERROR_ALREADY_EXISTS = 183; + int ERROR_MORE_DATA = 234; + int ERROR_NO_MORE_ITEMS = 259; + int ERROR_DIRECTORY = 267; + int ERROR_NOTIFY_ENUM_DIR = 1022; + int ERROR_PRIVILEGE_NOT_HELD = 1314; + int ERROR_NONE_MAPPED = 1332; + int ERROR_CANT_ACCESS_FILE = 1920; + int ERROR_NOT_A_REPARSE_POINT = 4390; + int ERROR_INVALID_REPARSE_DATA = 4392; + // https://learn.microsoft.com/windows/win32/sysinfo/registry-key-security-and-access-rights + int KEY_QUERY_VALUE = 0x0001; + int KEY_ENUMERATE_SUB_KEYS = 0x0008; int KEY_READ = 0x20019; + // https://learn.microsoft.com/windows/win32/sysinfo/registry-value-types + int REG_NONE = 0; + int REG_SZ = 1; + int REG_EXPAND_SZ = 2; + int REG_BINARY = 3; + int REG_DWORD_LITTLE_ENDIAN = 4; + int REG_DWORD_BIG_ENDIAN = 5; + int REG_LINK = 6; + int REG_MULTI_SZ = 7; + int REG_RESOURCE_LIST = 8; + int REG_FULL_RESOURCE_DESCRIPTOR = 9; + int REG_RESOURCE_REQUIREMENTS_LIST = 10; + int REG_QWORD_LITTLE_ENDIAN = 11; + // https://learn.microsoft.com/windows/win32/sysinfo/predefined-keys long HKEY_CLASSES_ROOT = 0x80000000L; long HKEY_CURRENT_USER = 0x80000001L; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/WinReg.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/WinReg.java index f725197cf..dd2a894f7 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/WinReg.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/WinReg.java @@ -17,11 +17,28 @@ */ package org.jackhuang.hmcl.util.platform.windows; +import com.sun.jna.Memory; +import com.sun.jna.Pointer; +import com.sun.jna.WString; +import com.sun.jna.ptr.IntByReference; +import com.sun.jna.ptr.PointerByReference; +import org.jackhuang.hmcl.util.platform.NativeUtils; + +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + /** * @author Glavo */ public abstract class WinReg { + public static final WinReg INSTANCE = NativeUtils.USE_JNA && Advapi32.INSTANCE != null + ? new JNAWinReg(Advapi32.INSTANCE) : null; + /** * @see Predefined Keys */ @@ -36,7 +53,6 @@ public abstract class WinReg { HKEY_CURRENT_CONFIG(0x80000005), HKEY_DYN_DATA(0x80000006), HKEY_CURRENT_USER_LOCAL_SETTINGS(0x80000007); - private final int value; HKEY(int value) { @@ -46,5 +62,156 @@ public abstract class WinReg { public int getValue() { return value; } + + public Pointer toPointer() { + return Pointer.createConstant((long) value); + } } + + public abstract boolean exists(HKEY root, String key); + + public abstract Object queryValue(HKEY root, String key, String valueName); + + public abstract List queryKeys(HKEY root, String key); + + private static final class JNAWinReg extends WinReg { + + private final Advapi32 advapi32; + + JNAWinReg(Advapi32 advapi32) { + this.advapi32 = advapi32; + } + + @Override + public boolean exists(HKEY root, String key) { + PointerByReference phkKey = new PointerByReference(); + int status = advapi32.RegOpenKeyExW(root.toPointer(), new WString(key), 0, WinConstants.KEY_READ, phkKey); + + if (status == WinConstants.ERROR_SUCCESS) { + advapi32.RegCloseKey(phkKey.getValue()); + return true; + } else + return false; + } + + private static void checkLength(int expected, int actual) { + if (expected != actual) { + throw new IllegalStateException("Expected " + expected + " bytes, but got " + actual); + } + } + + @Override + public Object queryValue(HKEY root, String key, String valueName) { + PointerByReference phkKey = new PointerByReference(); + if (advapi32.RegOpenKeyExW(root.toPointer(), new WString(key), 0, WinConstants.KEY_READ, phkKey) != WinConstants.ERROR_SUCCESS) + return null; + + Pointer hkey = phkKey.getValue(); + try { + IntByReference lpType = new IntByReference(); + IntByReference lpcbData = new IntByReference(); + int status = advapi32.RegQueryValueExW(hkey, new WString(valueName), null, lpType, null, lpcbData); + if (status != WinConstants.ERROR_SUCCESS) { + if (status == WinConstants.ERROR_FILE_NOT_FOUND) + return null; + else + throw new RuntimeException("Failed to query value: " + status); + } + + int type = lpType.getValue(); + int cbData = lpcbData.getValue(); + + try (Memory lpData = new Memory(cbData)) { + status = advapi32.RegQueryValueExW(hkey, new WString(valueName), null, null, lpData, lpcbData); + if (status != WinConstants.ERROR_SUCCESS) { + if (status == WinConstants.ERROR_FILE_NOT_FOUND) + return null; + else + throw new RuntimeException("Failed to query value: " + status); + } + + checkLength(cbData, lpcbData.getValue()); + + switch (type) { + case WinConstants.REG_NONE: + case WinConstants.REG_BINARY: + return lpData.getByteArray(0L, cbData); + case WinConstants.REG_DWORD_LITTLE_ENDIAN: + case WinConstants.REG_DWORD_BIG_ENDIAN: { + checkLength(4, cbData); + int value = lpData.getInt(0L); + ByteOrder expectedOrder = type == WinConstants.REG_DWORD_LITTLE_ENDIAN ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN; + if (expectedOrder != ByteOrder.nativeOrder()) + value = Integer.reverseBytes(value); + return value; + } + case WinConstants.REG_QWORD_LITTLE_ENDIAN: { + checkLength(8, cbData); + long value = lpData.getLong(0L); + if (ByteOrder.nativeOrder() != ByteOrder.LITTLE_ENDIAN) + value = Long.reverseBytes(value); + return value; + } + case WinConstants.REG_SZ: + case WinConstants.REG_EXPAND_SZ: + case WinConstants.REG_LINK: { + if (cbData < 2) + throw new RuntimeException("Illegal length: " + cbData); + if (lpData.getChar(cbData - 2) != '\0') + throw new RuntimeException("The string does not end with \\0"); + + return lpData.getWideString(0L); + } + default: + throw new RuntimeException("Unknown reg type: " + type); + } + } + } catch (Throwable e) { + LOG.warning("Failed to query value", e); + } finally { + advapi32.RegCloseKey(hkey); + } + + return null; + } + + @Override + public List queryKeys(HKEY root, String key) { + PointerByReference phkKey = new PointerByReference(); + if (advapi32.RegOpenKeyExW(root.toPointer(), new WString(key), 0, WinConstants.KEY_READ, phkKey) != WinConstants.ERROR_SUCCESS) + return Collections.emptyList(); + + Pointer hkey = phkKey.getValue(); + try { + String prefix = key.endsWith("\\") ? key : key + "\\"; + ArrayList res = new ArrayList<>(); + int maxKeyLength = 256; + try (Memory lpName = new Memory(maxKeyLength * 2)) { + IntByReference lpcchName = new IntByReference(); + int i = 0; + + while (true) { + lpcchName.setValue(maxKeyLength); + int status = advapi32.RegEnumKeyExW(hkey, i, lpName, lpcchName, null, null, null, null); + if (status == WinConstants.ERROR_SUCCESS) { + res.add(prefix + lpName.getWideString(0L)); + i++; + } else { + if (status != WinConstants.ERROR_NO_MORE_ITEMS) + LOG.warning("Failed to enum key: " + status); + break; + } + } + } + return res; + } catch (Throwable e) { + LOG.warning("Failed to query keys", e); + } finally { + advapi32.RegCloseKey(hkey); + } + + return Collections.emptyList(); + } + } + } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/WinTypes.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/WinTypes.java index cc674e744..8f1b55c0d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/WinTypes.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/WinTypes.java @@ -87,6 +87,4 @@ public interface WinTypes { "ullTotalVirtual", "ullAvailVirtual", "ullAvailExtendedVirtual"); } } - - ; } diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/platform/windows/WinRegTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/platform/windows/WinRegTest.java new file mode 100644 index 000000000..80e5dc706 --- /dev/null +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/platform/windows/WinRegTest.java @@ -0,0 +1,113 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.util.platform.windows; + +import com.sun.jna.platform.win32.Advapi32Util; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIf; + +import java.util.Arrays; +import java.util.Collections; +import java.util.UUID; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * @author Glavo + */ +@EnabledIf("isEnabled") +public final class WinRegTest { + public static boolean isEnabled() { + return WinReg.INSTANCE != null; + } + + private static final com.sun.jna.platform.win32.WinReg.HKEY ROOT_KEY = com.sun.jna.platform.win32.WinReg.HKEY_CURRENT_USER; + private static final String KEY_BASE = "Software\\JavaSoft\\Prefs\\hmcl\\test"; + private static String key; + + private static final String[] SUBKEYS = { + "Sub0", "Sub1", "Sub2", "Sub3" + }; + + private static final byte[] TEST_DATA = new byte[128]; + + static { + for (int i = 0; i < TEST_DATA.length; i++) { + TEST_DATA[i] = (byte) i; + } + } + + @BeforeAll + public static void setup() { + key = KEY_BASE + "\\" + UUID.randomUUID(); + if (!Advapi32Util.registryCreateKey(ROOT_KEY, key)) + throw new AssertionError("Failed to create key"); + + Advapi32Util.registrySetBinaryValue(ROOT_KEY, key, "BINARY", TEST_DATA); + Advapi32Util.registrySetStringValue(ROOT_KEY, key, "SZ", "Hello World!"); + Advapi32Util.registrySetIntValue(ROOT_KEY, key, "DWORD", 0xCAFEBABE); + Advapi32Util.registrySetLongValue(ROOT_KEY, key, "QWORD", 0xCAFEBABEL); + + for (String subkey : SUBKEYS) { + if (!Advapi32Util.registryCreateKey(ROOT_KEY, key, subkey)) + throw new AssertionError("Failed to create key"); + } + } + + @AfterAll + public static void cleanUp() { + if (key != null) { + if (!Advapi32Util.registryKeyExists(ROOT_KEY, key)) + return; + + for (String subKey : Advapi32Util.registryGetKeys(ROOT_KEY, key)) + Advapi32Util.registryDeleteKey(ROOT_KEY, key, subKey); + + Advapi32Util.registryDeleteKey(ROOT_KEY, key); + } + } + + @Test + public void testQueryValue() { + WinReg.HKEY hkey = WinReg.HKEY.HKEY_CURRENT_USER; + WinReg reg = WinReg.INSTANCE; + + assertArrayEquals(TEST_DATA, (byte[]) reg.queryValue(hkey, key, "BINARY")); + assertEquals("Hello World!", reg.queryValue(hkey, key, "SZ")); + assertEquals(0xCAFEBABE, reg.queryValue(hkey, key, "DWORD")); + assertEquals(0xCAFEBABEL, reg.queryValue(hkey, key, "QWORD")); + assertNull(reg.queryValue(hkey, key, "UNKNOWN")); + assertNull(reg.queryValue(hkey, KEY_BASE + "\\" + "NOT_EXIST", "UNKNOWN")); + } + + @Test + public void testQueryKeys() { + WinReg.HKEY hkey = WinReg.HKEY.HKEY_CURRENT_USER; + WinReg reg = WinReg.INSTANCE; + + assertEquals(Arrays.asList(SUBKEYS).stream().map(it -> key + "\\" + it).collect(Collectors.toList()), + reg.queryKeys(hkey, key).stream().sorted().collect(Collectors.toList())); + for (String subkey : SUBKEYS) { + assertEquals(Collections.emptyList(), reg.queryKeys(hkey, key + "\\" + subkey)); + } + assertEquals(Collections.emptyList(), reg.queryKeys(hkey, key + "\\NOT_EXIST")); + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ff6923997..d2fcd1d61 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,6 +34,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } chardet = { module = "org.glavo:chardet", version.ref = "chardet" } twelvemonkeys-imageio-webp = { module = "com.twelvemonkeys.imageio:imageio-webp", version.ref = "twelvemonkeys" } jna = { module = "net.java.dev.jna:jna", version.ref = "jna" } +jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jna" } pci-ids = { module = "org.glavo:pci-ids", version.ref = "pci-ids" } [plugins]