Basic files and required changes to configure(.ac) to create a stub version of the robocopy utility
Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=43653 Signed-off-by: Florian Eder others.meder@gmail.com --- v2: Modified according to code review, again, please feel free to give any kind of feedback regarding the code :-) --- 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 1adbf1789f3..b27b1b14e7d 100755 --- a/configure +++ b/configure @@ -21266,6 +21266,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 @@ -22799,4 +22800,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 eb970972936..b7faae4e4c4 100644 --- a/configure.ac +++ b/configure.ac @@ -3965,6 +3965,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..00dacc8ab6c --- /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 0; +} 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"
Basic files and required changes to configure(.ac) to create a scaffolding for the conformance tests for the robocopy utility
Signed-off-by: Florian Eder others.meder@gmail.com --- Modified according to code review, shorted variable names, removed unnecessary CreateProcess flags and moved ok-check to caller --- configure | 1 + configure.ac | 1 + programs/robocopy/tests/Makefile.in | 4 ++ programs/robocopy/tests/robocopy.c | 66 +++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+) create mode 100644 programs/robocopy/tests/Makefile.in create mode 100644 programs/robocopy/tests/robocopy.c
diff --git a/configure b/configure index b27b1b14e7d..31f5607c894 100755 --- a/configure +++ b/configure @@ -21267,6 +21267,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 b7faae4e4c4..ebeb4bf872a 100644 --- a/configure.ac +++ b/configure.ac @@ -3966,6 +3966,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..02798663ba5 --- /dev/null +++ b/programs/robocopy/tests/robocopy.c @@ -0,0 +1,66 @@ +/* + * 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/test.h> +#define WIN32_LEAN_AND_MEAN +#include <windows.h> +#include <wchar.h> + +static DWORD execute_robocopy(const WCHAR *cmd_line) +{ + STARTUPINFOW startup_info; + PROCESS_INFORMATION process_info; + DWORD exit_code; + WCHAR cmd_line_copy[2048]; + + memset(&startup_info, 0, sizeof(STARTUPINFOW)); + startup_info.dwFlags = STARTF_USESTDHANDLES; + + /* CreateProcess must not be called with static strings */ + wcscpy(cmd_line_copy, cmd_line); + + if (!CreateProcessW(NULL, cmd_line_copy, NULL, NULL, TRUE, 0, NULL, NULL, &startup_info, &process_info)) + return -1; + + if (WaitForSingleObject(process_info.hProcess, 30000) == WAIT_TIMEOUT) + return -1; + + GetExitCodeProcess(process_info.hProcess, &exit_code); + + CloseHandle(process_info.hThread); + CloseHandle(process_info.hProcess); + + return exit_code; +} + +START_TEST(robocopy) +{ + WCHAR previous_cwd_path[4096], temp_path[4096]; + + ok(GetFullPathNameW(L".", ARRAY_SIZE(previous_cwd_path), previous_cwd_path, NULL) != 0, "couldn't get CWD path"); + ok(GetTempPathW(ARRAY_SIZE(temp_path), temp_path) != 0, "couldn't get temp folder path"); + + /* robocopy is only available from Vista onwards, abort test if not available */ + if (execute_robocopy(L"robocopy.exe") == -1) return; + + ok(SetCurrentDirectoryW(temp_path), "couldn't set CWD to temp folder "%S"", temp_path); + + /* TODO: conformance tests here */ + + ok(SetCurrentDirectoryW(previous_cwd_path), "couldn't set CWD to previous CWD folder "%S"", previous_cwd_path); +}
Signed-off-by: Zebediah Figura zfigura@codeweavers.com
Parses (relative / absolute) path arguments as source, destination and files to include
Signed-off-by: Florian Eder others.meder@gmail.com --- Removed is_robocopy_flag as a seperate function, shorted function name, added comment to get_absolute_path and fixed code style issues in header file --- programs/robocopy/Makefile.in | 1 + programs/robocopy/main.c | 74 +++++++++++++++++++++++++++++++++++ programs/robocopy/robocopy.h | 33 ++++++++++++++++ 3 files changed, 108 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 00dacc8ab6c..e5400952ec5 100644 --- a/programs/robocopy/main.c +++ b/programs/robocopy/main.c @@ -21,9 +21,83 @@ 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; + +WCHAR *get_absolute_path(WCHAR *path) +{ + DWORD size; + WCHAR *absolute_path; + + /* allocate absolute path + potential backslash + null WCHAR */ + size = GetFullPathNameW(path, 0, NULL, NULL) + 2; + if (!wcsnicmp(path, L"\\?\", 4)) + { + /* already prefixed with \?\ */ + absolute_path = calloc(size, sizeof(WCHAR)); + GetFullPathNameW(path, size, absolute_path, NULL); + PathCchAddBackslashEx(absolute_path, size, NULL, NULL); + } + else + { + /* not prefixed with \?, we must add it in front of the path */ + absolute_path = calloc(size + 4, sizeof(WCHAR)); + wcscpy(absolute_path, L"\\?\"); + GetFullPathNameW(path, size, &(absolute_path[4]), NULL); + PathCchAddBackslashEx(absolute_path, size + 4, 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", 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] = calloc(wcslen(argv[i]) + 1, sizeof(WCHAR)); + wcscpy(options.files->array[options.files->size], argv[i]); + options.files->size++; + } + } + } +}
int __cdecl wmain(int argc, WCHAR *argv[]) { + parse_arguments(argc, argv); + WINE_FIXME("robocopy stub"); return 0; } 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; +};
Just a couple more nitpicks here; sorry I missed them the first time...
On 9/15/21 16:56, Florian Eder wrote:
diff --git a/programs/robocopy/main.c b/programs/robocopy/main.c index 00dacc8ab6c..e5400952ec5 100644 --- a/programs/robocopy/main.c +++ b/programs/robocopy/main.c @@ -21,9 +21,83 @@ 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;
+WCHAR *get_absolute_path(WCHAR *path)
Missing "static". The argument could also be const.
+{
- DWORD size;
- WCHAR *absolute_path;
- /* allocate absolute path + potential backslash + null WCHAR */
- size = GetFullPathNameW(path, 0, NULL, NULL) + 2;
- if (!wcsnicmp(path, L"\\?\", 4))
- {
/* already prefixed with \\?\ */
absolute_path = calloc(size, sizeof(WCHAR));
GetFullPathNameW(path, size, absolute_path, NULL);
PathCchAddBackslashEx(absolute_path, size, NULL, NULL);
- }
- else
- {
/* not prefixed with \\?\, we must add it in front of the path */
absolute_path = calloc(size + 4, sizeof(WCHAR));
wcscpy(absolute_path, L"\\\\?\\");
GetFullPathNameW(path, size, &(absolute_path[4]), NULL);
PathCchAddBackslashEx(absolute_path, size + 4, 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", argv[i]);
I think %S makes sense in the tests, but in the code it's probably better to use debugstr_w(), so that we can clearly see non-ASCII characters.
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] = calloc(wcslen(argv[i]) + 1, sizeof(WCHAR));
wcscpy(options.files->array[options.files->size], argv[i]);
This looks like wcsdup().
options.files->size++;
}
}
- }
+}
int __cdecl wmain(int argc, WCHAR *argv[]) {
- parse_arguments(argc, argv);
}WINE_FIXME("robocopy stub"); return 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@gmail.com --- Removed STRING_ADDITIONAL_INFO, as it isn't necessary to translate it, changed output_message to accept a string pointer to a format string, instead of a string table entry, and added an additional function to get a format string from a string table id --- programs/robocopy/main.c | 70 +++++++++++++++++++++++++++++++++++ programs/robocopy/robocopy.h | 6 +++ programs/robocopy/robocopy.rc | 11 ++++++ 3 files changed, 87 insertions(+)
diff --git a/programs/robocopy/main.c b/programs/robocopy/main.c index e5400952ec5..fc16fa1c2a2 100644 --- a/programs/robocopy/main.c +++ b/programs/robocopy/main.c @@ -27,6 +27,57 @@ 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 *strip_path_prefix(WCHAR* path) +{ + /* returns a path without the \?\ prefix */ + if (wcslen(path) <= 4) return path; + return &(path[4]); +} + WCHAR *get_absolute_path(WCHAR *path) { DWORD size; @@ -94,10 +145,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), strip_path_prefix(options.source)); + + if (!options.destination) output_message(format_string(STRING_DESTINATION), L"-"); + else output_message(format_string(STRING_DESTINATION), strip_path_prefix(options.destination)); + + output_message(format_string(STRING_FILES), options.files->array[0]); + for (i = 1; 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 0; } 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..3a2ddc9474f 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: %1\n" +} + LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL
#define WINE_FILEDESCRIPTION_STR "Wine Robocopy"
On 9/15/21 16:56, Florian Eder wrote:
+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), strip_path_prefix(options.source));
- if (!options.destination) output_message(format_string(STRING_DESTINATION), L"-");
- else output_message(format_string(STRING_DESTINATION), strip_path_prefix(options.destination));
- output_message(format_string(STRING_FILES), options.files->array[0]);
- for (i = 1; i < options.files->size; i++)
output_message(L" %1\n", options.files->array[i]);
Sorry if I was unclear. What I was trying to propose was to print the first file on the line *after* the "Files:" header, rather than on the same line. Then you can align all the paths the same arbitrary amount.
+}
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@gmail.com --- Combined patches 5, 6 and 11 from V1 to one patch, replaced open coded function with PathAllocCombine, removed some obvious comments and removed pointless check whether some directory is a file or directory in get_file_paths_in_folder as any error thrown by FindFirstFile is caught anyway
Still does not have any kind of max depth, so the complete source directory tree will be copied
Import of shlwapi is required for PathIsDirectoryW, which is AFAIAA not exported by kernelbase :-/ --- programs/robocopy/Makefile.in | 2 +- programs/robocopy/main.c | 166 +++++++++++++++++++++++++++++++++- programs/robocopy/robocopy.h | 17 ++++ programs/robocopy/robocopy.rc | 6 ++ 4 files changed, 188 insertions(+), 3 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 fc16fa1c2a2..16d1ba16445 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 *strip_path_prefix(WCHAR* path) { /* returns a path without the \?\ prefix */ @@ -145,6 +162,134 @@ static void parse_arguments(int argc, WCHAR *argv[]) } }
+static BOOL matches_array_entry(WCHAR *name, struct path_array *names_to_match) +{ + int i; + for (i = 0; i < names_to_match->size; i++) + { + if (PathMatchSpecW(name, names_to_match->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(strip_path_prefix(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(), strip_path_prefix(current_folder)); + return FALSE; + } + } + else + output_message(format_string(STRING_CREATE_DIRECTORY), strip_path_prefix(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_ENSURE_IS_EXTENDED_LENGTH_PATH, &parent_absolute_path); + + /* append * to recieve every file / subdirectory in this directory */ + PathAllocCombine(parent_absolute_path, L"*", PATHCCH_ENSURE_IS_EXTENDED_LENGTH_PATH, ¤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_ENSURE_IS_EXTENDED_LENGTH_PATH, ¤t_relative_path); + PathAllocCombine(directory_path, current_relative_path, PATHCCH_ENSURE_IS_EXTENDED_LENGTH_PATH, ¤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) && matches_array_entry(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, strip_path_prefix(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_ENSURE_IS_EXTENDED_LENGTH_PATH, ¤t_absolute_path); + + /* append the relative source path to the destination to get the target path */ + PathAllocCombine(options.destination, current_path->name, PATHCCH_ENSURE_IS_EXTENDED_LENGTH_PATH, &target_path); + + if (PathIsDirectoryW(current_absolute_path)) + { + if (!create_directory_path(target_path)) + output_error(STRING_ERROR_WRITE_DIRECTORY, GetLastError(), strip_path_prefix(target_path)); + } + else + { + create_directory_path(target_path); + if (!CopyFileW(current_absolute_path, target_path, FALSE)) + output_error(STRING_ERROR_WRITE_FILE, GetLastError(), strip_path_prefix(target_path)); + else + { + output_message(format_string(STRING_CREATE_FILE), strip_path_prefix(target_path)); + } + } + } + return TRUE; +} + static void print_header(void) { UINT i; @@ -166,8 +311,25 @@ 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] = calloc(64, sizeof(WCHAR)); + wcscpy(options.files->array[0], L"*.*"); + options.files->size++; + } + print_header();
- WINE_FIXME("robocopy stub"); - return 0; + /* 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 3a2ddc9474f..fd74722ae63 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: %1\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
On 9/15/21 16:56, Florian Eder wrote:
@@ -145,6 +162,134 @@ static void parse_arguments(int argc, WCHAR *argv[]) } }
+static BOOL matches_array_entry(WCHAR *name, struct path_array *names_to_match)
Nitpicking the naming again, but "matches_array_entry" is a bit ambiguous as it doesn't say what's in the array. Maybe "path_in_array"?
On the other hand, "names_to_match" is more verbose than it needs to be; we can already tell what the function is doing, so I'd shorten it to "names" (or "paths" or something).
+{
- int i;
- for (i = 0; i < names_to_match->size; i++)
- {
if (PathMatchSpecW(name, names_to_match->array[i])) return TRUE;
- }
- return FALSE;
+}
...
@@ -166,8 +311,25 @@ 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] = calloc(64, sizeof(WCHAR));
wcscpy(options.files->array[0], L"*.*");
How about "wcsdup(L"*.*")"?
options.files->size++;
- }
print_header();
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@gmail.com --- programs/robocopy/tests/robocopy.c | 189 ++++++++++++++++++++++++++++- 1 file changed, 187 insertions(+), 2 deletions(-)
diff --git a/programs/robocopy/tests/robocopy.c b/programs/robocopy/tests/robocopy.c index 02798663ba5..60ce0aeecca 100644 --- a/programs/robocopy/tests/robocopy.c +++ b/programs/robocopy/tests/robocopy.c @@ -48,9 +48,153 @@ static DWORD execute_robocopy(const WCHAR *cmd_line) 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", 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", 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", 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", 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", 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", relative_path); + else + ok(FALSE, "file "%S" DeleteFileW returned error %d\n", relative_path, GetLastError()); + } + else + { + ok(should_exist, "file "%S" should not exist, but does exist\n", 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", 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", relative_path); + else if (GetLastError() == ERROR_DIR_NOT_EMPTY) + ok(FALSE, "directory "%S" is unexpectedly not empty\n", relative_path); + else + ok(FALSE, "directory "%S" DeleteFileW returned error %d\n", relative_path, GetLastError()); + } + else + { + ok(should_exist, "directory "%S" should not exist, but does exist\n", 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) { - WCHAR previous_cwd_path[4096], temp_path[4096]; + WCHAR temp_command_line[2048], previous_cwd_path[4096], temp_path[4096]; + DWORD exit_code;
ok(GetFullPathNameW(L".", ARRAY_SIZE(previous_cwd_path), previous_cwd_path, NULL) != 0, "couldn't get CWD path"); ok(GetTempPathW(ARRAY_SIZE(temp_path), temp_path) != 0, "couldn't get temp folder path"); @@ -60,7 +204,48 @@ START_TEST(robocopy)
ok(SetCurrentDirectoryW(temp_path), "couldn't set CWD to temp folder "%S"", temp_path);
- /* TODO: conformance tests here */ + winetest_push_context("basic copy test 1"); + create_test_source_folder(); + exit_code = execute_robocopy(L"robocopy.exe 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.exe ./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.exe ./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_command_line, ARRAY_SIZE(temp_command_line), + L"robocopy.exe %s\robocopy_source %s\robocopy_destination /r:1 /w:0", + temp_path, temp_path); + exit_code = execute_robocopy(temp_command_line); + 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_command_line, ARRAY_SIZE(temp_command_line), + L"robocopy.exe %s\third_folder\..\robocopy_source %s\third_folder\..\robocopy_destination /r:1 /w:0", + temp_path, temp_path); + exit_code = execute_robocopy(temp_command_line); + ok(exit_code == 1, "unexpected exit code %d\n", exit_code); + check_basic_copy_test(); + winetest_pop_context();
ok(SetCurrentDirectoryW(previous_cwd_path), "couldn't set CWD to previous CWD folder "%S"", previous_cwd_path); }