[PATCH v5 1/6] robocopy: Add stub
Basic files and required changes to configure(.ac) to create a stub version of the robocopy utility Signed-off-by: Florian Eder <others.meder(a)gmail.com> --- v5: Stub returns non-zero exit code and no more "Wine-Bug:" line --- configure | 2 +- configure.ac | 1 + programs/robocopy/Makefile.in | 8 ++++++++ programs/robocopy/main.c | 29 +++++++++++++++++++++++++++++ programs/robocopy/robocopy.rc | 34 ++++++++++++++++++++++++++++++++++ 5 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 programs/robocopy/Makefile.in create mode 100644 programs/robocopy/main.c create mode 100644 programs/robocopy/robocopy.rc diff --git a/configure b/configure index 00a5be6a504..a5767af3c60 100755 --- a/configure +++ b/configure @@ -21234,6 +21234,7 @@ wine_fn_config_makefile programs/regedit/tests enable_tests wine_fn_config_makefile programs/regini enable_regini wine_fn_config_makefile programs/regsvcs enable_regsvcs wine_fn_config_makefile programs/regsvr32 enable_regsvr32 +wine_fn_config_makefile programs/robocopy enable_robocopy wine_fn_config_makefile programs/rpcss enable_rpcss wine_fn_config_makefile programs/rundll.exe16 enable_win16 wine_fn_config_makefile programs/rundll32 enable_rundll32 @@ -22767,4 +22768,3 @@ IFS="$ac_save_IFS" $as_echo " $as_me: Finished. Do '${ac_make}' to compile Wine. " >&6 - diff --git a/configure.ac b/configure.ac index dea5fe61bf3..97c46565db3 100644 --- a/configure.ac +++ b/configure.ac @@ -3958,6 +3958,7 @@ WINE_CONFIG_MAKEFILE(programs/regedit/tests) WINE_CONFIG_MAKEFILE(programs/regini) WINE_CONFIG_MAKEFILE(programs/regsvcs) WINE_CONFIG_MAKEFILE(programs/regsvr32) +WINE_CONFIG_MAKEFILE(programs/robocopy) WINE_CONFIG_MAKEFILE(programs/rpcss) WINE_CONFIG_MAKEFILE(programs/rundll.exe16,enable_win16) WINE_CONFIG_MAKEFILE(programs/rundll32) diff --git a/programs/robocopy/Makefile.in b/programs/robocopy/Makefile.in new file mode 100644 index 00000000000..5a48be3725b --- /dev/null +++ b/programs/robocopy/Makefile.in @@ -0,0 +1,8 @@ +MODULE = robocopy.exe + +EXTRADLLFLAGS = -mconsole -municode -mno-cygwin + +C_SRCS = \ + main.c + +RC_SRCS = robocopy.rc diff --git a/programs/robocopy/main.c b/programs/robocopy/main.c new file mode 100644 index 00000000000..bbda15f573a --- /dev/null +++ b/programs/robocopy/main.c @@ -0,0 +1,29 @@ +/* + * Copyright 2021 Florian Eder + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include "wine/debug.h" +WINE_DEFAULT_DEBUG_CHANNEL(robocopy); + +#define WIN32_LEAN_AND_MEAN +#include <windows.h> + +int __cdecl wmain(int argc, WCHAR *argv[]) +{ + WINE_FIXME("robocopy stub"); + return 1; +} diff --git a/programs/robocopy/robocopy.rc b/programs/robocopy/robocopy.rc new file mode 100644 index 00000000000..edae5b71d86 --- /dev/null +++ b/programs/robocopy/robocopy.rc @@ -0,0 +1,34 @@ +/* + * Copyright 2021 Florian Eder + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include <windef.h> + +#pragma makedep po + +LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL + +#define WINE_FILEDESCRIPTION_STR "Wine Robocopy" +#define WINE_FILENAME_STR "robocopy.exe" +#define WINE_FILETYPE VFT_APP +#define WINE_FILEVERSION 5,1,10,1027 +#define WINE_FILEVERSION_STR "5.1.10.1027" + +#define WINE_PRODUCTVERSION 5,1,10,1027 +#define WINE_PRODUCTVERSION_STR "XP027" + +#include "wine/wine_common_ver.rc" -- 2.32.0
Basic files and required changes to configure(.ac) to create basic syntax tests and a scaffolding for the conformance tests to follow for the robocopy utility Signed-off-by: Florian Eder <others.meder(a)gmail.com> --- v5: Added *very* basic syntax tests, as the unit tests following the implementation of each switch / flag will automatically test the correct syntax also Made execute_robocopy only accept args instead of the whole commandline, dynamically allocate the commandline string, changed the return value of execute_robocopy from -1 to explicitly MAXDWORD and reduced the size of temp_path to MAX_PATH, which is the max size that GetTempPathW can return --- configure | 1 + configure.ac | 1 + programs/robocopy/tests/Makefile.in | 4 ++ programs/robocopy/tests/robocopy.c | 87 +++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+) create mode 100644 programs/robocopy/tests/Makefile.in create mode 100644 programs/robocopy/tests/robocopy.c diff --git a/configure b/configure index a5767af3c60..d63321522f4 100755 --- a/configure +++ b/configure @@ -21235,6 +21235,7 @@ wine_fn_config_makefile programs/regini enable_regini wine_fn_config_makefile programs/regsvcs enable_regsvcs wine_fn_config_makefile programs/regsvr32 enable_regsvr32 wine_fn_config_makefile programs/robocopy enable_robocopy +wine_fn_config_makefile programs/robocopy/tests enable_tests wine_fn_config_makefile programs/rpcss enable_rpcss wine_fn_config_makefile programs/rundll.exe16 enable_win16 wine_fn_config_makefile programs/rundll32 enable_rundll32 diff --git a/configure.ac b/configure.ac index 97c46565db3..6d89ceea089 100644 --- a/configure.ac +++ b/configure.ac @@ -3959,6 +3959,7 @@ WINE_CONFIG_MAKEFILE(programs/regini) WINE_CONFIG_MAKEFILE(programs/regsvcs) WINE_CONFIG_MAKEFILE(programs/regsvr32) WINE_CONFIG_MAKEFILE(programs/robocopy) +WINE_CONFIG_MAKEFILE(programs/robocopy/tests) WINE_CONFIG_MAKEFILE(programs/rpcss) WINE_CONFIG_MAKEFILE(programs/rundll.exe16,enable_win16) WINE_CONFIG_MAKEFILE(programs/rundll32) diff --git a/programs/robocopy/tests/Makefile.in b/programs/robocopy/tests/Makefile.in new file mode 100644 index 00000000000..d86e70f4c43 --- /dev/null +++ b/programs/robocopy/tests/Makefile.in @@ -0,0 +1,4 @@ +TESTDLL = robocopy.exe + +C_SRCS = \ + robocopy.c diff --git a/programs/robocopy/tests/robocopy.c b/programs/robocopy/tests/robocopy.c new file mode 100644 index 00000000000..8c594b78d92 --- /dev/null +++ b/programs/robocopy/tests/robocopy.c @@ -0,0 +1,87 @@ +/* + * Copyright 2021 Florian Eder + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#define WIN32_LEAN_AND_MEAN +#include <windows.h> +#include <wchar.h> +#include <wine/test.h> + +static DWORD execute_robocopy(const WCHAR *args) +{ + STARTUPINFOW startup_info; + PROCESS_INFORMATION process_info; + DWORD exit_code; + WCHAR *cmd; + + memset(&startup_info, 0, sizeof(STARTUPINFOW)); + startup_info.dwFlags = STARTF_USESTDHANDLES; + startup_info.hStdInput = INVALID_HANDLE_VALUE; + startup_info.hStdOutput = INVALID_HANDLE_VALUE; + startup_info.hStdError = INVALID_HANDLE_VALUE; + + cmd = calloc(ARRAY_SIZE(L"robocopy.exe ") + wcslen(args), sizeof(WCHAR)); + wcscpy(cmd, L"robocopy.exe "); + wcscat(cmd, args); + + if (!CreateProcessW(NULL, cmd, NULL, NULL, TRUE, 0, NULL, NULL, &startup_info, &process_info)) + return MAXDWORD; + + if (WaitForSingleObject(process_info.hProcess, 30000) == WAIT_TIMEOUT) + return MAXDWORD; + + GetExitCodeProcess(process_info.hProcess, &exit_code); + + CloseHandle(process_info.hThread); + CloseHandle(process_info.hProcess); + + return exit_code; +} + +START_TEST(robocopy) +{ + DWORD exit_code; + WCHAR temp_path[MAX_PATH]; + + /* robocopy is only available from Vista onwards, abort test if not available */ + if (execute_robocopy(L"") == MAXDWORD) return; + + ok(GetTempPathW(ARRAY_SIZE(temp_path), temp_path) != 0, "couldn't get temp folder path"); + + ok(SetCurrentDirectoryW(temp_path), "couldn't set CWD to temp folder \"%s\"", debugstr_w(temp_path)); + + winetest_push_context("syntax test 1"); + RemoveDirectoryW(L"invalid_folder"); + exit_code = execute_robocopy(L""); + todo_wine ok(exit_code == 16, "unexpected exit code %d\n", exit_code); + winetest_pop_context(); + + winetest_push_context("syntax test 2"); + exit_code = execute_robocopy(L"invalid_folder"); + todo_wine ok(exit_code == 16, "unexpected exit code %d\n", exit_code); + winetest_pop_context(); + + winetest_push_context("syntax test 3"); + exit_code = execute_robocopy(L"-flag invalid_folder"); + todo_wine ok(exit_code == 16, "unexpected exit code %d\n", exit_code); + winetest_pop_context(); + + winetest_push_context("syntax test 4"); + exit_code = execute_robocopy(L"invalid_folder robocopy_destination"); + todo_wine ok(exit_code == 16, "unexpected exit code %d\n", exit_code); + winetest_pop_context(); +} -- 2.32.0
Hi Florian, On Tue, 21 Sept 2021 at 04:52, Florian Eder wrote: [snip]
+ +static DWORD execute_robocopy(const WCHAR *args) +{ + STARTUPINFOW startup_info; + PROCESS_INFORMATION process_info; + DWORD exit_code; + WCHAR *cmd; + + memset(&startup_info, 0, sizeof(STARTUPINFOW)); + startup_info.dwFlags = STARTF_USESTDHANDLES; + startup_info.hStdInput = INVALID_HANDLE_VALUE; + startup_info.hStdOutput = INVALID_HANDLE_VALUE; + startup_info.hStdError = INVALID_HANDLE_VALUE; + + cmd = calloc(ARRAY_SIZE(L"robocopy.exe ") + wcslen(args), sizeof(WCHAR)); + wcscpy(cmd, L"robocopy.exe "); + wcscat(cmd, args);
Dynamically allocating memory is a little unnecessary, although I do like your thinking. Why don't you start with an array of 256 WCHARs? You can always increase the size if needed later. I quite like copying "robocopy.exe", so you could keep that. Alternatively, rename the function to something like run_test, run_exe or run_rc and put "robocopy.exe" back in the cmdline you pass in.
+ if (!CreateProcessW(NULL, cmd, NULL, NULL, TRUE, 0, NULL, NULL, &startup_info, &process_info)) + return MAXDWORD; + + if (WaitForSingleObject(process_info.hProcess, 30000) == WAIT_TIMEOUT) + return MAXDWORD; + + GetExitCodeProcess(process_info.hProcess, &exit_code); + + CloseHandle(process_info.hThread); + CloseHandle(process_info.hProcess); + + return exit_code; +} + +START_TEST(robocopy) +{ + DWORD exit_code; + WCHAR temp_path[MAX_PATH]; + + /* robocopy is only available from Vista onwards, abort test if not available */ + if (execute_robocopy(L"") == MAXDWORD) return;
This still seems overly cumbersome. I strongly suggest execute_robocopy returns BOOL, and takes a pointer to a DWORD argument for the exit code.
+ ok(GetTempPathW(ARRAY_SIZE(temp_path), temp_path) != 0, "couldn't get temp folder path"); + + ok(SetCurrentDirectoryW(temp_path), "couldn't set CWD to temp folder \"%s\"", debugstr_w(temp_path)); + + winetest_push_context("syntax test 1");
Out of interest, do you need a context for every test? Or do you plan to group tests?
+ RemoveDirectoryW(L"invalid_folder");
Why RemoveDirectoryW()? What's that doing here?
+ exit_code = execute_robocopy(L""); + todo_wine ok(exit_code == 16, "unexpected exit code %d\n", exit_code);
This goes back to my earlier point. IMHO, I find assigning to exit_code makes this harder to read than necessary.
+ winetest_pop_context();
Can you add "robocopy.exe" (no flags), "robocopy.exe /?" and "robocopy.exe -?" as well?
+ winetest_push_context("syntax test 2"); + exit_code = execute_robocopy(L"invalid_folder"); + todo_wine ok(exit_code == 16, "unexpected exit code %d\n", exit_code); + winetest_pop_context(); + + winetest_push_context("syntax test 3"); + exit_code = execute_robocopy(L"-flag invalid_folder"); + todo_wine ok(exit_code == 16, "unexpected exit code %d\n", exit_code); + winetest_pop_context(); + + winetest_push_context("syntax test 4"); + exit_code = execute_robocopy(L"invalid_folder robocopy_destination"); + todo_wine ok(exit_code == 16, "unexpected exit code %d\n", exit_code); + winetest_pop_context(); +} -- 2.32.0
Parses (relative / absolute) path arguments as source, destination and files to include Signed-off-by: Florian Eder <others.meder(a)gmail.com> --- v5: Identical to patch sent in v4 --- programs/robocopy/Makefile.in | 1 + programs/robocopy/main.c | 61 +++++++++++++++++++++++++++++++++++ programs/robocopy/robocopy.h | 33 +++++++++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 programs/robocopy/robocopy.h diff --git a/programs/robocopy/Makefile.in b/programs/robocopy/Makefile.in index 5a48be3725b..0f4f5c76119 100644 --- a/programs/robocopy/Makefile.in +++ b/programs/robocopy/Makefile.in @@ -1,4 +1,5 @@ MODULE = robocopy.exe +IMPORTS = kernelbase EXTRADLLFLAGS = -mconsole -municode -mno-cygwin diff --git a/programs/robocopy/main.c b/programs/robocopy/main.c index bbda15f573a..e1800fbd74e 100644 --- a/programs/robocopy/main.c +++ b/programs/robocopy/main.c @@ -21,9 +21,70 @@ WINE_DEFAULT_DEBUG_CHANNEL(robocopy); #define WIN32_LEAN_AND_MEAN #include <windows.h> +#include <stdlib.h> +#include <pathcch.h> +#include "robocopy.h" + +struct robocopy_options options; + +static WCHAR *get_absolute_path(const WCHAR *path) +{ + DWORD size; + WCHAR *absolute_path; + + /* allocate absolute path + potential backslash + null WCHAR */ + size = GetFullPathNameW(path, 0, NULL, NULL) + 2; + absolute_path = calloc(size, sizeof(WCHAR)); + GetFullPathNameW(path, size, absolute_path, NULL); + PathCchAddBackslashEx(absolute_path, size, NULL, NULL); + return absolute_path; +} + +static void parse_arguments(int argc, WCHAR *argv[]) +{ + int i; + + memset(&options, 0, sizeof(options)); + options.files = calloc(1, offsetof(struct path_array, array[argc])); + + for (i = 1; i < argc; i++) + { + /* + * Robocopy switches contain one (and only one) backslash at the start + * /xf => valid flag + * /r:1 => valid flag + * /r:1aö => valid flag + * /r:1aö/ => not a valid flag, is interpreted as a filename + */ + if ((argv[i][0] == L'/') && (wcschr(argv[i] + 1, L'/') == NULL)) + WINE_FIXME("encountered an unknown robocopy flag: %s\n", debugstr_w(argv[i])); + else + { + /* + *(Probably) not a flag, we can parse it as the source / the destination / a filename + * Priority: Source > Destination > (more than one) File + */ + if (!options.source) + { + options.source = get_absolute_path(argv[i]); + } + else if (!options.destination) + { + options.destination = get_absolute_path(argv[i]); + } + else + { + options.files->array[options.files->size] = wcsdup(argv[i]); + options.files->size++; + } + } + } +} int __cdecl wmain(int argc, WCHAR *argv[]) { + parse_arguments(argc, argv); + WINE_FIXME("robocopy stub"); return 1; } diff --git a/programs/robocopy/robocopy.h b/programs/robocopy/robocopy.h new file mode 100644 index 00000000000..f62eb92c663 --- /dev/null +++ b/programs/robocopy/robocopy.h @@ -0,0 +1,33 @@ +/* + * Copyright 2021 Florian Eder + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#define WIN32_LEAN_AND_MEAN +#include <windows.h> + +struct path_array +{ + UINT size; + WCHAR *array[1]; +}; + +struct robocopy_options +{ + WCHAR *destination; + WCHAR *source; + struct path_array *files; +}; -- 2.32.0
Adds output method and prints a header and the source / destination / files to include back to the user Signed-off-by: Florian Eder <others.meder(a)gmail.com> --- v5: Identical to patch sent in v4 --- programs/robocopy/main.c | 63 +++++++++++++++++++++++++++++++++++ programs/robocopy/robocopy.h | 6 ++++ programs/robocopy/robocopy.rc | 11 ++++++ 3 files changed, 80 insertions(+) diff --git a/programs/robocopy/main.c b/programs/robocopy/main.c index e1800fbd74e..6781fc17504 100644 --- a/programs/robocopy/main.c +++ b/programs/robocopy/main.c @@ -27,6 +27,50 @@ WINE_DEFAULT_DEBUG_CHANNEL(robocopy); struct robocopy_options options; +static const WCHAR *format_string(UINT format_string_id) +{ + WCHAR format_string[2048]; + if (!LoadStringW(GetModuleHandleW(NULL), format_string_id, format_string, ARRAY_SIZE(format_string))) + { + WINE_ERR("invalid string loaded"); + return L""; + } + return wcsdup(format_string); +} + +static void output_message(const WCHAR *format_string, ...) +{ + __ms_va_list va_args; + WCHAR *string; + DWORD length, bytes_written; + + __ms_va_start(va_args, format_string); + length = FormatMessageW(FORMAT_MESSAGE_FROM_STRING | FORMAT_MESSAGE_ALLOCATE_BUFFER, + format_string, 0, 0, (LPWSTR)&string, 0, &va_args); + __ms_va_end(va_args); + if (!length) + { + WINE_ERR("string formation failed"); + return; + } + + /* If WriteConsole fails, the output is being redirected to a file */ + if (!WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE), string, wcslen(string), &bytes_written, NULL)) + { + CHAR *string_multibyte; + DWORD length_multibyte; + + length_multibyte = WideCharToMultiByte(GetConsoleOutputCP(), 0, string, wcslen(string), NULL, 0, NULL, NULL); + string_multibyte = malloc(length_multibyte); + + WideCharToMultiByte(GetConsoleOutputCP(), 0, string, wcslen(string), string_multibyte, length_multibyte, NULL, NULL); + WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), string_multibyte, length_multibyte, &bytes_written, NULL); + free(string_multibyte); + } + + LocalFree(string); +} + static WCHAR *get_absolute_path(const WCHAR *path) { DWORD size; @@ -81,10 +125,29 @@ static void parse_arguments(int argc, WCHAR *argv[]) } } +static void print_header(void) +{ + UINT i; + + output_message(format_string(STRING_HEADER)); + + if (!options.source) output_message(format_string(STRING_SOURCE), L"-"); + else output_message(format_string(STRING_SOURCE), options.source); + + if (!options.destination) output_message(format_string(STRING_DESTINATION), L"-"); + else output_message(format_string(STRING_DESTINATION), options.destination); + + output_message(format_string(STRING_FILES)); + for (i = 0; i < options.files->size; i++) + output_message(L" %1\n", options.files->array[i]); +} + int __cdecl wmain(int argc, WCHAR *argv[]) { parse_arguments(argc, argv); + print_header(); + WINE_FIXME("robocopy stub"); return 1; } diff --git a/programs/robocopy/robocopy.h b/programs/robocopy/robocopy.h index f62eb92c663..f5c2ac56fcf 100644 --- a/programs/robocopy/robocopy.h +++ b/programs/robocopy/robocopy.h @@ -31,3 +31,9 @@ struct robocopy_options WCHAR *source; struct path_array *files; }; + +/* Resource strings */ +#define STRING_HEADER 1000 +#define STRING_SOURCE 1003 +#define STRING_DESTINATION 1004 +#define STRING_FILES 1005 diff --git a/programs/robocopy/robocopy.rc b/programs/robocopy/robocopy.rc index edae5b71d86..044bab77774 100644 --- a/programs/robocopy/robocopy.rc +++ b/programs/robocopy/robocopy.rc @@ -17,9 +17,20 @@ */ #include <windef.h> +#include "robocopy.h" #pragma makedep po +LANGUAGE LANG_ENGLISH, SUBLANG_DEFAULT + +STRINGTABLE +{ + STRING_HEADER, "Robocopy for Wine\n\n" + STRING_SOURCE, " Source: %1\n" + STRING_DESTINATION, " Destination: %1\n\n" + STRING_FILES, " Files:\n" +} + LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL #define WINE_FILEDESCRIPTION_STR "Wine Robocopy" -- 2.32.0
Reads all files in the source folder that match any of the files to include and copies them to the destination, creating necessary folders in the process Signed-off-by: Florian Eder <others.meder(a)gmail.com> --- v5: Remove wine_todo from return value tests in basic syntax tests, as these tests pass now --- programs/robocopy/Makefile.in | 2 +- programs/robocopy/main.c | 177 ++++++++++++++++++++++++++++- programs/robocopy/robocopy.h | 17 +++ programs/robocopy/robocopy.rc | 6 + programs/robocopy/tests/robocopy.c | 8 +- 5 files changed, 203 insertions(+), 7 deletions(-) diff --git a/programs/robocopy/Makefile.in b/programs/robocopy/Makefile.in index 0f4f5c76119..1c640599b4b 100644 --- a/programs/robocopy/Makefile.in +++ b/programs/robocopy/Makefile.in @@ -1,5 +1,5 @@ MODULE = robocopy.exe -IMPORTS = kernelbase +IMPORTS = kernelbase shlwapi EXTRADLLFLAGS = -mconsole -municode -mno-cygwin diff --git a/programs/robocopy/main.c b/programs/robocopy/main.c index 6781fc17504..05546715718 100644 --- a/programs/robocopy/main.c +++ b/programs/robocopy/main.c @@ -23,6 +23,8 @@ WINE_DEFAULT_DEBUG_CHANNEL(robocopy); #include <windows.h> #include <stdlib.h> #include <pathcch.h> +#include <shlwapi.h> +#include <wine/list.h> #include "robocopy.h" struct robocopy_options options; @@ -71,6 +73,21 @@ static void output_message(const WCHAR *format_string, ...) LocalFree(string); } +static void output_error(UINT format_string_id, HRESULT error_code, WCHAR* path) +{ + WCHAR *error_string, error_code_long[64], error_code_short[64]; + + FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_ALLOCATE_BUFFER, + NULL, error_code, 0, (LPWSTR)&error_string, 0, NULL); + + swprintf(error_code_long, ARRAY_SIZE(error_code_long), L"0x%08x", error_code); + swprintf(error_code_short, ARRAY_SIZE(error_code_short), L"%u", error_code); + + output_message(format_string(format_string_id), L"", error_code_short, error_code_long, path, error_string); + + LocalFree(error_string); +} + static WCHAR *get_absolute_path(const WCHAR *path) { DWORD size; @@ -125,6 +142,146 @@ static void parse_arguments(int argc, WCHAR *argv[]) } } +static BOOL path_in_array(WCHAR *name, struct path_array *names) +{ + int i; + for (i = 0; i < names->size; i++) + { + if (PathMatchSpecW(name, names->array[i])) return TRUE; + } + return FALSE; +} + +static BOOL create_directory_path(WCHAR *path) +{ + WCHAR *pointer, *current_folder; + current_folder = calloc(wcslen(path) + 1, sizeof(WCHAR)); + /* ignore the "\\?\" prefix, so that those backslashes are not matched */ + pointer = wcschr(path, L'\\'); + while (pointer != NULL) + { + if (!lstrcpynW(current_folder, path, pointer - path + 2)) return FALSE; + /* try to create the folder, ignoring any failure due to ERROR_ALREADY_EXISTS */ + if (!CreateDirectoryW(current_folder, NULL)) + { + if (GetLastError() != ERROR_ALREADY_EXISTS) + { + output_error(STRING_ERROR_WRITE_DIRECTORY, GetLastError(), current_folder); + return FALSE; + } + } + else + output_message(format_string(STRING_CREATE_DIRECTORY), current_folder); + pointer = wcschr(pointer + 1, L'\\'); + } + return TRUE; +} + +static void get_file_paths_in_folder(WCHAR *directory_path, struct list *paths) +{ + HANDLE temp_handle; + struct path *new_path, *current_path; + WIN32_FIND_DATAW entry_data; + WCHAR *parent_absolute_path, *current_relative_path, *current_absolute_path, *current_search_path; + + list_init(paths); + + /* initialize list with a empty relative path */ + new_path = calloc(1, sizeof(struct path)); + new_path->name = calloc(2, sizeof(WCHAR)); + list_add_tail(paths, &new_path->entry); + + LIST_FOR_EACH_ENTRY(current_path, paths, struct path, entry) + { + /* append relative path to the (prefix) directory path */ + PathAllocCombine(directory_path, current_path->name, + PATHCCH_ALLOW_LONG_PATHS | PATHCCH_FORCE_ENABLE_LONG_NAME_PROCESS, + &parent_absolute_path); + + /* append * to recieve every file / subdirectory in this directory */ + PathAllocCombine(parent_absolute_path, L"*", + PATHCCH_ALLOW_LONG_PATHS | PATHCCH_FORCE_ENABLE_LONG_NAME_PROCESS, + ¤t_search_path); + /* walk through all files / directories in this directory */ + temp_handle = FindFirstFileExW(current_search_path, FindExInfoStandard, &entry_data, FindExSearchNameMatch, NULL, 0); + if (temp_handle != INVALID_HANDLE_VALUE) + { + do + { + /* Ignore . and .. entries */ + if (!wcscmp(L".", entry_data.cFileName) || !wcscmp(L"..", entry_data.cFileName)) continue; + + PathAllocCombine(current_path->name, entry_data.cFileName, + PATHCCH_ALLOW_LONG_PATHS | PATHCCH_FORCE_ENABLE_LONG_NAME_PROCESS, + ¤t_relative_path); + PathAllocCombine(directory_path, current_relative_path, + PATHCCH_ALLOW_LONG_PATHS | PATHCCH_FORCE_ENABLE_LONG_NAME_PROCESS, + ¤t_absolute_path); + + /* If this entry is a matching file or empty directory, add it to the list of results */ + if ((!PathIsDirectoryW(current_absolute_path) && path_in_array(entry_data.cFileName, options.files)) || + (PathIsDirectoryW(current_absolute_path))) + { + new_path = calloc(1, sizeof(struct path)); + new_path->name = wcsdup(current_relative_path); + list_add_tail(paths, &new_path->entry); + } + } + while (FindNextFileW(temp_handle, &entry_data) != 0); + } + } +} + +static BOOL perform_copy(void) +{ + struct list paths_source; + struct path *current_path; + WCHAR *current_absolute_path, *target_path; + + list_init(&paths_source); + + if (!PathIsDirectoryW(options.source)) + { + output_error(STRING_ERROR_READ_DIRECTORY, ERROR_FILE_NOT_FOUND, options.source); + return FALSE; + } + + create_directory_path(options.destination); + + /* get files in the source folder */ + get_file_paths_in_folder(options.source, &paths_source); + + /* walk through files in the source folder */ + LIST_FOR_EACH_ENTRY(current_path, &paths_source, struct path, entry) + { + PathAllocCombine(options.source, current_path->name, + PATHCCH_ALLOW_LONG_PATHS | PATHCCH_FORCE_ENABLE_LONG_NAME_PROCESS, + ¤t_absolute_path); + + /* append the relative source path to the destination to get the target path */ + PathAllocCombine(options.destination, current_path->name, + PATHCCH_ALLOW_LONG_PATHS | PATHCCH_FORCE_ENABLE_LONG_NAME_PROCESS, + &target_path); + + if (PathIsDirectoryW(current_absolute_path)) + { + if (!create_directory_path(target_path)) + output_error(STRING_ERROR_WRITE_DIRECTORY, GetLastError(), target_path); + } + else + { + create_directory_path(target_path); + if (!CopyFileW(current_absolute_path, target_path, FALSE)) + output_error(STRING_ERROR_WRITE_FILE, GetLastError(), target_path); + else + { + output_message(format_string(STRING_CREATE_FILE), target_path); + } + } + } + return TRUE; +} + static void print_header(void) { UINT i; @@ -146,8 +303,24 @@ int __cdecl wmain(int argc, WCHAR *argv[]) { parse_arguments(argc, argv); + /* If no file filters are set, set *.* to include all files */ + if (options.files->size == 0) + { + options.files->array[options.files->size] = wcsdup(L"*.*"); + options.files->size++; + } + print_header(); - WINE_FIXME("robocopy stub"); - return 1; + /* Break if Source or Destination not set */ + if (!options.destination || !options.source) + { + output_message(format_string(STRING_MISSING_DESTINATION_OR_SOURCE)); + return ROBOCOPY_ERROR_NO_FILES_COPIED; + } + + if (!perform_copy()) + return ROBOCOPY_ERROR_NO_FILES_COPIED; + + return ROBOCOPY_NO_ERROR_FILES_COPIED; } diff --git a/programs/robocopy/robocopy.h b/programs/robocopy/robocopy.h index f5c2ac56fcf..86be775da01 100644 --- a/programs/robocopy/robocopy.h +++ b/programs/robocopy/robocopy.h @@ -18,6 +18,13 @@ #define WIN32_LEAN_AND_MEAN #include <windows.h> +#include <wine/list.h> + +struct path +{ + struct list entry; + WCHAR *name; +}; struct path_array { @@ -32,8 +39,18 @@ struct robocopy_options struct path_array *files; }; +/* Exit codes */ +#define ROBOCOPY_NO_ERROR_FILES_COPIED 1 +#define ROBOCOPY_ERROR_NO_FILES_COPIED 16 + /* Resource strings */ #define STRING_HEADER 1000 #define STRING_SOURCE 1003 #define STRING_DESTINATION 1004 #define STRING_FILES 1005 +#define STRING_MISSING_DESTINATION_OR_SOURCE 1010 +#define STRING_ERROR_READ_DIRECTORY 1011 +#define STRING_ERROR_WRITE_DIRECTORY 1012 +#define STRING_ERROR_WRITE_FILE 1014 +#define STRING_CREATE_DIRECTORY 1019 +#define STRING_CREATE_FILE 1022 diff --git a/programs/robocopy/robocopy.rc b/programs/robocopy/robocopy.rc index 044bab77774..1ce605f432d 100644 --- a/programs/robocopy/robocopy.rc +++ b/programs/robocopy/robocopy.rc @@ -29,6 +29,12 @@ STRINGTABLE STRING_SOURCE, " Source: %1\n" STRING_DESTINATION, " Destination: %1\n\n" STRING_FILES, " Files:\n" + STRING_MISSING_DESTINATION_OR_SOURCE, "No destination or source specified, can't copy anything\n" + STRING_ERROR_READ_DIRECTORY, "[%1] Error %2 (%3) occurred reading directory \"%4\":\n%5\n" + STRING_ERROR_WRITE_DIRECTORY, "[%1] Error %2 (%3) occurred writing directory \"%4\":\n%5\n" + STRING_ERROR_WRITE_FILE, "[%1] Error %2 (%3) occurred writing file \"%4\":\n%5\n" + STRING_CREATE_DIRECTORY, " Created Dir: %1\n" + STRING_CREATE_FILE, " Copied File: %1\n" } LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL diff --git a/programs/robocopy/tests/robocopy.c b/programs/robocopy/tests/robocopy.c index 8c594b78d92..de3d05fd7bc 100644 --- a/programs/robocopy/tests/robocopy.c +++ b/programs/robocopy/tests/robocopy.c @@ -67,21 +67,21 @@ START_TEST(robocopy) winetest_push_context("syntax test 1"); RemoveDirectoryW(L"invalid_folder"); exit_code = execute_robocopy(L""); - todo_wine ok(exit_code == 16, "unexpected exit code %d\n", exit_code); + ok(exit_code == 16, "unexpected exit code %d\n", exit_code); winetest_pop_context(); winetest_push_context("syntax test 2"); exit_code = execute_robocopy(L"invalid_folder"); - todo_wine ok(exit_code == 16, "unexpected exit code %d\n", exit_code); + ok(exit_code == 16, "unexpected exit code %d\n", exit_code); winetest_pop_context(); winetest_push_context("syntax test 3"); exit_code = execute_robocopy(L"-flag invalid_folder"); - todo_wine ok(exit_code == 16, "unexpected exit code %d\n", exit_code); + ok(exit_code == 16, "unexpected exit code %d\n", exit_code); winetest_pop_context(); winetest_push_context("syntax test 4"); exit_code = execute_robocopy(L"invalid_folder robocopy_destination"); - todo_wine ok(exit_code == 16, "unexpected exit code %d\n", exit_code); + ok(exit_code == 16, "unexpected exit code %d\n", exit_code); winetest_pop_context(); } -- 2.32.0
Basic conformance tests create a test source directory with files and execute robocopy on it, checking whether the resulting destination directory, the remaining source directory and the exit code is as expected Signed-off-by: Florian Eder <others.meder(a)gmail.com> --- v5: All wide strings are using debugstr_w in the ok tests, instead of %S in the format string, reduced size of cmd buffer to 511 chars --- programs/robocopy/tests/robocopy.c | 188 ++++++++++++++++++++++++++++- 1 file changed, 187 insertions(+), 1 deletion(-) diff --git a/programs/robocopy/tests/robocopy.c b/programs/robocopy/tests/robocopy.c index de3d05fd7bc..4dedd2aad8a 100644 --- a/programs/robocopy/tests/robocopy.c +++ b/programs/robocopy/tests/robocopy.c @@ -52,10 +52,153 @@ static DWORD execute_robocopy(const WCHAR *args) return exit_code; } +static void create_test_file(const WCHAR *relative_path, size_t size, LONGLONG fixed_filetime, long filetime_offset) +{ + HANDLE handle; + WCHAR path[1024]; + wcscpy(path, L"\\\\?\\"); + GetTempPathW(ARRAY_SIZE(path) - 4, &(path[4])); + wcscat(path, relative_path); + handle = CreateFileW(path, FILE_GENERIC_WRITE | FILE_GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_ALWAYS, 0, NULL); + ok(handle != INVALID_HANDLE_VALUE, "creation of %s failed (0x%08x)\n", debugstr_w(path), GetLastError()); + if (size != 0) + { + BYTE *data; + DWORD bytes_written; + data = calloc(size, sizeof(BYTE)); + ok(WriteFile(handle, data, size, &bytes_written, NULL), "writing to %s failed (%d)\n", debugstr_w(path), GetLastError()); + } + if (fixed_filetime != 0) + { + FILETIME time; + LARGE_INTEGER time_as_integer; + time_as_integer.QuadPart = fixed_filetime; + time.dwHighDateTime = time_as_integer.HighPart; + time.dwLowDateTime = time_as_integer.LowPart; + ok(SetFileTime(handle, &time, &time, &time), "filetime manipulation of %s failed (%d)\n", debugstr_w(path), GetLastError()); + } + if (filetime_offset != 0) + { + FILETIME time, modified_time, access_time; + LARGE_INTEGER time_as_integer; + GetFileTime(handle, &time, &modified_time, &access_time); + /* FILETIME is no union with LONGLONG / LONG64, casting could be unsafe */ + time_as_integer.HighPart = time.dwHighDateTime; + time_as_integer.LowPart = time.dwLowDateTime; + /* 1000 * 1000 * 60 * 60 * 24 = 86400000000ns per day */ + time_as_integer.QuadPart += 864000000000LL * filetime_offset; + time.dwHighDateTime = time_as_integer.HighPart; + time.dwLowDateTime = time_as_integer.LowPart; + ok(SetFileTime(handle, &time, &time, &time), "filetime manipulation of %s failed (%d)\n", debugstr_w(path), GetLastError()); + } + CloseHandle(handle); +} + +static void create_test_folder(const WCHAR *relative_path) +{ + WCHAR path[1024]; + wcscpy(path, L"\\\\?\\"); + GetTempPathW(ARRAY_SIZE(path) - 4, &(path[4])); + wcscat(path, relative_path); + CreateDirectoryW(path, NULL); +} + +static void create_test_source_folder(void) +{ + create_test_folder(L"robocopy_source"); + create_test_folder(L"robocopy_source\\folderA"); + create_test_folder(L"robocopy_source\\folderB"); + create_test_folder(L"robocopy_source\\folderC"); + create_test_folder(L"robocopy_source\\folderA\\folderD"); + create_test_folder(L"robocopy_source\\folderA\\folderE"); + create_test_file(L"robocopy_source\\fileA.a", 4000, 0, -10); + create_test_file(L"robocopy_source\\fileB.b", 8000, 0, -2); + create_test_file(L"robocopy_source\\folderA\\fileC.c", 60, 0, -2); + create_test_file(L"robocopy_source\\folderA\\fileD.d", 80, 0, 0); + create_test_file(L"robocopy_source\\folderA\\folderD\\fileE.e", 10000, 0, -10); + create_test_file(L"robocopy_source\\folderB\\fileF.f", 10000, 132223104000000000, 0); + create_test_file(L"robocopy_source\\folderB\\fileG.g", 200, 132223104000000000, 0); +} + +static void check_file_and_delete(const WCHAR *relative_path, BOOL should_exist) +{ + WCHAR path[1024]; + wcscpy(path, L"\\\\?\\"); + GetTempPathW(ARRAY_SIZE(path) - 4, &(path[4])); + wcscat(path, relative_path); + if (!DeleteFileW(path)) + { + if (GetLastError() == ERROR_FILE_NOT_FOUND) + ok(!should_exist, "file \"%s\" does not exist, but should exist\n", debugstr_w(relative_path)); + else if (GetLastError() == ERROR_PATH_NOT_FOUND) + ok(!should_exist, "file \"%s\" and the parent directory do not exist, but should exist\n", debugstr_w(relative_path)); + else + ok(FALSE, "file \"%s\" DeleteFileW returned error %d\n", debugstr_w(relative_path), GetLastError()); + } + else + { + ok(should_exist, "file \"%s\" should not exist, but does exist\n", debugstr_w(relative_path)); + } +} + +static void check_folder_and_delete(const WCHAR *relative_path, BOOL should_exist) +{ + WCHAR path[1024]; + wcscpy(path, L"\\\\?\\"); + GetTempPathW(ARRAY_SIZE(path) - 4, &(path[4])); + wcscat(path, relative_path); + if (!RemoveDirectoryW(path)) + { + if (GetLastError() == ERROR_FILE_NOT_FOUND) + ok(!should_exist, "directory \"%s\" does not exist, but should exist\n", debugstr_w(relative_path)); + else if (GetLastError() == ERROR_PATH_NOT_FOUND) + ok(!should_exist, "directory \"%s\" and the parent directory do not exist, but should exist\n", debugstr_w(relative_path)); + else if (GetLastError() == ERROR_DIR_NOT_EMPTY) + ok(FALSE, "directory \"%s\" is unexpectedly not empty\n", debugstr_w(relative_path)); + else + ok(FALSE, "directory \"%s\" DeleteFileW returned error %d\n", debugstr_w(relative_path), GetLastError()); + } + else + { + ok(should_exist, "directory \"%s\" should not exist, but does exist\n", debugstr_w(relative_path)); + } +} + +static void check_basic_copy_test(void) +{ + check_file_and_delete(L"robocopy_source\\fileA.a", TRUE); + check_file_and_delete(L"robocopy_source\\fileB.b", TRUE); + check_file_and_delete(L"robocopy_source\\folderA\\fileC.c", TRUE); + check_file_and_delete(L"robocopy_source\\folderA\\fileD.d", TRUE); + check_file_and_delete(L"robocopy_source\\folderA\\folderD\\fileE.e", TRUE); + check_file_and_delete(L"robocopy_source\\folderB\\fileF.f", TRUE); + check_file_and_delete(L"robocopy_source\\folderB\\fileG.g", TRUE); + check_folder_and_delete(L"robocopy_source\\folderA\\folderD", TRUE); + check_folder_and_delete(L"robocopy_source\\folderA\\folderE", TRUE); + check_folder_and_delete(L"robocopy_source\\folderA", TRUE); + check_folder_and_delete(L"robocopy_source\\folderB", TRUE); + check_folder_and_delete(L"robocopy_source\\folderC", TRUE); + check_folder_and_delete(L"robocopy_source", TRUE); + + check_file_and_delete(L"robocopy_destination\\fileA.a", TRUE); + check_file_and_delete(L"robocopy_destination\\fileB.b", TRUE); + todo_wine check_file_and_delete(L"robocopy_destination\\folderA\\fileC.c", FALSE); + todo_wine check_file_and_delete(L"robocopy_destination\\folderA\\fileD.d", FALSE); + todo_wine check_file_and_delete(L"robocopy_destination\\folderA\\folderD\\fileE.e", FALSE); + todo_wine check_file_and_delete(L"robocopy_destination\\folderB\\fileF.f", FALSE); + todo_wine check_file_and_delete(L"robocopy_destination\\folderB\\fileG.g", FALSE); + todo_wine check_folder_and_delete(L"robocopy_destination\\folderA\\folderD", FALSE); + check_folder_and_delete(L"robocopy_destination\\folderA\\folderE", FALSE); + todo_wine check_folder_and_delete(L"robocopy_destination\\folderA", FALSE); + todo_wine check_folder_and_delete(L"robocopy_destination\\folderB", FALSE); + check_folder_and_delete(L"robocopy_destination\\folderC", FALSE); + check_folder_and_delete(L"robocopy_destination", TRUE); +} + START_TEST(robocopy) { DWORD exit_code; - WCHAR temp_path[MAX_PATH]; + WCHAR temp_cmd[512], temp_path[MAX_PATH]; /* robocopy is only available from Vista onwards, abort test if not available */ if (execute_robocopy(L"") == MAXDWORD) return; @@ -84,4 +227,47 @@ START_TEST(robocopy) exit_code = execute_robocopy(L"invalid_folder robocopy_destination"); ok(exit_code == 16, "unexpected exit code %d\n", exit_code); winetest_pop_context(); + + winetest_push_context("basic copy test 1"); + create_test_source_folder(); + exit_code = execute_robocopy(L"robocopy_source robocopy_destination /r:1 /w:0"); + ok(exit_code == 1, "unexpected exit code %d\n", exit_code); + check_basic_copy_test(); + winetest_pop_context(); + + winetest_push_context("basic copy test 2"); + create_test_source_folder(); + exit_code = execute_robocopy(L"./robocopy_source third_folder/../robocopy_destination /r:1 /w:0"); + ok(exit_code == 1, "unexpected exit code %d\n", exit_code); + check_basic_copy_test(); + winetest_pop_context(); + + winetest_push_context("basic copy test 3"); + create_test_source_folder(); + create_test_folder(L"robocopy_destination"); + create_test_file(L"robocopy_destination\\fileA.a", 9000, 0, -10); + exit_code = execute_robocopy(L"./robocopy_source robocopy_source/../robocopy_destination /r:1 /w:0"); + ok(exit_code == 1, "unexpected exit code %d\n", exit_code); + check_basic_copy_test(); + winetest_pop_context(); + + winetest_push_context("basic copy test 4"); + create_test_source_folder(); + swprintf(temp_cmd, ARRAY_SIZE(temp_cmd), + L"%s\\robocopy_source %s\\robocopy_destination /r:1 /w:0", + temp_path, temp_path); + exit_code = execute_robocopy(temp_cmd); + ok(exit_code == 1, "unexpected exit code %d\n", exit_code); + check_basic_copy_test(); + winetest_pop_context(); + + winetest_push_context("basic copy test 5"); + create_test_source_folder(); + swprintf(temp_cmd, ARRAY_SIZE(temp_cmd), + L"%s\\third_folder\\..\\robocopy_source %s\\third_folder\\..\\robocopy_destination /r:1 /w:0", + temp_path, temp_path); + exit_code = execute_robocopy(temp_cmd); + ok(exit_code == 1, "unexpected exit code %d\n", exit_code); + check_basic_copy_test(); + winetest_pop_context(); } -- 2.32.0
participants (2)
-
Florian Eder -
Hugh McMaster