mirror of
https://github.com/reactos/reactos.git
synced 2025-07-31 18:22:35 +00:00
[WINESYNC] setupapi: Support full path enumerator in SetupDiGetClassDevs.
Based on a patch by Michael Müller. Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=35345 Signed-off-by: Zhiyi Zhang <zzhang@codeweavers.com> Signed-off-by: Alexandre Julliard <julliard@winehq.org> wine commit id d5e24897e2cd61a5af71a317c1797757acf1b794 by Zhiyi Zhang <zzhang@codeweavers.com> [WINESYNC] setupapi/tests: A spelling fix in an ok() message. Signed-off-by: Francois Gouget <fgouget@free.fr> Signed-off-by: Alexandre Julliard <julliard@winehq.org> wine commit id 21db058d14ac6fb769a42bdeea3dadfa4fd0e8bf by Francois Gouget <fgouget@free.fr> [WINESYNC] setupapi/tests: Fix a crash in the need_media tests. Signed-off-by: Sven Baars <sven.wine@gmail.com> Signed-off-by: Alexandre Julliard <julliard@winehq.org> wine commit id 448f4cc9a25c6f994395978df0dfd7c7487b5dbb by Sven Baars <sven.wine@gmail.com> [WINESYNC] setupapi: Fix a path leak (Valgrind). Signed-off-by: Sven Baars <sven.wine@gmail.com> Signed-off-by: Alexandre Julliard <julliard@winehq.org> wine commit id 5f0d53e20903b009ee47a238a642f2e95f27d712 by Sven Baars <sven.wine@gmail.com> [WINESYNC] setupapi: Fix an uninitialized variable warning (Valgrind). Signed-off-by: Sven Baars <sven.wine@gmail.com> Signed-off-by: Alexandre Julliard <julliard@winehq.org> wine commit id ecbd2dd34b4239318162242caaa197ae4f0911f5 by Sven Baars <sven.wine@gmail.com> [WINESYNC] setupapi/tests: Add some tests for SPFILENOTIFY_STARTCOPY. Signed-off-by: Zebediah Figura <z.figura12@gmail.com> Signed-off-by: Alexandre Julliard <julliard@winehq.org> wine commit id ab3cc3c11a78cbaec7173a4576fda93ecf70ae90 by Zebediah Figura <z.figura12@gmail.com> [WINESYNC] setupapi: Fix handling of FILEOP_SKIP from the SPFILENOTIFY_STARTCOPY callback. Fixes a regression introduced by 3e5c9798a80641e0e39e95e4467c60405b22b062. Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=47436 Signed-off-by: Zebediah Figura <z.figura12@gmail.com> Signed-off-by: Alexandre Julliard <julliard@winehq.org> wine commit id a431b4b54a36b49821cae363d7ea8733eefe62ef by Zebediah Figura <z.figura12@gmail.com> [WINESYNC] setupapi/tests: Remove win_9x checks. Signed-off-by: Vijay Kiran Kamuju <infyquest@gmail.com> Signed-off-by: Alexandre Julliard <julliard@winehq.org> wine commit id c59e9d302b79527f4a99be71d1346f56737dc3f3 by Vijay Kiran Kamuju <infyquest@gmail.com> [WINESYNC] setupapi: Define .inf section names for ARM platforms. Signed-off-by: Alexandre Julliard <julliard@winehq.org> wine commit id b2b3975f46c203a129a946128ccd018dd7ed6e46 by Alexandre Julliard <julliard@winehq.org> [WINESYNC] setupapi/tests: Fix timeout on win10 1809+. Signed-off-by: Zhiyi Zhang <zzhang@codeweavers.com> Signed-off-by: Alexandre Julliard <julliard@winehq.org> wine commit id 177d6d7f8981b9e036437d78023b76232dcadb3d by Zhiyi Zhang <zzhang@codeweavers.com> [WINESYNC] setupapi/tests: Add default device registry property tests. Signed-off-by: Zhiyi Zhang <zzhang@codeweavers.com> Signed-off-by: Alexandre Julliard <julliard@winehq.org> wine commit id 4fe3d7e220225ddd57ef0acd9142ef8437cdb9ca by Zhiyi Zhang <zzhang@codeweavers.com> [WINESYNC] setupapi: Set device SPDRP_CLASS registry property in create_device(). Signed-off-by: Zhiyi Zhang <zzhang@codeweavers.com> Signed-off-by: Alexandre Julliard <julliard@winehq.org> wine commit id 288a6625ad0177e258a0db1da166d292ff420b1e by Zhiyi Zhang <zzhang@codeweavers.com> [WINESYNC] setupapi: Add SetupGetInfDriverStoreLocationW stub. Signed-off-by: Derek Lesho <dlesho@codeweavers.com> Signed-off-by: Alexandre Julliard <julliard@winehq.org> wine commit id f1b94dc16c35a5c477b6df8aa94c6d3537642c77 by Derek Lesho <dlesho@codeweavers.com>
This commit is contained in:
parent
283648f6c5
commit
f554e58231
8 changed files with 331 additions and 105 deletions
|
@ -32,6 +32,16 @@ static const WCHAR source_disks_names_platform[] =
|
|||
{'S','o','u','r','c','e','D','i','s','k','s','N','a','m','e','s','.','a','m','d','6','4',0};
|
||||
static const WCHAR source_disks_files_platform[] =
|
||||
{'S','o','u','r','c','e','D','i','s','k','s','F','i','l','e','s','.','a','m','d','6','4',0};
|
||||
#elif defined(__arm__)
|
||||
static const WCHAR source_disks_names_platform[] =
|
||||
{'S','o','u','r','c','e','D','i','s','k','s','N','a','m','e','s','.','a','r','m',0};
|
||||
static const WCHAR source_disks_files_platform[] =
|
||||
{'S','o','u','r','c','e','D','i','s','k','s','F','i','l','e','s','.','a','r','m',0};
|
||||
#elif defined(__aarch64__)
|
||||
static const WCHAR source_disks_names_platform[] =
|
||||
{'S','o','u','r','c','e','D','i','s','k','s','N','a','m','e','s','.','a','r','m','6','4',0};
|
||||
static const WCHAR source_disks_files_platform[] =
|
||||
{'S','o','u','r','c','e','D','i','s','k','s','F','i','l','e','s','.','a','r','m','6','4',0};
|
||||
#else /* FIXME: other platforms */
|
||||
static const WCHAR source_disks_names_platform[] =
|
||||
{'S','o','u','r','c','e','D','i','s','k','s','N','a','m','e','s',0};
|
||||
|
@ -732,3 +742,21 @@ BOOL WINAPI SetupQueryInfOriginalFileInformationW(
|
|||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
#if (_WIN32_WINNT >= _WIN32_WINNT_VISTA) || (DLL_EXPORT_VERSION >= _WIN32_WINNT_VISTA)
|
||||
|
||||
/***********************************************************************
|
||||
* SetupGetInfDriverStoreLocationW (SETUPAPI.@)
|
||||
*/
|
||||
BOOL WINAPI SetupGetInfDriverStoreLocationW(
|
||||
PCWSTR FileName, PSP_ALTPLATFORM_INFO AlternativePlatformInfo,
|
||||
PCWSTR LocaleName, PWSTR ReturnBuffer, DWORD ReturnBufferSize,
|
||||
PDWORD RequiredSize)
|
||||
{
|
||||
FIXME("stub: %s %p %s %p %u %p\n", debugstr_w(FileName), AlternativePlatformInfo, debugstr_w(LocaleName), ReturnBuffer, ReturnBufferSize, RequiredSize);
|
||||
|
||||
SetLastError(ERROR_CALL_NOT_IMPLEMENTED);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
#endif // (_WIN32_WINNT >= _WIN32_WINNT_VISTA) || (DLL_EXPORT_VERSION >= _WIN32_WINNT_VISTA)
|
||||
|
|
|
@ -196,6 +196,7 @@ UINT CALLBACK QUEUE_callback_WtoA( void *context, UINT notification,
|
|||
switch(notification)
|
||||
{
|
||||
case SPFILENOTIFY_COPYERROR:
|
||||
buffer[0] = 0;
|
||||
param2 = (UINT_PTR)buffer;
|
||||
/* fall through */
|
||||
case SPFILENOTIFY_STARTDELETE:
|
||||
|
@ -1661,13 +1662,20 @@ BOOL WINAPI SetupCommitFileQueueW( HWND owner, HSPFILEQ handle, PSP_FILE_CALLBAC
|
|||
* actually isn't in a subdirectory, but keep track of what it
|
||||
* was, and then later strip it from the root path that we
|
||||
* ultimately resolve the source disk to. */
|
||||
WCHAR *src_path = op->src_path;
|
||||
WCHAR src_path[MAX_PATH];
|
||||
size_t path_len = 0;
|
||||
|
||||
op->src_path = NULL;
|
||||
if (src_path)
|
||||
src_path[0] = 0;
|
||||
if (op->src_path)
|
||||
{
|
||||
lstrcpyW(src_path, op->src_path);
|
||||
path_len = lstrlenW(src_path);
|
||||
|
||||
lstrcatW(op->media->root, backslashW);
|
||||
lstrcatW(op->media->root, src_path);
|
||||
lstrcatW(op->media->root, op->src_path);
|
||||
|
||||
heap_free(op->src_path);
|
||||
op->src_path = NULL;
|
||||
}
|
||||
|
||||
for (;;)
|
||||
|
@ -1703,12 +1711,11 @@ BOOL WINAPI SetupCommitFileQueueW( HWND owner, HSPFILEQ handle, PSP_FILE_CALLBAC
|
|||
|
||||
if (queue_copy_file( paths.Source, paths.Target, op, handler, context ))
|
||||
{
|
||||
if (src_path && !op->media->cabinet)
|
||||
if (path_len > 0 && !op->media->cabinet)
|
||||
{
|
||||
size_t root_len = lstrlenW(op->media->root), path_len = lstrlenW(src_path);
|
||||
size_t root_len = lstrlenW(op->media->root);
|
||||
if (path_len <= root_len && !wcsnicmp(op->media->root + root_len - path_len, src_path, path_len))
|
||||
op->media->root[root_len - path_len - 1] = 0;
|
||||
heap_free( src_path );
|
||||
}
|
||||
op->media->resolved = TRUE;
|
||||
#ifdef __REACTOS__
|
||||
|
@ -1732,7 +1739,7 @@ BOOL WINAPI SetupCommitFileQueueW( HWND owner, HSPFILEQ handle, PSP_FILE_CALLBAC
|
|||
if (op_result == FILEOP_ABORT)
|
||||
goto done;
|
||||
else if (op_result == FILEOP_SKIP)
|
||||
break;
|
||||
continue;
|
||||
else if (op_result != FILEOP_DOIT)
|
||||
FIXME("Unhandled return value %#x.\n", op_result);
|
||||
|
||||
|
|
|
@ -408,6 +408,7 @@
|
|||
@ stdcall SetupGetFileCompressionInfoW(wstr ptr ptr ptr ptr)
|
||||
@ stdcall SetupGetFileQueueCount(long long ptr)
|
||||
@ stdcall SetupGetFileQueueFlags(long ptr)
|
||||
@ stdcall -version=0x600+ SetupGetInfDriverStoreLocationW(wstr ptr wstr ptr long ptr)
|
||||
@ stdcall SetupGetInfFileListA(str long str long ptr)
|
||||
@ stdcall SetupGetInfFileListW(wstr long wstr long ptr)
|
||||
@ stdcall SetupGetInfInformationA(ptr long ptr long ptr)
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
#include "winuser.h"
|
||||
#include "winreg.h"
|
||||
#include "initguid.h"
|
||||
#include "devguid.h"
|
||||
#include "devpkey.h"
|
||||
#include "setupapi.h"
|
||||
#include "cfgmgr32.h"
|
||||
|
@ -1500,7 +1501,7 @@ static void test_registry_property_a(void)
|
|||
{
|
||||
static const CHAR bogus[] = "System\\CurrentControlSet\\Enum\\Root\\LEGACY_BOGUS";
|
||||
SP_DEVINFO_DATA device = {sizeof(device)};
|
||||
CHAR buf[6] = "";
|
||||
CHAR buf[64] = "";
|
||||
DWORD size, type;
|
||||
HDEVINFO set;
|
||||
BOOL ret;
|
||||
|
@ -1594,13 +1595,69 @@ todo_wine {
|
|||
|
||||
res = RegOpenKeyA(HKEY_LOCAL_MACHINE, bogus, &key);
|
||||
ok(res == ERROR_FILE_NOT_FOUND, "Key should not exist.\n");
|
||||
|
||||
/* Test existing registry properties right after device creation */
|
||||
set = SetupDiCreateDeviceInfoList(&guid, NULL);
|
||||
ok(set != INVALID_HANDLE_VALUE, "Failed to create device list, error %#x.\n", GetLastError());
|
||||
|
||||
/* Create device from a not registered class without device name */
|
||||
ret = SetupDiCreateDeviceInfoA(set, "LEGACY_BOGUS", &guid, NULL, NULL, DICD_GENERATE_ID, &device);
|
||||
ok(ret, "Failed to create device, error %#x.\n", GetLastError());
|
||||
|
||||
/* No SPDRP_DEVICEDESC property */
|
||||
SetLastError(0xdeadbeef);
|
||||
ret = SetupDiGetDeviceRegistryPropertyA(set, &device, SPDRP_DEVICEDESC, NULL, (BYTE *)buf, sizeof(buf), NULL);
|
||||
ok(!ret, "Expected failure.\n");
|
||||
ok(GetLastError() == ERROR_INVALID_DATA, "Got unexpected error %#x.\n", GetLastError());
|
||||
|
||||
/* No SPDRP_CLASS property */
|
||||
SetLastError(0xdeadbeef);
|
||||
ret = SetupDiGetDeviceRegistryPropertyA(set, &device, SPDRP_CLASS, NULL, (BYTE *)buf, sizeof(buf), NULL);
|
||||
ok(!ret, "Expected failure.\n");
|
||||
ok(GetLastError() == ERROR_INVALID_DATA, "Got unexpected error %#x.\n", GetLastError());
|
||||
|
||||
/* Have SPDRP_CLASSGUID property */
|
||||
memset(buf, 0, sizeof(buf));
|
||||
ret = SetupDiGetDeviceRegistryPropertyA(set, &device, SPDRP_CLASSGUID, NULL, (BYTE *)buf, sizeof(buf), NULL);
|
||||
ok(ret, "Failed to get property, error %#x.\n", GetLastError());
|
||||
ok(!lstrcmpiA(buf, "{6a55b5a4-3f65-11db-b704-0011955c2bdb}"), "Got unexpected value %s.\n", buf);
|
||||
|
||||
ret = SetupDiDeleteDeviceInfo(set, &device);
|
||||
ok(ret, "Failed to delete device, error %#x.\n", GetLastError());
|
||||
|
||||
/* Create device from a not registered class with a device name */
|
||||
ret = SetupDiCreateDeviceInfoA(set, "LEGACY_BOGUS", &guid, "device_name", NULL, DICD_GENERATE_ID, &device);
|
||||
ok(ret, "Failed to create device, error %#x.\n", GetLastError());
|
||||
|
||||
/* Have SPDRP_DEVICEDESC property */
|
||||
memset(buf, 0, sizeof(buf));
|
||||
ret = SetupDiGetDeviceRegistryPropertyA(set, &device, SPDRP_DEVICEDESC, NULL, (BYTE *)buf, sizeof(buf), NULL);
|
||||
ok(ret, "Failed to get property, error %#x.\n", GetLastError());
|
||||
ok(!lstrcmpA(buf, "device_name"), "Got unexpected value %s.\n", buf);
|
||||
|
||||
SetupDiDestroyDeviceInfoList(set);
|
||||
|
||||
/* Create device from a registered class */
|
||||
set = SetupDiCreateDeviceInfoList(&GUID_DEVCLASS_DISPLAY, NULL);
|
||||
ok(set != INVALID_HANDLE_VALUE, "Failed to create device list, error %#x.\n", GetLastError());
|
||||
|
||||
ret = SetupDiCreateDeviceInfoA(set, "display", &GUID_DEVCLASS_DISPLAY, NULL, NULL, DICD_GENERATE_ID, &device);
|
||||
ok(ret, "Failed to create device, error %#x.\n", GetLastError());
|
||||
|
||||
/* Have SPDRP_CLASS property */
|
||||
memset(buf, 0, sizeof(buf));
|
||||
ret = SetupDiGetDeviceRegistryPropertyA(set, &device, SPDRP_CLASS, NULL, (BYTE *)buf, sizeof(buf), NULL);
|
||||
ok(ret, "Failed to get property, error %#x.\n", GetLastError());
|
||||
ok(!lstrcmpA(buf, "Display"), "Got unexpected value %s.\n", buf);
|
||||
|
||||
SetupDiDestroyDeviceInfoList(set);
|
||||
}
|
||||
|
||||
static void test_registry_property_w(void)
|
||||
{
|
||||
WCHAR friendly_name[] = {'B','o','g','u','s',0};
|
||||
SP_DEVINFO_DATA device = {sizeof(device)};
|
||||
WCHAR buf[6] = {0};
|
||||
WCHAR buf[64] = {0};
|
||||
DWORD size, type;
|
||||
HDEVINFO set;
|
||||
BOOL ret;
|
||||
|
@ -1698,6 +1755,62 @@ todo_wine {
|
|||
|
||||
res = RegOpenKeyW(HKEY_LOCAL_MACHINE, bogus, &key);
|
||||
ok(res == ERROR_FILE_NOT_FOUND, "Key should not exist.\n");
|
||||
|
||||
/* Test existing registry properties right after device creation */
|
||||
set = SetupDiCreateDeviceInfoList(&guid, NULL);
|
||||
ok(set != INVALID_HANDLE_VALUE, "Failed to create device list, error %#x.\n", GetLastError());
|
||||
|
||||
/* Create device from a not registered class without device name */
|
||||
ret = SetupDiCreateDeviceInfoW(set, L"LEGACY_BOGUS", &guid, NULL, NULL, DICD_GENERATE_ID, &device);
|
||||
ok(ret, "Failed to create device, error %#x.\n", GetLastError());
|
||||
|
||||
/* No SPDRP_DEVICEDESC property */
|
||||
SetLastError(0xdeadbeef);
|
||||
ret = SetupDiGetDeviceRegistryPropertyW(set, &device, SPDRP_DEVICEDESC, NULL, (BYTE *)buf, sizeof(buf), NULL);
|
||||
ok(!ret, "Expected failure.\n");
|
||||
ok(GetLastError() == ERROR_INVALID_DATA, "Got unexpected error %#x.\n", GetLastError());
|
||||
|
||||
/* No SPDRP_CLASS property */
|
||||
SetLastError(0xdeadbeef);
|
||||
ret = SetupDiGetDeviceRegistryPropertyW(set, &device, SPDRP_CLASS, NULL, (BYTE *)buf, sizeof(buf), NULL);
|
||||
ok(!ret, "Expected failure.\n");
|
||||
ok(GetLastError() == ERROR_INVALID_DATA, "Got unexpected error %#x.\n", GetLastError());
|
||||
|
||||
/* Have SPDRP_CLASSGUID property */
|
||||
memset(buf, 0, sizeof(buf));
|
||||
ret = SetupDiGetDeviceRegistryPropertyW(set, &device, SPDRP_CLASSGUID, NULL, (BYTE *)buf, sizeof(buf), NULL);
|
||||
ok(ret, "Failed to get property, error %#x.\n", GetLastError());
|
||||
ok(!lstrcmpiW(buf, L"{6a55b5a4-3f65-11db-b704-0011955c2bdb}"), "Got unexpected value %s.\n", wine_dbgstr_w(buf));
|
||||
|
||||
ret = SetupDiDeleteDeviceInfo(set, &device);
|
||||
ok(ret, "Failed to delete device, error %#x.\n", GetLastError());
|
||||
|
||||
/* Create device from a not registered class with a device name */
|
||||
ret = SetupDiCreateDeviceInfoW(set, L"LEGACY_BOGUS", &guid, L"device_name", NULL, DICD_GENERATE_ID, &device);
|
||||
ok(ret, "Failed to create device, error %#x.\n", GetLastError());
|
||||
|
||||
/* Have SPDRP_DEVICEDESC property */
|
||||
memset(buf, 0, sizeof(buf));
|
||||
ret = SetupDiGetDeviceRegistryPropertyW(set, &device, SPDRP_DEVICEDESC, NULL, (BYTE *)buf, sizeof(buf), NULL);
|
||||
ok(ret, "Failed to get property, error %#x.\n", GetLastError());
|
||||
ok(!lstrcmpW(buf, L"device_name"), "Got unexpected value %s.\n", wine_dbgstr_w(buf));
|
||||
|
||||
SetupDiDestroyDeviceInfoList(set);
|
||||
|
||||
/* Create device from a registered class */
|
||||
set = SetupDiCreateDeviceInfoList(&GUID_DEVCLASS_DISPLAY, NULL);
|
||||
ok(set != INVALID_HANDLE_VALUE, "Failed to create device list, error %#x.\n", GetLastError());
|
||||
|
||||
ret = SetupDiCreateDeviceInfoW(set, L"display", &GUID_DEVCLASS_DISPLAY, NULL, NULL, DICD_GENERATE_ID, &device);
|
||||
ok(ret, "Failed to create device, error %#x.\n", GetLastError());
|
||||
|
||||
/* Have SPDRP_CLASS property */
|
||||
memset(buf, 0, sizeof(buf));
|
||||
ret = SetupDiGetDeviceRegistryPropertyW(set, &device, SPDRP_CLASS, NULL, (BYTE *)buf, sizeof(buf), NULL);
|
||||
ok(ret, "Failed to get property, error %#x.\n", GetLastError());
|
||||
ok(!lstrcmpW(buf, L"Display"), "Got unexpected value %s.\n", wine_dbgstr_w(buf));
|
||||
|
||||
SetupDiDestroyDeviceInfoList(set);
|
||||
}
|
||||
|
||||
static void test_get_inf_class(void)
|
||||
|
@ -2069,12 +2182,6 @@ static void test_get_actual_section(void)
|
|||
ok(!strcasecmp(section, "section1"), "Got unexpected section %s.\n", section);
|
||||
ok(size == 9, "Got size %u.\n", size);
|
||||
|
||||
extptr = section;
|
||||
ret = SetupDiGetActualSectionToInstallA(hinf, "section1", section, ARRAY_SIZE(section), NULL, &extptr);
|
||||
ok(ret, "Failed to get section, error %#x.\n", GetLastError());
|
||||
ok(!strcasecmp(section, "section1"), "Got unexpected section %s.\n", section);
|
||||
ok(!extptr || !*extptr /* Windows 10 1809 */, "Got extension %s.\n", extptr);
|
||||
|
||||
extptr = section;
|
||||
ret = SetupDiGetActualSectionToInstallA(hinf, "section2", section, ARRAY_SIZE(section), NULL, &extptr);
|
||||
ok(ret, "Failed to get section, error %#x.\n", GetLastError());
|
||||
|
@ -2105,24 +2212,6 @@ static void test_get_actual_section(void)
|
|||
ok(!strcasecmp(section, "section6.NT" MYEXT), "Got unexpected section %s.\n", section);
|
||||
ok(extptr == section + 8, "Got extension %s.\n", extptr);
|
||||
|
||||
extptr = section;
|
||||
ret = SetupDiGetActualSectionToInstallA(hinf, "section7", section, ARRAY_SIZE(section), NULL, &extptr);
|
||||
ok(ret, "Failed to get section, error %#x.\n", GetLastError());
|
||||
ok(!strcasecmp(section, "section7"), "Got unexpected section %s.\n", section);
|
||||
ok(!extptr || !*extptr /* Windows 10 1809 */, "Got extension %s.\n", extptr);
|
||||
|
||||
extptr = section;
|
||||
ret = SetupDiGetActualSectionToInstallA(hinf, "section8", section, ARRAY_SIZE(section), NULL, &extptr);
|
||||
ok(ret, "Failed to get section, error %#x.\n", GetLastError());
|
||||
ok(!strcasecmp(section, "section8"), "Got unexpected section %s.\n", section);
|
||||
ok(!extptr || !*extptr /* Windows 10 1809 */, "Got extension %s.\n", extptr);
|
||||
|
||||
extptr = section;
|
||||
ret = SetupDiGetActualSectionToInstallA(hinf, "nonexistent", section, ARRAY_SIZE(section), NULL, &extptr);
|
||||
ok(ret, "Failed to get section, error %#x.\n", GetLastError());
|
||||
ok(!strcasecmp(section, "nonexistent"), "Got unexpected section %s.\n", section);
|
||||
ok(!extptr || !*extptr /* Windows 10 1809 */, "Got extension %s.\n", extptr);
|
||||
|
||||
extptr = section;
|
||||
ret = SetupDiGetActualSectionToInstallA(hinf, "section9", section, ARRAY_SIZE(section), NULL, &extptr);
|
||||
ok(ret, "Failed to get section, error %#x.\n", GetLastError());
|
||||
|
@ -2131,7 +2220,31 @@ static void test_get_actual_section(void)
|
|||
|
||||
if (0)
|
||||
{
|
||||
/* For some reason, this call hangs on Windows 10 1809. */
|
||||
/* For some reason, these calls hang on Windows 10 1809+. */
|
||||
extptr = section;
|
||||
ret = SetupDiGetActualSectionToInstallA(hinf, "section1", section, ARRAY_SIZE(section), NULL, &extptr);
|
||||
ok(ret, "Failed to get section, error %#x.\n", GetLastError());
|
||||
ok(!strcasecmp(section, "section1"), "Got unexpected section %s.\n", section);
|
||||
ok(!extptr || !*extptr /* Windows 10 1809 */, "Got extension %s.\n", extptr);
|
||||
|
||||
extptr = section;
|
||||
ret = SetupDiGetActualSectionToInstallA(hinf, "section7", section, ARRAY_SIZE(section), NULL, &extptr);
|
||||
ok(ret, "Failed to get section, error %#x.\n", GetLastError());
|
||||
ok(!strcasecmp(section, "section7"), "Got unexpected section %s.\n", section);
|
||||
ok(!extptr || !*extptr /* Windows 10 1809 */, "Got extension %s.\n", extptr);
|
||||
|
||||
extptr = section;
|
||||
ret = SetupDiGetActualSectionToInstallA(hinf, "section8", section, ARRAY_SIZE(section), NULL, &extptr);
|
||||
ok(ret, "Failed to get section, error %#x.\n", GetLastError());
|
||||
ok(!strcasecmp(section, "section8"), "Got unexpected section %s.\n", section);
|
||||
ok(!extptr || !*extptr /* Windows 10 1809 */, "Got extension %s.\n", extptr);
|
||||
|
||||
extptr = section;
|
||||
ret = SetupDiGetActualSectionToInstallA(hinf, "nonexistent", section, ARRAY_SIZE(section), NULL, &extptr);
|
||||
ok(ret, "Failed to get section, error %#x.\n", GetLastError());
|
||||
ok(!strcasecmp(section, "nonexistent"), "Got unexpected section %s.\n", section);
|
||||
ok(!extptr || !*extptr /* Windows 10 1809 */, "Got extension %s.\n", extptr);
|
||||
|
||||
extptr = section;
|
||||
ret = SetupDiGetActualSectionToInstallA(hinf, "section10", section, ARRAY_SIZE(section), NULL, &extptr);
|
||||
ok(ret, "Failed to get section, error %#x.\n", GetLastError());
|
||||
|
@ -2827,11 +2940,9 @@ static void test_get_class_devs(void)
|
|||
set = SetupDiGetClassDevsA(NULL, "ROOT\\LEGACY_BOGUS", NULL, DIGCF_ALLCLASSES);
|
||||
ok(set != INVALID_HANDLE_VALUE, "Failed to create device list, error %#x.\n", GetLastError());
|
||||
check_device_list(set, &GUID_NULL);
|
||||
todo_wine {
|
||||
check_device_info(set, 0, &guid2, "ROOT\\LEGACY_BOGUS\\BAR");
|
||||
check_device_info(set, 1, &guid, "ROOT\\LEGACY_BOGUS\\FOO");
|
||||
check_device_info(set, 2, &guid, "ROOT\\LEGACY_BOGUS\\QUX");
|
||||
}
|
||||
check_device_info(set, 3, NULL, NULL);
|
||||
check_device_iface(set, NULL, &iface_guid, 0, 0, NULL);
|
||||
ret = SetupDiDestroyDeviceInfoList(set);
|
||||
|
@ -2840,10 +2951,8 @@ todo_wine {
|
|||
set = SetupDiGetClassDevsA(&guid, "ROOT\\LEGACY_BOGUS", NULL, 0);
|
||||
ok(set != INVALID_HANDLE_VALUE, "Failed to create device list, error %#x.\n", GetLastError());
|
||||
check_device_list(set, &guid);
|
||||
todo_wine {
|
||||
check_device_info(set, 0, &guid, "ROOT\\LEGACY_BOGUS\\FOO");
|
||||
check_device_info(set, 1, &guid, "ROOT\\LEGACY_BOGUS\\QUX");
|
||||
}
|
||||
check_device_info(set, 2, NULL, NULL);
|
||||
check_device_iface(set, NULL, &iface_guid, 0, 0, NULL);
|
||||
ret = SetupDiDestroyDeviceInfoList(set);
|
||||
|
@ -2852,11 +2961,9 @@ todo_wine {
|
|||
set = SetupDiGetClassDevsA(&guid, "ROOT\\LEGACY_BOGUS", NULL, DIGCF_ALLCLASSES);
|
||||
ok(set != INVALID_HANDLE_VALUE, "Failed to create device list, error %#x.\n", GetLastError());
|
||||
check_device_list(set, &GUID_NULL);
|
||||
todo_wine {
|
||||
check_device_info(set, 0, &guid2, "ROOT\\LEGACY_BOGUS\\BAR");
|
||||
check_device_info(set, 1, &guid, "ROOT\\LEGACY_BOGUS\\FOO");
|
||||
check_device_info(set, 2, &guid, "ROOT\\LEGACY_BOGUS\\QUX");
|
||||
}
|
||||
check_device_info(set, 3, NULL, NULL);
|
||||
check_device_iface(set, NULL, &iface_guid, 0, 0, NULL);
|
||||
ret = SetupDiDestroyDeviceInfoList(set);
|
||||
|
|
|
@ -29,8 +29,6 @@
|
|||
|
||||
#include "wine/test.h"
|
||||
|
||||
static BOOL is_win9x;
|
||||
|
||||
static void test_SetupCreateDiskSpaceListA(void)
|
||||
{
|
||||
HDSKSPC ret;
|
||||
|
@ -152,34 +150,29 @@ static void test_SetupDuplicateDiskSpaceListA(void)
|
|||
{
|
||||
HDSKSPC handle, duplicate;
|
||||
|
||||
if (is_win9x)
|
||||
win_skip("SetupDuplicateDiskSpaceListA crashes with NULL disk space handle on Win9x\n");
|
||||
else
|
||||
{
|
||||
SetLastError(0xdeadbeef);
|
||||
duplicate = SetupDuplicateDiskSpaceListA(NULL, NULL, 0, 0);
|
||||
ok(!duplicate, "Expected SetupDuplicateDiskSpaceList to return NULL, got %p\n", duplicate);
|
||||
ok(GetLastError() == ERROR_INVALID_HANDLE,
|
||||
"Expected GetLastError() to return ERROR_INVALID_HANDLE, got %u\n", GetLastError());
|
||||
SetLastError(0xdeadbeef);
|
||||
duplicate = SetupDuplicateDiskSpaceListA(NULL, NULL, 0, 0);
|
||||
ok(!duplicate, "Expected SetupDuplicateDiskSpaceList to return NULL, got %p\n", duplicate);
|
||||
ok(GetLastError() == ERROR_INVALID_HANDLE,
|
||||
"Expected GetLastError() to return ERROR_INVALID_HANDLE, got %u\n", GetLastError());
|
||||
|
||||
SetLastError(0xdeadbeef);
|
||||
duplicate = SetupDuplicateDiskSpaceListA(NULL, (void *)0xdeadbeef, 0, 0);
|
||||
ok(!duplicate, "Expected SetupDuplicateDiskSpaceList to return NULL, got %p\n", duplicate);
|
||||
ok(GetLastError() == ERROR_INVALID_PARAMETER,
|
||||
"Expected GetLastError() to return ERROR_INVALID_PARAMETER, got %u\n", GetLastError());
|
||||
SetLastError(0xdeadbeef);
|
||||
duplicate = SetupDuplicateDiskSpaceListA(NULL, (void *)0xdeadbeef, 0, 0);
|
||||
ok(!duplicate, "Expected SetupDuplicateDiskSpaceList to return NULL, got %p\n", duplicate);
|
||||
ok(GetLastError() == ERROR_INVALID_PARAMETER,
|
||||
"Expected GetLastError() to return ERROR_INVALID_PARAMETER, got %u\n", GetLastError());
|
||||
|
||||
SetLastError(0xdeadbeef);
|
||||
duplicate = SetupDuplicateDiskSpaceListA(NULL, NULL, 0xdeadbeef, 0);
|
||||
ok(!duplicate, "Expected SetupDuplicateDiskSpaceList to return NULL, got %p\n", duplicate);
|
||||
ok(GetLastError() == ERROR_INVALID_PARAMETER,
|
||||
"Expected GetLastError() to return ERROR_INVALID_PARAMETER, got %u\n", GetLastError());
|
||||
SetLastError(0xdeadbeef);
|
||||
duplicate = SetupDuplicateDiskSpaceListA(NULL, NULL, 0xdeadbeef, 0);
|
||||
ok(!duplicate, "Expected SetupDuplicateDiskSpaceList to return NULL, got %p\n", duplicate);
|
||||
ok(GetLastError() == ERROR_INVALID_PARAMETER,
|
||||
"Expected GetLastError() to return ERROR_INVALID_PARAMETER, got %u\n", GetLastError());
|
||||
|
||||
SetLastError(0xdeadbeef);
|
||||
duplicate = SetupDuplicateDiskSpaceListA(NULL, NULL, 0, ~0U);
|
||||
ok(!duplicate, "Expected SetupDuplicateDiskSpaceList to return NULL, got %p\n", duplicate);
|
||||
ok(GetLastError() == ERROR_INVALID_PARAMETER,
|
||||
"Expected GetLastError() to return ERROR_INVALID_PARAMETER, got %u\n", GetLastError());
|
||||
}
|
||||
SetLastError(0xdeadbeef);
|
||||
duplicate = SetupDuplicateDiskSpaceListA(NULL, NULL, 0, ~0U);
|
||||
ok(!duplicate, "Expected SetupDuplicateDiskSpaceList to return NULL, got %p\n", duplicate);
|
||||
ok(GetLastError() == ERROR_INVALID_PARAMETER,
|
||||
"Expected GetLastError() to return ERROR_INVALID_PARAMETER, got %u\n", GetLastError());
|
||||
|
||||
handle = SetupCreateDiskSpaceListA(NULL, 0, 0);
|
||||
ok(handle != NULL,
|
||||
|
@ -306,42 +299,37 @@ static void test_SetupQuerySpaceRequiredOnDriveA(void)
|
|||
HDSKSPC handle;
|
||||
LONGLONG space;
|
||||
|
||||
if (is_win9x)
|
||||
win_skip("SetupQuerySpaceRequiredOnDriveA crashes with NULL disk space handle on Win9x\n");
|
||||
else
|
||||
{
|
||||
SetLastError(0xdeadbeef);
|
||||
ret = SetupQuerySpaceRequiredOnDriveA(NULL, NULL, NULL, NULL, 0);
|
||||
ok(!ret, "Expected SetupQuerySpaceRequiredOnDriveA to return FALSE, got %d\n", ret);
|
||||
ok(GetLastError() == ERROR_INVALID_PARAMETER,
|
||||
"Expected GetLastError() to return ERROR_INVALID_PARAMETER, got %u\n",
|
||||
GetLastError());
|
||||
SetLastError(0xdeadbeef);
|
||||
ret = SetupQuerySpaceRequiredOnDriveA(NULL, NULL, NULL, NULL, 0);
|
||||
ok(!ret, "Expected SetupQuerySpaceRequiredOnDriveA to return FALSE, got %d\n", ret);
|
||||
ok(GetLastError() == ERROR_INVALID_PARAMETER,
|
||||
"Expected GetLastError() to return ERROR_INVALID_PARAMETER, got %u\n",
|
||||
GetLastError());
|
||||
|
||||
SetLastError(0xdeadbeef);
|
||||
space = 0xdeadbeef;
|
||||
ret = SetupQuerySpaceRequiredOnDriveA(NULL, NULL, &space, NULL, 0);
|
||||
ok(!ret, "Expected SetupQuerySpaceRequiredOnDriveA to return FALSE, got %d\n", ret);
|
||||
ok(space == 0xdeadbeef, "Expected output space parameter to be untouched\n");
|
||||
ok(GetLastError() == ERROR_INVALID_PARAMETER,
|
||||
"Expected GetLastError() to return ERROR_INVALID_PARAMETER, got %u\n",
|
||||
GetLastError());
|
||||
SetLastError(0xdeadbeef);
|
||||
space = 0xdeadbeef;
|
||||
ret = SetupQuerySpaceRequiredOnDriveA(NULL, NULL, &space, NULL, 0);
|
||||
ok(!ret, "Expected SetupQuerySpaceRequiredOnDriveA to return FALSE, got %d\n", ret);
|
||||
ok(space == 0xdeadbeef, "Expected output space parameter to be untouched\n");
|
||||
ok(GetLastError() == ERROR_INVALID_PARAMETER,
|
||||
"Expected GetLastError() to return ERROR_INVALID_PARAMETER, got %u\n",
|
||||
GetLastError());
|
||||
|
||||
SetLastError(0xdeadbeef);
|
||||
ret = SetupQuerySpaceRequiredOnDriveA(NULL, "", NULL, NULL, 0);
|
||||
ok(!ret, "Expected SetupQuerySpaceRequiredOnDriveA to return FALSE, got %d\n", ret);
|
||||
ok(GetLastError() == ERROR_INVALID_HANDLE,
|
||||
"Expected GetLastError() to return ERROR_INVALID_HANDLE, got %u\n",
|
||||
GetLastError());
|
||||
SetLastError(0xdeadbeef);
|
||||
ret = SetupQuerySpaceRequiredOnDriveA(NULL, "", NULL, NULL, 0);
|
||||
ok(!ret, "Expected SetupQuerySpaceRequiredOnDriveA to return FALSE, got %d\n", ret);
|
||||
ok(GetLastError() == ERROR_INVALID_HANDLE,
|
||||
"Expected GetLastError() to return ERROR_INVALID_HANDLE, got %u\n",
|
||||
GetLastError());
|
||||
|
||||
SetLastError(0xdeadbeef);
|
||||
space = 0xdeadbeef;
|
||||
ret = SetupQuerySpaceRequiredOnDriveA(NULL, "", &space, NULL, 0);
|
||||
ok(!ret, "Expected SetupQuerySpaceRequiredOnDriveA to return FALSE, got %d\n", ret);
|
||||
ok(space == 0xdeadbeef, "Expected output space parameter to be untouched\n");
|
||||
ok(GetLastError() == ERROR_INVALID_HANDLE,
|
||||
"Expected GetLastError() to return ERROR_INVALID_HANDLE, got %u\n",
|
||||
GetLastError());
|
||||
}
|
||||
SetLastError(0xdeadbeef);
|
||||
space = 0xdeadbeef;
|
||||
ret = SetupQuerySpaceRequiredOnDriveA(NULL, "", &space, NULL, 0);
|
||||
ok(!ret, "Expected SetupQuerySpaceRequiredOnDriveA to return FALSE, got %d\n", ret);
|
||||
ok(space == 0xdeadbeef, "Expected output space parameter to be untouched\n");
|
||||
ok(GetLastError() == ERROR_INVALID_HANDLE,
|
||||
"Expected GetLastError() to return ERROR_INVALID_HANDLE, got %u\n",
|
||||
GetLastError());
|
||||
|
||||
handle = SetupCreateDiskSpaceListA(NULL, 0, 0);
|
||||
ok(handle != NULL,
|
||||
|
@ -474,8 +462,6 @@ static void test_SetupQuerySpaceRequiredOnDriveW(void)
|
|||
|
||||
START_TEST(diskspace)
|
||||
{
|
||||
is_win9x = !SetupCreateDiskSpaceListW((void *)0xdeadbeef, 0xdeadbeef, 0) &&
|
||||
GetLastError() == ERROR_CALL_NOT_IMPLEMENTED;
|
||||
test_SetupCreateDiskSpaceListA();
|
||||
test_SetupCreateDiskSpaceListW();
|
||||
test_SetupDuplicateDiskSpaceListA();
|
||||
|
|
|
@ -1903,6 +1903,7 @@ static void test_need_media(void)
|
|||
queue = SetupOpenFileQueue();
|
||||
ok(queue != INVALID_HANDLE_VALUE, "Failed to open queue, error %#x.\n", GetLastError());
|
||||
copy_params.LayoutInf = hinf;
|
||||
copy_params.QueueHandle = queue;
|
||||
/* In fact this fails with ERROR_INVALID_PARAMETER on 8+. */
|
||||
if (SetupQueueCopyIndirectA(©_params))
|
||||
{
|
||||
|
@ -1944,6 +1945,101 @@ static void test_close_queue(void)
|
|||
SetupTermDefaultQueueCallback(context);
|
||||
}
|
||||
|
||||
static unsigned int got_start_copy, start_copy_ret;
|
||||
|
||||
static UINT WINAPI start_copy_cb(void *context, UINT message, UINT_PTR param1, UINT_PTR param2)
|
||||
{
|
||||
if (winetest_debug > 1) trace("%p, %#x, %#lx, %#lx\n", context, message, param1, param2);
|
||||
|
||||
if (message == SPFILENOTIFY_STARTCOPY)
|
||||
{
|
||||
const FILEPATHS_A *paths = (const FILEPATHS_A *)param1;
|
||||
|
||||
++got_start_copy;
|
||||
|
||||
if (strstr(paths->Source, "two.txt"))
|
||||
{
|
||||
SetLastError(0xdeadf00d);
|
||||
return start_copy_ret;
|
||||
}
|
||||
}
|
||||
|
||||
return SetupDefaultQueueCallbackA(context, message, param1, param2);
|
||||
}
|
||||
|
||||
static void test_start_copy(void)
|
||||
{
|
||||
HSPFILEQ queue;
|
||||
void *context;
|
||||
BOOL ret;
|
||||
|
||||
ret = CreateDirectoryA("src", NULL);
|
||||
ok(ret, "Failed to create test directory, error %u.\n", GetLastError());
|
||||
create_file("src/one.txt");
|
||||
create_file("src/two.txt");
|
||||
create_file("src/three.txt");
|
||||
|
||||
start_copy_ret = FILEOP_DOIT;
|
||||
got_start_copy = 0;
|
||||
queue = SetupOpenFileQueue();
|
||||
ok(queue != INVALID_HANDLE_VALUE, "Failed to open queue, error %#x.\n", GetLastError());
|
||||
ret = SetupQueueCopyA(queue, "src", NULL, "one.txt", NULL, NULL, "dst", NULL, 0);
|
||||
ok(ret, "Failed to queue copy, error %#x.\n", GetLastError());
|
||||
ret = SetupQueueCopyA(queue, "src", NULL, "two.txt", NULL, NULL, "dst", NULL, 0);
|
||||
ok(ret, "Failed to queue copy, error %#x.\n", GetLastError());
|
||||
ret = SetupQueueCopyA(queue, "src", NULL, "three.txt", NULL, NULL, "dst", NULL, 0);
|
||||
ok(ret, "Failed to queue copy, error %#x.\n", GetLastError());
|
||||
run_queue(queue, start_copy_cb);
|
||||
ok(got_start_copy == 3, "Got %u callbacks.\n", got_start_copy);
|
||||
ok(delete_file("dst/one.txt"), "Destination file should exist.\n");
|
||||
ok(delete_file("dst/two.txt"), "Destination file should exist.\n");
|
||||
ok(delete_file("dst/three.txt"), "Destination file should exist.\n");
|
||||
|
||||
start_copy_ret = FILEOP_SKIP;
|
||||
got_start_copy = 0;
|
||||
queue = SetupOpenFileQueue();
|
||||
ok(queue != INVALID_HANDLE_VALUE, "Failed to open queue, error %#x.\n", GetLastError());
|
||||
ret = SetupQueueCopyA(queue, "src", NULL, "one.txt", NULL, NULL, "dst", NULL, 0);
|
||||
ok(ret, "Failed to queue copy, error %#x.\n", GetLastError());
|
||||
ret = SetupQueueCopyA(queue, "src", NULL, "two.txt", NULL, NULL, "dst", NULL, 0);
|
||||
ok(ret, "Failed to queue copy, error %#x.\n", GetLastError());
|
||||
ret = SetupQueueCopyA(queue, "src", NULL, "three.txt", NULL, NULL, "dst", NULL, 0);
|
||||
ok(ret, "Failed to queue copy, error %#x.\n", GetLastError());
|
||||
run_queue(queue, start_copy_cb);
|
||||
ok(got_start_copy == 3, "Got %u callbacks.\n", got_start_copy);
|
||||
ok(delete_file("dst/one.txt"), "Destination file should exist.\n");
|
||||
ok(!file_exists("dst/two.txt"), "Destination file should not exist.\n");
|
||||
ok(delete_file("dst/three.txt"), "Destination file should exist.\n");
|
||||
|
||||
start_copy_ret = FILEOP_ABORT;
|
||||
got_start_copy = 0;
|
||||
queue = SetupOpenFileQueue();
|
||||
ok(queue != INVALID_HANDLE_VALUE, "Failed to open queue, error %#x.\n", GetLastError());
|
||||
ret = SetupQueueCopyA(queue, "src", NULL, "one.txt", NULL, NULL, "dst", NULL, 0);
|
||||
ok(ret, "Failed to queue copy, error %#x.\n", GetLastError());
|
||||
ret = SetupQueueCopyA(queue, "src", NULL, "two.txt", NULL, NULL, "dst", NULL, 0);
|
||||
ok(ret, "Failed to queue copy, error %#x.\n", GetLastError());
|
||||
ret = SetupQueueCopyA(queue, "src", NULL, "three.txt", NULL, NULL, "dst", NULL, 0);
|
||||
ok(ret, "Failed to queue copy, error %#x.\n", GetLastError());
|
||||
context = SetupInitDefaultQueueCallbackEx(NULL, INVALID_HANDLE_VALUE, 0, 0, 0);
|
||||
SetLastError(0xdeadbeef);
|
||||
ret = SetupCommitFileQueueA(NULL, queue, start_copy_cb, context);
|
||||
ok(!ret, "Expected failure.\n");
|
||||
ok(GetLastError() == 0xdeadf00d, "Got unexpected error %u.\n", GetLastError());
|
||||
SetupTermDefaultQueueCallback(context);
|
||||
SetupCloseFileQueue(queue);
|
||||
ok(got_start_copy == 2, "Got %u callbacks.\n", got_start_copy);
|
||||
ok(delete_file("dst/one.txt"), "Destination file should exist.\n");
|
||||
ok(!file_exists("dst/two.txt"), "Destination file should not exist.\n");
|
||||
ok(!file_exists("dst/three.txt"), "Destination file should not exist.\n");
|
||||
|
||||
delete_file("src/one.txt");
|
||||
delete_file("src/two.txt");
|
||||
delete_file("src/three.txt");
|
||||
delete_file("src/");
|
||||
delete_file("dst/");
|
||||
}
|
||||
|
||||
START_TEST(install)
|
||||
{
|
||||
char temp_path[MAX_PATH], prev_path[MAX_PATH];
|
||||
|
@ -1972,6 +2068,7 @@ START_TEST(install)
|
|||
test_need_media();
|
||||
test_close_queue();
|
||||
test_install_file();
|
||||
test_start_copy();
|
||||
|
||||
UnhookWindowsHookEx(hhook);
|
||||
|
||||
|
|
|
@ -312,7 +312,7 @@ static void test_SetupCopyOEMInf(void)
|
|||
ok(res == TRUE, "Expected TRUE, got %d\n", res);
|
||||
ok(GetLastError() == ERROR_SUCCESS, "Expected ERROR_SUCCESS, got %d\n", GetLastError());
|
||||
ok(is_in_inf_dir(dest), "Got unexpected path '%s'.\n", dest);
|
||||
ok(strcmp(dest, orig_dest), "Expected INF files to be copied to differnet paths.\n");
|
||||
ok(strcmp(dest, orig_dest), "Expected INF files to be copied to different paths.\n");
|
||||
|
||||
res = SetupUninstallOEMInfA(strrchr(dest, '\\') + 1, 0, NULL);
|
||||
ok(res, "Failed to uninstall '%s', error %u.\n", dest, GetLastError());
|
||||
|
|
|
@ -10,4 +10,4 @@ files:
|
|||
dlls/setupapi/setupcab.c: dll/win32/setupapi/setupcab.c
|
||||
dlls/setupapi/stringtable.c: dll/win32/setupapi/stringtable_wine.c
|
||||
tags:
|
||||
wine: 72d6759f3a86c24e2db0b6190f4d2fae642e487f
|
||||
wine: f1b94dc16c35a5c477b6df8aa94c6d3537642c77
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue