diff --git a/.cz.json b/.cz.json index 2066c1a..997e7b0 100644 --- a/.cz.json +++ b/.cz.json @@ -1,7 +1,13 @@ { "commitizen": { "tag_format": "v$version", - "version_files": ["CMakeLists.txt", "README.md", "WinPrefs/WinPrefs.psd1", "package.json"], + "version_files": [ + "CMakeLists.txt", + "Doxyfile.in", + "README.md", + "WinPrefs/WinPrefs.psd1", + "package.json" + ], "version_provider": "npm", "version_scheme": "semver" } diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e9d6ce..b90a718 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,6 +6,7 @@ project( LANGUAGES C VERSION 0.2.1) +option(BUILD_DOCS "Build documentation." OFF) option(BUILD_TESTS "Build and run tests." OFF) option(ENABLE_COVERAGE "Link tests with gcov." OFF) option(ENABLE_VLD "Debug only: enable Visual Leak Detector." OFF) @@ -14,6 +15,18 @@ if(BUILD_TESTS) find_package(cmocka REQUIRED) endif() +if(BUILD_DOCS) + find_package(Doxygen REQUIRED) + set(DOXYGEN_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile.in ${DOXYGEN_OUT} @ONLY) + add_custom_target( + doc ALL + COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYGEN_OUT} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) + install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/docs/html + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/doc/bincookie-0.1.6) +endif() + add_subdirectory(native) set(CPACK_PACKAGE_NAME "winprefs") diff --git a/native/CMakeLists.txt b/native/CMakeLists.txt index f9e0959..e285660 100644 --- a/native/CMakeLists.txt +++ b/native/CMakeLists.txt @@ -1,10 +1,12 @@ include(GNUInstallDirs) if(CMAKE_BUILD_TYPE MATCHES "Debug|RelWithDebInfo" AND WITH_VLD) - find_library(VLD vld HINTS "C:/Program Files (x86)/Visual Leak Detector/lib/Win64" - "C:/Program Files/Visual Leak Detector/lib/Win64" REQUIRED) - find_path(VLD_H vld.h HINTS "C:/Program Files (x86)/Visual Leak Detector/include" - "C:/Program Files/Visual Leak Detector/include" REQUIRED) + find_library( + VLD vld HINTS "C:/Program Files (x86)/Visual Leak Detector/lib/Win64" + "C:/Program Files/Visual Leak Detector/lib/Win64" REQUIRED) + find_path(VLD_H vld.h + HINTS "C:/Program Files (x86)/Visual Leak Detector/include" + "C:/Program Files/Visual Leak Detector/include" REQUIRED) endif() set(GCC_CLANG_SHARED_C_FLAGS -fno-builtin -std=gnu2x) @@ -49,10 +51,16 @@ add_executable( arg.h constants.c constants.h + debug.c + debug.h + git.c + git.h macros.h main.c reg_command.c reg_command.h + registry.c + registry.h shell.c shell.h) target_compile_definitions(winprefs PRIVATE UNICODE _UNICODE) @@ -72,14 +80,17 @@ target_link_libraries( PRIVATE $<$,$,$>>:${VLD}> ) -if(CMAKE_C_COMPILER_ID MATCHES "Clang|GNU" OR CMAKE_C_COMPILER MATCHES "/winegcc$") +if(CMAKE_C_COMPILER_ID MATCHES "Clang|GNU" OR CMAKE_C_COMPILER MATCHES + "/winegcc$") get_property(IS_64BIT GLOBAL PROPERTY FIND_LIBRARY_USE_LIB64_PATHS) # mingw can do 2000 without extra help. 64-bit requires at least XP. - target_compile_definitions(winprefs - PRIVATE _WIN32_WINNT=$,0x501,0x0500>) - target_compile_options(winprefs PRIVATE ${GCC_CLANG_SHARED_C_FLAGS} - $<$:${GCC_CLANG_DEBUG_C_FLAGS}>) - target_link_libraries(winprefs PRIVATE ole32 shell32) + target_compile_definitions( + winprefs + PRIVATE _WIN32_WINNT=$,0x501,0x0500>) + target_compile_options( + winprefs PRIVATE ${GCC_CLANG_SHARED_C_FLAGS} + $<$:${GCC_CLANG_DEBUG_C_FLAGS}>) + target_link_libraries(winprefs PRIVATE shell32) target_link_options(winprefs PRIVATE -municode) if(CMAKE_C_COMPILER MATCHES "/winegcc$") target_include_directories(winprefs PRIVATE /usr/include/wine/msvcrt) diff --git a/native/arg.h b/native/arg.h index 26c9689..7989138 100644 --- a/native/arg.h +++ b/native/arg.h @@ -6,48 +6,50 @@ #ifndef ARG_H_INCLUDED -static int -ARG_LONG_func(wchar_t **argv0, char const *name) -{ - wchar_t *argIt = *argv0; - while (*argIt == *name && *argIt) - argIt++, name++; - if (*argIt == *name || (*argIt == '=' && !*name)) { - *argv0 = argIt; - return 1; - } - return 0; +static int ARG_LONG_func(wchar_t **argv0, char const *name) { + wchar_t *argIt = *argv0; + while (*argIt == *name && *argIt) + argIt++, name++; + if (*argIt == *name || (*argIt == '=' && !*name)) { + *argv0 = argIt; + return 1; + } + return 0; } -#define ARG_BEGIN do { \ - for (argv[0] && (--argc, ++argv); \ - argv[0] && argv[0][0] == '-'; \ - argv[0] && (--argc, ++argv)) { \ - int isFlag = 1; \ - wchar_t *arg = argv[0]; \ - if (arg[1] == '-' && arg[2] == 0 && (--argc, ++argv, 1)) \ - break; \ - ARG_BEGIN_REP: \ - switch ((++arg)[0]) { \ - case '-': \ - isFlag = 0; \ - if (arg[-1] == '-') \ - ++arg; +#define ARG_BEGIN \ + do { \ + for (argv[0] && (--argc, ++argv); argv[0] && argv[0][0] == '-'; \ + argv[0] && (--argc, ++argv)) { \ + int isFlag = 1; \ + wchar_t *arg = argv[0]; \ + if (arg[1] == '-' && arg[2] == 0 && (--argc, ++argv, 1)) \ + break; \ + ARG_BEGIN_REP: \ + switch ((++arg)[0]) { \ + case '-': \ + isFlag = 0; \ + if (arg[-1] == '-') \ + ++arg; #define ARG_LONG(name) ARG_LONG_func(&(arg), (name)) -#define ARG_VAL() \ - (isFlag ? (arg[1] ? ++arg : *(--argc, ++argv)) : \ - (arg[0] == '=' ? ++arg : *(--argc, ++argv))) +#define ARG_VAL() \ + (isFlag ? (arg[1] ? ++arg : *(--argc, ++argv)) : (arg[0] == '=' ? ++arg : *(--argc, ++argv))) -#define ARG_FLAG() if (isFlag && arg[1]) goto ARG_BEGIN_REP +#define ARG_FLAG() \ + if (isFlag && arg[1]) \ + goto ARG_BEGIN_REP -#define ARG_END } } } while(0) +#define ARG_END \ + } \ + } \ + } \ + while (0) #define ARG_H_INCLUDED #endif - /* * Example: */ @@ -56,8 +58,8 @@ ARG_LONG_func(wchar_t **argv0, char const *name) // spell-checker: disable -#include #include +#include #ifdef ARG_FUZZ #define main argMain @@ -65,63 +67,81 @@ ARG_LONG_func(wchar_t **argv0, char const *name) #define stderr stdout #endif - -int -main(int argc, char **argv) -{ - char *argv0 = argv[0]; - int a = 0, b = 0, c = 0, reverse = 0; - char const *input = "default", *output = "default"; - int readstdin = 0; - - ARG_BEGIN { - if (0) { - case 'a': a = 1; ARG_FLAG(); break; - case 'b': b = 1; ARG_FLAG(); break; - case 'c': c = 1; ARG_FLAG(); break; - case '\0': readstdin = 1; break; - } else if (ARG_LONG("reverse")) case 'r': { - reverse = 1; - ARG_FLAG(); - } else if (ARG_LONG("input")) case 'i': { - input = ARG_VAL(); - } else if (ARG_LONG("output")) case 'o': { - output = ARG_VAL(); - } else if (ARG_LONG("help")) case 'h': case '?': { - printf("Usage: %s [OPTION...] [STRING...]\n", argv0); - puts("Example usage of arg.h\n"); - puts("Options:"); - puts(" -a, set a to true"); - puts(" -b, set b to true"); - puts(" -c, set c to true"); - puts(" -r, --reverse set reverse to true"); - puts(" -i, --input=STR set input string to STR"); - puts(" -o, --output=STR set output string to STR"); - puts(" -h, --help display this help and exit"); - return EXIT_SUCCESS; - } else { default: - fprintf(stderr, - "%s: invalid option '%s'\n" - "Try '%s --help' for more information.\n", - argv0, *argv, argv0); - return EXIT_FAILURE; - } - } ARG_END; - - printf("a = %s\n", a ? "true" : "false"); - printf("b = %s\n", b ? "true" : "false"); - printf("c = %s\n", c ? "true" : "false"); - printf("reverse = %s\n", reverse ? "true" : "false"); - printf("readstdin = %s\n", readstdin ? "true" : "false"); - printf("input = %s\n", input); - printf("output = %s\n", output); - - printf("\nargc: %d", argc); - puts("\nargv:"); - while (*argv) - printf(" %s\n", *argv++); - - return 0; +int main(int argc, char **argv) { + char *argv0 = argv[0]; + int a = 0, b = 0, c = 0, reverse = 0; + char const *input = "default", *output = "default"; + int readstdin = 0; + + ARG_BEGIN { + if (0) { + case 'a': + a = 1; + ARG_FLAG(); + break; + case 'b': + b = 1; + ARG_FLAG(); + break; + case 'c': + c = 1; + ARG_FLAG(); + break; + case '\0': + readstdin = 1; + break; + } else if (ARG_LONG("reverse")) + case 'r': { + reverse = 1; + ARG_FLAG(); + } + else if (ARG_LONG("input")) case 'i': { + input = ARG_VAL(); + } + else if (ARG_LONG("output")) case 'o': { + output = ARG_VAL(); + } + else if (ARG_LONG("help")) case 'h': + case '?': { + printf("Usage: %s [OPTION...] [STRING...]\n", argv0); + puts("Example usage of arg.h\n"); + puts("Options:"); + puts(" -a, set a to true"); + puts(" -b, set b to true"); + puts(" -c, set c to true"); + puts(" -r, --reverse set reverse to true"); + puts(" -i, --input=STR set input string to STR"); + puts(" -o, --output=STR set output string to STR"); + puts(" -h, --help display this help and exit"); + return EXIT_SUCCESS; + } + else { + default: + fprintf(stderr, + "%s: invalid option '%s'\n" + "Try '%s --help' for more information.\n", + argv0, + *argv, + argv0); + return EXIT_FAILURE; + } + } + ARG_END; + + printf("a = %s\n", a ? "true" : "false"); + printf("b = %s\n", b ? "true" : "false"); + printf("c = %s\n", c ? "true" : "false"); + printf("reverse = %s\n", reverse ? "true" : "false"); + printf("readstdin = %s\n", readstdin ? "true" : "false"); + printf("input = %s\n", input); + printf("output = %s\n", output); + + printf("\nargc: %d", argc); + puts("\nargv:"); + while (*argv) + printf(" %s\n", *argv++); + + return 0; } #endif /* ARG_EXAMPLE */ @@ -136,33 +156,31 @@ main(int argc, char **argv) #include #define MAX_ARGC 50000 -int -LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) -{ - if (size < 2) - return -1; +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size < 2) + return -1; - char *buf = malloc(size+2); - memcpy(buf, data, size); - buf[size] = buf[size+1] = 0; + char *buf = malloc(size + 2); + memcpy(buf, data, size); + buf[size] = buf[size + 1] = 0; - static char *argv[MAX_ARGC+1]; - size_t argc = 0; - for (char *ptr = buf; *ptr && argc < MAX_ARGC;) { - argv[argc++] = ptr; - while (*ptr++); - } - argv[argc] = 0; + static char *argv[MAX_ARGC + 1]; + size_t argc = 0; + for (char *ptr = buf; *ptr && argc < MAX_ARGC;) { + argv[argc++] = ptr; + while (*ptr++) + ; + } + argv[argc] = 0; - main(argc, argv); - free(buf); + main(argc, argv); + free(buf); - return 0; + return 0; } #endif - /* * Copyright (c) 2021 Olaf Berstein * Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/native/constants.h b/native/constants.h index e8f70bc..392fd7d 100644 --- a/native/constants.h +++ b/native/constants.h @@ -1,11 +1,16 @@ +/** \file */ #ifndef CONSTANTS_H #define CONSTANTS_H #include +//! Maximum `cmd.exe` command line length. extern const size_t CMD_MAX_COMMAND_LENGTH; +//! Maximum length of a registry key. extern const size_t MAX_KEY_LENGTH; +//! Maximum length of the value-name in a `reg add` command. extern const size_t MAX_VALUE_NAME; +//! Alias of `sizeof(wchar_t)`. extern const size_t WL; extern const wchar_t *AUTOMATIC_COMMIT_MESSAGE_PREFIX; diff --git a/native/debug.c b/native/debug.c new file mode 100644 index 0000000..7853525 --- /dev/null +++ b/native/debug.c @@ -0,0 +1,13 @@ +#include +#include + +#include "debug.h" + +void debug_print(const wchar_t *format, ...) { + if (debug_print_enabled) { + va_list args; + va_start(args, format); + vfwprintf(stderr, format, args); + va_end(args); + } +} diff --git a/native/debug.h b/native/debug.h new file mode 100644 index 0000000..680e99f --- /dev/null +++ b/native/debug.h @@ -0,0 +1,15 @@ +/** \file */ +#ifndef DEBUG_H +#define DEBUG_H + +#include "macros.h" + +extern bool debug_print_enabled; + +//! Print only when debug logging is enabled. +/*! + \param format Format string. + */ +void debug_print(const wchar_t *format, ...); + +#endif diff --git a/native/git.c b/native/git.c new file mode 100644 index 0000000..ba5a97e --- /dev/null +++ b/native/git.c @@ -0,0 +1,264 @@ +#include +#include +#include + +#include + +#include "constants.h" +#include "debug.h" +#include "git.h" +#include "macros.h" + +static inline bool has_git() { + return _wspawnlp(P_WAIT, L"git.exe", L"git", L"--version", nullptr) >= 0; +} + +static inline bool dir_exists(wchar_t *path) { + DWORD attrib = GetFileAttributes(path); + return attrib != INVALID_FILE_ATTRIBUTES && (attrib & FILE_ATTRIBUTE_DIRECTORY); +} + +bool git_commit(const wchar_t *output_dir, const wchar_t *deploy_key) { + if (!has_git()) { + debug_print(L"Wanted to commit but git.exe is not in PATH or failed to run.\n"); + return false; + } + debug_print(L"Committing changes.\n"); + size_t work_tree_arg_len = wcslen(output_dir) + wcslen(L"--work-tree=") + 1; + wchar_t *work_tree_arg = calloc(work_tree_arg_len, WL); + if (!work_tree_arg) { + return false; + } + wmemset(work_tree_arg, L'\0', work_tree_arg_len); + _snwprintf(work_tree_arg, work_tree_arg_len, L"--work-tree=%ls", output_dir); + size_t git_dir_len = wcslen(output_dir) + wcslen(L"\\.git") + 1; + wchar_t *git_dir = calloc(git_dir_len, WL); + if (!git_dir) { + return false; + } + _snwprintf(git_dir, git_dir_len, L"%ls\\.git", output_dir); + git_dir[git_dir_len - 1] = L'\0'; + if (!dir_exists(git_dir)) { + wchar_t *cwd = calloc(MAX_PATH, WL); + if (!cwd) { + return false; + } + wmemset(cwd, L'\0', MAX_PATH); + if (!_wgetcwd(cwd, MAX_PATH)) { + return false; + } + if (_wchdir(output_dir) != 0) { + return false; + } + if (_wspawnlp(P_WAIT, L"git.exe", L"git", L"init", L"--quiet", nullptr) != 0) { + return false; + } + if (_wchdir(cwd) != 0) { + return false; + } + } + size_t git_dir_arg_len = git_dir_len + wcslen(L"--git-dir=") + 1; + wchar_t *git_dir_arg = calloc(git_dir_arg_len, WL); + if (!git_dir_arg) { + return false; + } + wmemset(git_dir_arg, L'\0', git_dir_arg_len); + _snwprintf(git_dir_arg, git_dir_arg_len, L"--git-dir=%ls", git_dir); + free(git_dir); + git_dir_arg[git_dir_arg_len - 1] = L'\0'; + if (_wspawnlp(P_WAIT, L"git.exe", L"git", git_dir_arg, work_tree_arg, L"add", L".", nullptr) != + 0) { + return false; + } + size_t time_needed_size = + (size_t)GetTimeFormat(LOCALE_USER_DEFAULT, 0, nullptr, nullptr, nullptr, 0); + if (!time_needed_size) { + return false; + } + wchar_t *time_buf = calloc(time_needed_size, WL); + if (!GetTimeFormat(LOCALE_USER_DEFAULT, 0, nullptr, nullptr, time_buf, (int)time_needed_size)) { + return false; + } + size_t date_needed_size = + (size_t)GetDateFormat(LOCALE_USER_DEFAULT, 0, nullptr, nullptr, nullptr, 0); + if (!date_needed_size) { + return false; + } + wchar_t *date_buf = calloc(date_needed_size, WL); + if (!GetDateFormat(LOCALE_USER_DEFAULT, 0, nullptr, nullptr, date_buf, (int)date_needed_size)) { + return false; + } + size_t needed_size = + wcslen(AUTOMATIC_COMMIT_MESSAGE_PREFIX) + 3 + time_needed_size + date_needed_size; + wchar_t *message_buf = calloc(needed_size, WL); + if (!message_buf) { + return false; + } + wmemset(message_buf, L'\0', needed_size); + _snwprintf(message_buf, + needed_size, + L"\"%ls%ls %ls\"", + AUTOMATIC_COMMIT_MESSAGE_PREFIX, + date_buf, + time_buf); + free(date_buf); + free(time_buf); + if (_wspawnlp(P_WAIT, + L"git.exe", + L"git", + git_dir_arg, + work_tree_arg, + L"commit", + L"--no-gpg-sign", + L"--quiet", + L"--no-verify", + L"\"--author=winprefs \"", + L"-m", + message_buf, + nullptr) != 0) { + return false; + } + free(message_buf); + if (deploy_key) { + wchar_t full_deploy_key_path[MAX_PATH]; + if (!_wfullpath(full_deploy_key_path, deploy_key, MAX_PATH)) { + return false; + } + debug_print(L"Deploy key: %ls\n", full_deploy_key_path); + size_t ssh_command_len = 68 + wcslen(full_deploy_key_path) + 3; + wchar_t *ssh_command = calloc(ssh_command_len, WL); + if (!ssh_command) { + return false; + } + wmemset(ssh_command, L'\0', ssh_command_len); + _snwprintf(ssh_command, + ssh_command_len, + L"\"ssh -i %ls -F nul -o UserKnownHostsFile=nul -o StrictHostKeyChecking=no\"", + full_deploy_key_path); + if (_wspawnlp(P_WAIT, + L"git.exe", + L"git", + git_dir_arg, + work_tree_arg, + L"config", + L"core.sshCommand", + ssh_command, + nullptr) != 0) { + return false; + } + wchar_t *branch_arg = + get_git_branch(git_dir_arg, git_dir_arg_len, work_tree_arg, work_tree_arg_len); + debug_print(L"git.exe \"%ls\" \"%ls\" push -u --porcelain --no-signed origin origin %ls\n", + git_dir_arg, + work_tree_arg, + branch_arg); + if (_wspawnlp(P_WAIT, + L"git.exe", + L"git", + git_dir_arg, + work_tree_arg, + L"push", + L"-u", + L"--porcelain", + L"--no-signed", + L"origin", + L"origin", + branch_arg, + nullptr) != 0) { + return false; + } + free(ssh_command); + free(branch_arg); + } + free(git_dir_arg); + free(work_tree_arg); + return true; +} + +// Based on https://stackoverflow.com/a/35658917/374110 +wchar_t *get_git_branch(const wchar_t *git_dir_arg, + size_t git_dir_arg_len, + const wchar_t *work_tree_arg, + size_t work_tree_arg_len) { + char *result = malloc(255); + if (!result) { + return nullptr; + } + memset(result, 0, 255); + HANDLE pipe_read, pipe_write; + SECURITY_ATTRIBUTES sa_attr = {.lpSecurityDescriptor = nullptr, + .bInheritHandle = + TRUE, // Pipe handles are inherited by child process. + .nLength = sizeof(SECURITY_ATTRIBUTES)}; + // Create a pipe to get results from child's stdout. + if (!CreatePipe(&pipe_read, &pipe_write, &sa_attr, 0)) { + free(result); + return nullptr; + } + STARTUPINFOW si = {.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES, + .hStdOutput = pipe_write, + .hStdError = pipe_write, + .wShowWindow = SW_HIDE}; + PROCESS_INFORMATION pi = {0}; + size_t cmd_len = git_dir_arg_len + work_tree_arg_len + 30; + wchar_t *cmd = calloc(cmd_len, WL); + if (!cmd) { + free(result); + CloseHandle(pipe_write); + CloseHandle(pipe_read); + return nullptr; + } + wmemset(cmd, L'\0', cmd_len); + _snwprintf(cmd, cmd_len, L"git.exe %ls %ls branch --show-current", git_dir_arg, work_tree_arg); + cmd[cmd_len - 1] = L'\0'; + BOOL ret = CreateProcess( + nullptr, cmd, nullptr, nullptr, TRUE, CREATE_NEW_CONSOLE, nullptr, nullptr, &si, &pi); + if (!ret) { + CloseHandle(pipe_write); + CloseHandle(pipe_read); + free(result); + return nullptr; + } + free(cmd); + bool proc_ended = false; + for (; !proc_ended;) { + // Give some time slice (50 ms), so we won't waste 100% CPU. + proc_ended = WaitForSingleObject(pi.hProcess, 50) == WAIT_OBJECT_0; + // Even if process exited - we continue reading, if + // there is some data available over pipe. + for (;;) { + char buf[255]; + memset(buf, L'\0', 255); + DWORD read = 0; + DWORD avail = 0; + if (!PeekNamedPipe(pipe_read, nullptr, 0, nullptr, &avail, nullptr)) { + break; + } + if (!avail) { // No data available, return + break; + } + if (!ReadFile(pipe_read, buf, min(sizeof(buf) - 1, avail), &read, nullptr) || !read) { + // Error, the child process might have ended + break; + } + buf[min(sizeof(buf) - 1, avail)] = L'\0'; + if (avail) { + strncat(result, buf, proc_ended ? avail - 1 : avail); // Strip newline + } + } + } + CloseHandle(pipe_write); + CloseHandle(pipe_read); + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + size_t res_len = strlen(result); + int w_len = MultiByteToWideChar(CP_UTF8, 0, result, (int)res_len, nullptr, 0); + wchar_t *w_result = calloc((size_t)w_len + 1, WL); + if (!w_result) { + return nullptr; + } + MultiByteToWideChar(CP_UTF8, 0, result, (int)res_len, w_result, w_len); + w_result[w_len] = L'\0'; + free(result); + return w_result; +} diff --git a/native/git.h b/native/git.h new file mode 100644 index 0000000..b434df7 --- /dev/null +++ b/native/git.h @@ -0,0 +1,19 @@ +/** \file */ +#ifndef GIT_H +#define GIT_H + +//! Get the current Git branch in a checked out repository. +/*! + \param git_dir_arg `--git-dir=...` argument. + \param git_dir_arg_len `git_dir_arg` length in word size. + \param work_tree_arg `--work-tree=...` argument. + \param work_tree_arg_len `work_tree_arg` length in word size. + \return Pointer to string with branch name. Must be freed on caller side. + */ +wchar_t *get_git_branch(const wchar_t *git_dir_arg, + size_t git_dir_arg_len, + const wchar_t *work_tree_arg, + size_t work_tree_arg_len); +bool git_commit(const wchar_t *output_dir, const wchar_t *deploy_key); + +#endif // GIT_H diff --git a/native/macros.h b/native/macros.h index a4f9978..0fd29fc 100644 --- a/native/macros.h +++ b/native/macros.h @@ -4,6 +4,7 @@ #if (defined(__STDC_VERSION__) && __STDC_VERSION__ > 201710L) #include #else +//! `nullptr` definition if not defined by `stddef.h`. #define nullptr (void *)0 #endif @@ -16,6 +17,7 @@ #endif #ifndef _WC_ERR_INVALID_CHARS +//! Fallback for pre-Vista. Same value as `WC_ERR_INVALID_CHARS`. #define _WC_ERR_INVALID_CHARS 0x0080 #endif diff --git a/native/main.c b/native/main.c index 1cfe3bc..c61312f 100644 --- a/native/main.c +++ b/native/main.c @@ -1,3 +1,5 @@ +/** \file */ +#include #include #include #include @@ -13,460 +15,33 @@ #include "arg.h" #include "constants.h" +#include "debug.h" +#include "git.h" #include "macros.h" #include "reg_command.h" +#include "registry.h" -BOOL dir_exists(wchar_t *path) { - DWORD attrib = GetFileAttributes(path); - return attrib != INVALID_FILE_ATTRIBUTES && (attrib & FILE_ATTRIBUTE_DIRECTORY); -} - -// Based on https://stackoverflow.com/a/35658917/374110 -wchar_t *get_git_branch(const wchar_t *git_dir_arg, - size_t git_dir_arg_len, - const wchar_t *work_tree_arg, - size_t work_tree_arg_len) { - char *result = malloc(255); - if (!result) { - return nullptr; - } - memset(result, 0, 255); - HANDLE pipe_read, pipe_write; - SECURITY_ATTRIBUTES sa_attr = {.lpSecurityDescriptor = nullptr, - .bInheritHandle = - TRUE, // Pipe handles are inherited by child process. - .nLength = sizeof(SECURITY_ATTRIBUTES)}; - // Create a pipe to get results from child's stdout. - if (!CreatePipe(&pipe_read, &pipe_write, &sa_attr, 0)) { - free(result); - return nullptr; - } - STARTUPINFOW si = {.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES, - .hStdOutput = pipe_write, - .hStdError = pipe_write, - .wShowWindow = SW_HIDE}; - PROCESS_INFORMATION pi = {0}; - size_t cmd_len = git_dir_arg_len + work_tree_arg_len + 30; - wchar_t *cmd = calloc(cmd_len, WL); - if (!cmd) { - free(result); - CloseHandle(pipe_write); - CloseHandle(pipe_read); - return nullptr; - } - wmemset(cmd, L'\0', cmd_len); - _snwprintf(cmd, cmd_len, L"git.exe %ls %ls branch --show-current", git_dir_arg, work_tree_arg); - cmd[cmd_len - 1] = L'\0'; - BOOL ret = CreateProcess( - nullptr, cmd, nullptr, nullptr, TRUE, CREATE_NEW_CONSOLE, nullptr, nullptr, &si, &pi); - if (!ret) { - CloseHandle(pipe_write); - CloseHandle(pipe_read); - free(result); - return nullptr; - } - free(cmd); - bool proc_ended = false; - for (; !proc_ended;) { - // Give some time slice (50 ms), so we won't waste 100% CPU. - proc_ended = WaitForSingleObject(pi.hProcess, 50) == WAIT_OBJECT_0; - // Even if process exited - we continue reading, if - // there is some data available over pipe. - for (;;) { - char buf[255]; - memset(buf, L'\0', 255); - DWORD read = 0; - DWORD avail = 0; - if (!PeekNamedPipe(pipe_read, nullptr, 0, nullptr, &avail, nullptr)) { - break; - } - if (!avail) { // No data available, return - break; - } - if (!ReadFile(pipe_read, buf, min(sizeof(buf) - 1, avail), &read, nullptr) || !read) { - // Error, the child process might have ended - break; - } - buf[min(sizeof(buf) - 1, avail)] = L'\0'; - if (avail) { - strncat(result, buf, proc_ended ? avail - 1 : avail); // Strip newline - } - } - } - CloseHandle(pipe_write); - CloseHandle(pipe_read); - CloseHandle(pi.hProcess); - CloseHandle(pi.hThread); - size_t res_len = strlen(result); - int w_len = MultiByteToWideChar(CP_UTF8, 0, result, (int)res_len, nullptr, 0); - wchar_t *w_result = calloc((size_t)w_len + 1, WL); - if (!w_result) { - return nullptr; - } - MultiByteToWideChar(CP_UTF8, 0, result, (int)res_len, w_result, w_len); - w_result[w_len] = L'\0'; - free(result); - return w_result; -} - -void write_reg_commands(HKEY hk, - const wchar_t *stem, - int max_depth, - int depth, - HANDLE out_fp, - const wchar_t *prior_stem, - bool debug) { - if (depth >= max_depth) { - if (debug) { - fwprintf(stderr, - L"%ls: Skipping %ls due to depth limit of %d.\n", - prior_stem, - stem, - max_depth); - } - return; - } - HKEY hk_out; - size_t full_path_len = WL * MAX_KEY_LENGTH; - wchar_t *full_path = calloc(MAX_KEY_LENGTH, WL); - if (full_path == nullptr) { - fwprintf(stderr, L"%ls: Stopping due to memory error.\n", prior_stem); - abort(); - } - wmemset(full_path, L'\0', MAX_KEY_LENGTH); - size_t prior_stem_len = wcslen(prior_stem) * WL; - size_t stem_len = stem ? wcslen(stem) : 0; - if ((prior_stem_len + (stem_len * WL) + 2) > (full_path_len - 2)) { - if (debug) { - fwprintf( - stderr, L"%ls: Skipping %ls because of length limitation.\n", prior_stem, stem); - } - free(full_path); - return; - } - memcpy(full_path, prior_stem, prior_stem_len); - if (stem) { - wcsncat(full_path, L"\\", 1); - wcsncat(full_path, stem, stem_len); - } - if (wcsstr(full_path, L"Classes\\Extensions\\ContractId\\Windows.BackgroundTasks\\PackageId") || - wcsstr(full_path, L"CloudStore\\Store\\Cache\\") || - wcsstr(full_path, - L"CurrentVersion\\Authentication\\LogonUI\\Notifications\\BackgroundCapability") || - wcsstr(full_path, L"CurrentVersion\\CloudStore\\Store\\DefaultAccount\\Current\\") || - wcsstr(full_path, L"Explorer\\ComDlg32\\CIDSizeMRU") || - wcsstr(full_path, L"Explorer\\ComDlg32\\FirstFolder") || - wcsstr(full_path, L"Explorer\\ComDlg32\\LastVisitedPidlMRU") || - wcsstr(full_path, L"Explorer\\ComDlg32\\OpenSavePidlMRU") || - wcsstr(full_path, L"IrisService\\Cache") || - wcsstr(full_path, L"Microsoft\\Windows\\Shell\\Bags") || - wcsstr(full_path, L"Windows\\Shell\\BagMRU")) { - if (debug) { - fwprintf(stderr, L"%ls: Skipping %ls due to filter.\n", prior_stem, stem); - } - free(full_path); - return; - } - if (RegOpenKeyEx(hk, stem, 0, KEY_READ, &hk_out) == ERROR_SUCCESS) { - DWORD n_sub_keys = 0; - DWORD n_values = 0; - LSTATUS ret_code = RegQueryInfoKey(hk_out, - nullptr, - nullptr, - nullptr, - &n_sub_keys, - nullptr, - nullptr, - &n_values, - nullptr, - nullptr, - nullptr, - nullptr); - if (n_sub_keys) { - size_t ach_key_len = 0; - wchar_t *ach_key = calloc(MAX_KEY_LENGTH, WL); - if (!ach_key) { - abort(); - } - unsigned i; - for (i = 0; i < n_sub_keys; i++) { - ach_key_len = MAX_KEY_LENGTH; - wmemset(ach_key, L'\0', MAX_KEY_LENGTH); - ret_code = RegEnumKeyEx( - hk_out, i, ach_key, (LPDWORD)&ach_key_len, nullptr, nullptr, nullptr, nullptr); - if (ret_code == ERROR_SUCCESS) { - write_reg_commands( - hk_out, ach_key, max_depth, depth + 1, out_fp, full_path, debug); - } else { - if (debug) { - fprintf(stderr, - "%ls: Skipping %ls because RegEnumKeyEx() failed.\n", - prior_stem, - full_path); - } - } - } - free(ach_key); - } else if (debug) { - fwprintf(stderr, L"%ls: No subkeys in %ls.\n", prior_stem, stem); - } - if (n_values) { - do_write_reg_commands(out_fp, hk_out, n_values, full_path, debug); - } else if (debug) { - fwprintf(stderr, L"%ls: No values in %ls.\n", prior_stem, stem); - } - RegCloseKey(hk_out); - } else { - if (debug) { - fwprintf(stderr, L"%ls: Skipping %ls. Does the location exist?\n", prior_stem, stem); - } - } - free(full_path); -} +bool debug_print_enabled = false; -int save_preferences(bool commit, - const wchar_t *deploy_key, - const wchar_t *output_dir, - int max_depth, - HKEY hk, - const wchar_t *specified_path, - bool debug) { - wchar_t full_output_dir[MAX_PATH]; - if (_wfullpath(full_output_dir, output_dir, MAX_PATH) == nullptr) { - abort(); +static inline void print_leaks() { +#ifndef ENABLE_VLD + if (debug_print_enabled) { + _CrtDumpMemoryLeaks(); } - if (debug) { - fwprintf(stderr, L"Output directory: %ls\n", full_output_dir); - } - SHCreateDirectoryEx(nullptr, full_output_dir, nullptr); - PathAppend(full_output_dir, L"exec-reg.bat"); - full_output_dir[MAX_PATH - 1] = L'\0'; - wchar_t full_deploy_key_path[MAX_PATH]; - if (deploy_key) { - if (_wfullpath(full_deploy_key_path, deploy_key, MAX_PATH) == nullptr) { - abort(); - } - if (debug) { - fwprintf(stderr, L"Deploy key: %ls\n", full_deploy_key_path); - } - } - full_output_dir[MAX_PATH - 1] = '\0'; - HANDLE out_fp = CreateFile(full_output_dir, - GENERIC_READ | GENERIC_WRITE, - 0, - nullptr, - CREATE_ALWAYS, - FILE_ATTRIBUTE_NORMAL, - nullptr); - write_reg_commands(hk, - nullptr, - max_depth, - 0, - out_fp, - hk == HKEY_CLASSES_ROOT ? L"HKCR" : - hk == HKEY_CURRENT_CONFIG ? L"HKCC" : - hk == HKEY_CURRENT_USER ? L"HKCU" : - hk == HKEY_LOCAL_MACHINE ? L"HKLM" : - hk == HKEY_USERS ? L"HKU" : - hk == HKEY_DYN_DATA ? L"HKDD" : - specified_path, - debug); - CloseHandle(out_fp); - bool has_git = _wspawnlp(P_WAIT, L"git.exe", L"git", L"--version", nullptr) >= 0; - if (commit) { - if (!has_git) { - if (debug) { - fwprintf(stderr, - L"Wanted to commit but git.exe is not in PATH or failed to run.\n"); - } - return 0; - } - if (debug) { - fwprintf(stderr, L"Committing changes.\n"); - } - size_t work_tree_arg_len = wcslen(output_dir) + wcslen(L"--work-tree=") + 1; - wchar_t *work_tree_arg = calloc(work_tree_arg_len, WL); - if (!work_tree_arg) { - abort(); - } - wmemset(work_tree_arg, L'\0', work_tree_arg_len); - _snwprintf(work_tree_arg, work_tree_arg_len, L"--work-tree=%ls", output_dir); - size_t git_dir_len = wcslen(output_dir) + wcslen(L"\\.git") + 1; - wchar_t *git_dir = calloc(git_dir_len, WL); - if (!git_dir) { - abort(); - } - _snwprintf(git_dir, git_dir_len, L"%ls\\.git", output_dir); - git_dir[git_dir_len - 1] = L'\0'; - if (!dir_exists(git_dir)) { - wchar_t *cwd = calloc(MAX_PATH, WL); - if (!cwd) { - abort(); - } - wmemset(cwd, L'\0', MAX_PATH); - if (!_wgetcwd(cwd, MAX_PATH)) { - abort(); - } - if (_wchdir(output_dir) != 0) { - abort(); - } - if (_wspawnlp(P_WAIT, L"git.exe", L"git", L"init", L"--quiet", nullptr) != 0) { - abort(); - } - if (_wchdir(cwd) != 0) { - abort(); - } - } - size_t git_dir_arg_len = git_dir_len + wcslen(L"--git-dir=") + 1; - wchar_t *git_dir_arg = calloc(git_dir_arg_len, WL); - if (!git_dir_arg) { - abort(); - } - wmemset(git_dir_arg, L'\0', git_dir_arg_len); - _snwprintf(git_dir_arg, git_dir_arg_len, L"--git-dir=%ls", git_dir); - free(git_dir); - git_dir_arg[git_dir_arg_len - 1] = L'\0'; - if (_wspawnlp( - P_WAIT, L"git.exe", L"git", git_dir_arg, work_tree_arg, L"add", L".", nullptr) != - 0) { - abort(); - } - size_t time_needed_size = - (size_t)GetTimeFormat(LOCALE_USER_DEFAULT, 0, nullptr, nullptr, nullptr, 0); - if (!time_needed_size) { - abort(); - } - wchar_t *time_buf = calloc(time_needed_size, WL); - if (!GetTimeFormat( - LOCALE_USER_DEFAULT, 0, nullptr, nullptr, time_buf, (int)time_needed_size)) { - abort(); - } - size_t date_needed_size = - (size_t)GetDateFormat(LOCALE_USER_DEFAULT, 0, nullptr, nullptr, nullptr, 0); - if (!date_needed_size) { - abort(); - } - wchar_t *date_buf = calloc(date_needed_size, WL); - if (!GetDateFormat( - LOCALE_USER_DEFAULT, 0, nullptr, nullptr, date_buf, (int)date_needed_size)) { - abort(); - } - size_t needed_size = - wcslen(AUTOMATIC_COMMIT_MESSAGE_PREFIX) + 3 + time_needed_size + date_needed_size; - wchar_t *message_buf = calloc(needed_size, WL); - if (!message_buf) { - abort(); - } - wmemset(message_buf, L'\0', needed_size); - _snwprintf(message_buf, - needed_size, - L"\"%ls%ls %ls\"", - AUTOMATIC_COMMIT_MESSAGE_PREFIX, - date_buf, - time_buf); - free(date_buf); - free(time_buf); - if (_wspawnlp(P_WAIT, - L"git.exe", - L"git", - git_dir_arg, - work_tree_arg, - L"commit", - L"--no-gpg-sign", - L"--quiet", - L"--no-verify", - L"\"--author=winprefs \"", - L"-m", - message_buf, - nullptr) != 0) { - abort(); - } - free(message_buf); - if (deploy_key) { - size_t ssh_command_len = 68 + wcslen(full_deploy_key_path) + 3; - wchar_t *ssh_command = calloc(ssh_command_len, WL); - if (!ssh_command) { - abort(); - } - wmemset(ssh_command, L'\0', ssh_command_len); - _snwprintf( - ssh_command, - ssh_command_len, - L"\"ssh -i %ls -F nul -o UserKnownHostsFile=nul -o StrictHostKeyChecking=no\"", - full_deploy_key_path); - if (_wspawnlp(P_WAIT, - L"git.exe", - L"git", - git_dir_arg, - work_tree_arg, - L"config", - L"core.sshCommand", - ssh_command, - nullptr) != 0) { - abort(); - } - wchar_t *branch_arg = - get_git_branch(git_dir_arg, git_dir_arg_len, work_tree_arg, work_tree_arg_len); - if (debug) { - fwprintf( - stderr, - L"git.exe \"%ls\" \"%ls\" push -u --porcelain --no-signed origin origin %ls\n", - git_dir_arg, - work_tree_arg, - branch_arg); - } - if (_wspawnlp(P_WAIT, - L"git.exe", - L"git", - git_dir_arg, - work_tree_arg, - L"push", - L"-u", - L"--porcelain", - L"--no-signed", - L"origin", - L"origin", - branch_arg, - nullptr) != 0) { - abort(); - } - free(ssh_command); - free(branch_arg); - } - free(git_dir_arg); - free(work_tree_arg); - } - return 0; -} - -HKEY get_top_key(wchar_t *reg_path) { - if (!_wcsnicmp(reg_path, L"HKCR", 4) || !_wcsnicmp(reg_path, L"HKEY_CLASSES_ROOT", 17)) { - return HKEY_CLASSES_ROOT; - } - if (!_wcsnicmp(reg_path, L"HKLM", 4) || !wcsnicmp(reg_path, L"HKEY_LOCAL_MACHINE", 18)) { - return HKEY_LOCAL_MACHINE; - } - if (!_wcsnicmp(reg_path, L"HKCC", 4) || !_wcsnicmp(reg_path, L"HKEY_CURRENT_CONFIG", 19)) { - return HKEY_CURRENT_CONFIG; - } - if (!_wcsnicmp(reg_path, L"HKU", 3) || !_wcsnicmp(reg_path, L"HKEY_USERS", 10)) { - return HKEY_USERS; - } - if (!_wcsnicmp(reg_path, L"HKDD", 4) || !_wcsnicmp(reg_path, L"HKEY_DYN_DATA", 13)) { - return HKEY_DYN_DATA; - } - return nullptr; +#endif } +//! Entry point. int wmain(int argc, wchar_t *argv[]) { (void)argc; - wchar_t *argv0 = argv[0]; + HKEY starting_key = HKEY_CURRENT_USER; bool commit = false; bool debug = false; - wchar_t *output_dir = nullptr; - wchar_t *deploy_key = nullptr; int max_depth = 20; - HKEY starting_key = HKEY_CURRENT_USER; + wchar_t *argv0 = argv[0]; + wchar_t *deploy_key = nullptr; + wchar_t *output_dir = nullptr; + wchar_t *output_file = nullptr; ARG_BEGIN { if (ARG_LONG("deploy-key")) case 'K': { @@ -480,6 +55,9 @@ int wmain(int argc, wchar_t *argv[]) { debug = true; ARG_FLAG(); } + else if (ARG_LONG("output-file")) case 'f': { + output_file = ARG_VAL(); + } else if (ARG_LONG("output-dir")) case 'o': { output_dir = ARG_VAL(); } @@ -504,6 +82,7 @@ int wmain(int argc, wchar_t *argv[]) { puts(" -d, --debug Enable debug logging."); puts(" -m, --max-depth=INT Set maximum depth."); puts(" -o, --output-dir Output directory."); + puts(" -f, --output-file Output filename."); puts(" -h, --help Display this help and exit."); return EXIT_SUCCESS; } @@ -538,45 +117,15 @@ int wmain(int argc, wchar_t *argv[]) { wchar_t *subkey = wcschr(reg_path, L'\\') + 1; if (RegOpenKeyEx(top_key, subkey, 0, KEY_READ, &starting_key) != ERROR_SUCCESS) { // See if it's a full path to value - wchar_t *last_backslash = wcsrchr(reg_path, '\\'); - wchar_t *value_name_p = last_backslash + 1; - size_t value_name_len = wcslen(value_name_p); - wchar_t *value_name = calloc(value_name_len, WL); - if (!value_name) { - abort(); - } - wmemcpy(value_name, value_name_p, value_name_len); - *last_backslash = L'\0'; - if (RegOpenKeyEx(top_key, subkey, 0, KEY_READ, &starting_key) != ERROR_SUCCESS) { - free(value_name); - fwprintf(stderr, L"Invalid subkey: '%ls'.\n", subkey); - return EXIT_FAILURE; - } - size_t buf_size = 8192; - char *data = malloc(buf_size); - DWORD reg_type = REG_NONE; - LSTATUS ret = RegQueryValueEx( - starting_key, value_name, NULL, ®_type, (LPBYTE)data, (LPDWORD)&buf_size); - if (ret == ERROR_MORE_DATA) { - free(data); - fwprintf(stderr, L"Value too large (%ls\\%ls).\n", subkey, value_name); - return EXIT_FAILURE; - } - if (ret != ERROR_SUCCESS) { - free(data); - fwprintf(stderr, L"Invalid value name '%ls'.\n", value_name); - return EXIT_FAILURE; - } - do_write_reg_command(stdout, reg_path, value_name, data, buf_size, reg_type, debug); - free(data); - return EXIT_SUCCESS; + return export_single_value(reg_path, top_key) ? EXIT_SUCCESS : EXIT_FAILURE; } } } if (!output_dir) { output_dir = calloc(MAX_PATH, WL); if (!output_dir) { - abort(); + fprintf(stderr, "Failed to allocate memory.\n"); + return EXIT_FAILURE; } wmemset(output_dir, L'\0', MAX_PATH); if (SUCCEEDED(SHGetFolderPath(nullptr, CSIDL_APPDATA, nullptr, 0, output_dir))) { @@ -584,8 +133,33 @@ int wmain(int argc, wchar_t *argv[]) { } output_dir[MAX_PATH - 1] = L'\0'; } - int exit_code = - save_preferences(commit, deploy_key, output_dir, max_depth, starting_key, reg_path, debug); + debug_print_enabled = debug; + bool success = save_preferences(commit, + deploy_key, + output_dir, + output_file ? output_file : L"exec-reg.bat", + max_depth, + starting_key, + reg_path); free(output_dir); - return exit_code; + if (!success) { + fwprintf(stderr, L"Error occurred. Possibilities:\n"); + DWORD last_win_error = GetLastError(); + wchar_t p_message_buf[8192]; + FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + last_win_error, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPTSTR)&p_message_buf, + 8192, + NULL); + fprintf(stderr, "POSIX (%d): %s\n", errno, strerror(errno)); + fwprintf(stderr, L"Windows (%d): %ls", last_win_error, p_message_buf); + print_leaks(); + return EXIT_FAILURE; + } +#ifndef ENABLE_VLD + print_leaks(); +#endif + return EXIT_SUCCESS; } diff --git a/native/reg_command.c b/native/reg_command.c index 2476690..4cd5474 100644 --- a/native/reg_command.c +++ b/native/reg_command.c @@ -10,6 +10,7 @@ #endif #include "constants.h" +#include "debug.h" #include "macros.h" #include "reg_command.h" #include "shell.h" @@ -32,7 +33,7 @@ wchar_t *fix_v_param(const wchar_t *prop, size_t prop_len, bool *heap) { size_t escaped_len = (7 + wcslen(escaped)) * WL; wchar_t *out = calloc(7 + wcslen(escaped), WL); if (!out) { - abort(); + return nullptr; } memset(out, 0, escaped_len); _snwprintf(out, escaped_len, L"/v \"%ls\" ", escaped); @@ -47,7 +48,7 @@ wchar_t *convert_data_for_reg(DWORD reg_type, const char *data, size_t data_len) size_t new_len = n_bin_chars + 1; wchar_t *bin = calloc(new_len, WL); if (!bin) { - abort(); + return nullptr; } wmemset(bin, L'\0', new_len); for (i = 0; i < data_len; i++) { @@ -59,7 +60,7 @@ wchar_t *convert_data_for_reg(DWORD reg_type, const char *data, size_t data_len) size_t s_size = new_len + 5; wchar_t *out = calloc(s_size, WL); if (!out) { - abort(); + return nullptr; } wmemset(out, L'\0', s_size); _snwprintf(out, s_size, L" /d %ls ", bin); @@ -74,7 +75,7 @@ wchar_t *convert_data_for_reg(DWORD reg_type, const char *data, size_t data_len) size_t s_size = (wcslen(s) + 8); wchar_t *out = calloc(s_size, WL); if (!out) { - abort(); + return nullptr; } memset(out, 0, s_size); _snwprintf(out, s_size, L" /d \"%ls\" ", s); @@ -85,7 +86,7 @@ wchar_t *convert_data_for_reg(DWORD reg_type, const char *data, size_t data_len) size_t s_size = 20; wchar_t *out = calloc(s_size, WL); if (!out) { - abort(); + return nullptr; } memset(out, 0, s_size); if (reg_type == REG_DWORD) { @@ -95,16 +96,16 @@ wchar_t *convert_data_for_reg(DWORD reg_type, const char *data, size_t data_len) } return out; } + errno = EINVAL; return nullptr; } -void do_write_reg_command(HANDLE out_fp, +bool do_write_reg_command(HANDLE out_fp, const wchar_t *full_path, const wchar_t *prop, const char *value, size_t data_len, - unsigned long type, - bool debug) { + unsigned long type) { wchar_t *escaped_d = convert_data_for_reg(type, value, data_len); wchar_t *escaped_reg_key = escape_for_batch(full_path, wcslen(full_path)); bool v_heap = false; @@ -136,7 +137,7 @@ void do_write_reg_command(HANDLE out_fp, } wchar_t *out = calloc(CMD_MAX_COMMAND_LENGTH, WL); if (!out) { - abort(); + return nullptr; } wmemset(out, L'\0', CMD_MAX_COMMAND_LENGTH); int wrote = _snwprintf(out, @@ -158,7 +159,7 @@ void do_write_reg_command(HANDLE out_fp, 0, NULL, NULL); - char *mb_out = malloc(req_size + 1); + char *mb_out = malloc(req_size); WideCharToMultiByte(CP_UTF8, IsWindowsVistaOrGreater() ? _WC_ERR_INVALID_CHARS : 0, out, @@ -167,14 +168,12 @@ void do_write_reg_command(HANDLE out_fp, (int)req_size, NULL, NULL); - mb_out[req_size] = '\n'; + mb_out[req_size - 1] = '\n'; DWORD written; - WriteFile(out_fp, mb_out, (DWORD)(req_size + 1), &written, nullptr); + WriteFile(out_fp, mb_out, (DWORD)(req_size), &written, nullptr); free(mb_out); } else { - if (debug) { - fwprintf(stderr, L"%ls %ls: Skipping due to length of command.\n", full_path, prop); - } + debug_print(L"%ls %ls: Skipping due to length of command.\n", full_path, prop); } if (escaped_d) { free(escaped_d); @@ -184,20 +183,27 @@ void do_write_reg_command(HANDLE out_fp, } free(out); free(escaped_reg_key); + return true; } -void do_write_reg_commands( - HANDLE out_fp, HKEY hk, unsigned n_values, const wchar_t *full_path, bool debug) { +bool do_write_reg_commands(HANDLE out_fp, HKEY hk, unsigned n_values, const wchar_t *full_path) { + if (n_values == 0) { + return true; + } + if (!out_fp || !full_path) { + errno = EINVAL; + return false; + } size_t data_len; DWORD i; DWORD reg_type; size_t value_len; wchar_t *value = calloc(MAX_VALUE_NAME, WL); + if (!value) { + return false; + } int ret = ERROR_SUCCESS; char data[8192]; - if (!out_fp || !value) { - abort(); - } for (i = 0; i < n_values; i++) { data_len = sizeof(data); wmemset(value, L'\0', MAX_VALUE_NAME); @@ -211,7 +217,107 @@ void do_write_reg_commands( if (ret == ERROR_NO_MORE_ITEMS) { break; } - do_write_reg_command(out_fp, full_path, value, data, data_len, reg_type, debug); + do_write_reg_command(out_fp, full_path, value, data, data_len, reg_type); } free(value); + return true; +} + +bool write_reg_commands(HKEY hk, + const wchar_t *stem, + int max_depth, + int depth, + HANDLE out_fp, + const wchar_t *prior_stem) { + if (depth >= max_depth) { + debug_print(L"%ls: Skipping %ls due to depth limit of %d.\n", prior_stem, stem, max_depth); + return true; + } + HKEY hk_out; + size_t full_path_len = WL * MAX_KEY_LENGTH; + wchar_t *full_path = calloc(MAX_KEY_LENGTH, WL); + if (!full_path) { + return false; + } + wmemset(full_path, L'\0', MAX_KEY_LENGTH); + size_t prior_stem_len = wcslen(prior_stem) * WL; + size_t stem_len = stem ? wcslen(stem) : 0; + if ((prior_stem_len + (stem_len * WL) + 2) > (full_path_len - 2)) { + debug_print(L"%ls: Skipping %ls because of length limitation.\n", prior_stem, stem); + free(full_path); + return true; + } + memcpy(full_path, prior_stem, prior_stem_len); + if (stem) { + wcsncat(full_path, L"\\", 1); + wcsncat(full_path, stem, stem_len); + } + if (wcsstr(full_path, L"Classes\\Extensions\\ContractId\\Windows.BackgroundTasks\\PackageId") || + wcsstr(full_path, L"CloudStore\\Store\\Cache\\") || + wcsstr(full_path, + L"CurrentVersion\\Authentication\\LogonUI\\Notifications\\BackgroundCapability") || + wcsstr(full_path, L"CurrentVersion\\CloudStore\\Store\\DefaultAccount\\Current\\") || + wcsstr(full_path, L"Explorer\\ComDlg32\\CIDSizeMRU") || + wcsstr(full_path, L"Explorer\\ComDlg32\\FirstFolder") || + wcsstr(full_path, L"Explorer\\ComDlg32\\LastVisitedPidlMRU") || + wcsstr(full_path, L"Explorer\\ComDlg32\\OpenSavePidlMRU") || + wcsstr(full_path, L"IrisService\\Cache") || + wcsstr(full_path, L"Microsoft\\Windows\\Shell\\Bags") || + wcsstr(full_path, L"Windows\\Shell\\BagMRU")) { + debug_print(L"%ls: Skipping %ls due to filter.\n", prior_stem, stem); + free(full_path); + return true; + } + if (RegOpenKeyEx(hk, stem, 0, KEY_READ, &hk_out) == ERROR_SUCCESS) { + DWORD n_sub_keys = 0; + DWORD n_values = 0; + LSTATUS ret_code = RegQueryInfoKey(hk_out, + nullptr, + nullptr, + nullptr, + &n_sub_keys, + nullptr, + nullptr, + &n_values, + nullptr, + nullptr, + nullptr, + nullptr); + if (n_sub_keys) { + size_t ach_key_len = 0; + wchar_t *ach_key = calloc(MAX_KEY_LENGTH, WL); + if (!ach_key) { + return false; + } + unsigned i; + for (i = 0; i < n_sub_keys; i++) { + ach_key_len = MAX_KEY_LENGTH; + wmemset(ach_key, L'\0', MAX_KEY_LENGTH); + ret_code = RegEnumKeyEx( + hk_out, i, ach_key, (LPDWORD)&ach_key_len, nullptr, nullptr, nullptr, nullptr); + if (ret_code == ERROR_SUCCESS) { + write_reg_commands(hk_out, ach_key, max_depth, depth + 1, out_fp, full_path); + } else { + debug_print(L"%ls: Skipping %ls because RegEnumKeyEx() failed.\n", + prior_stem, + full_path); + } + } + free(ach_key); + } else { + debug_print(L"%ls: No subkeys in %ls.\n", prior_stem, stem); + } + if (n_values) { + if (!do_write_reg_commands(out_fp, hk_out, n_values, full_path)) { + return false; + } + } else { + debug_print(L"%ls: No values in %ls.\n", prior_stem, stem); + } + RegCloseKey(hk_out); + } else { + debug_print(L"%ls: Skipping %ls. Does the location exist?\n", prior_stem, stem); + } + free(full_path); + return true; } diff --git a/native/reg_command.h b/native/reg_command.h index 85161eb..75cfac2 100644 --- a/native/reg_command.h +++ b/native/reg_command.h @@ -1,3 +1,4 @@ +/** \file */ #ifndef REG_COMMAND_H #define REG_COMMAND_H @@ -5,14 +6,28 @@ #include "macros.h" -void do_write_reg_command(HANDLE out_fp, +bool do_write_reg_command(HANDLE out_fp, const wchar_t *full_path, const wchar_t *prop, const char *value, size_t value_len, - unsigned long type, - bool debug); -void do_write_reg_commands( - HANDLE out_fp, HKEY hk, unsigned n_values, const wchar_t *prior_stem, bool debug); + unsigned long type); +bool do_write_reg_commands(HANDLE out_fp, HKEY hk, unsigned n_values, const wchar_t *prior_stem); +//! Starts the registry exporting process. +/*! + \param hk Key handle. + \param stem Subkey. + \param max_depth Maximum key depth. + \param depth Current depth. Used internally. Should be `0` at start. + \param out_fp File handle. + \param prior_stem Last subkey. Used internally. Should be `NULL`. + \return `true` if successful, `false` otherwise. + */ +bool write_reg_commands(HKEY hk, + const wchar_t *stem, + int max_depth, + int depth, + HANDLE out_fp, + const wchar_t *prior_stem); #endif // REG_COMMAND_H diff --git a/native/registry.c b/native/registry.c new file mode 100644 index 0000000..106ed2b --- /dev/null +++ b/native/registry.c @@ -0,0 +1,151 @@ +#include + +#include +#include + +#include "constants.h" +#include "debug.h" +#include "git.h" +#include "reg_command.h" +#include "registry.h" + +HKEY get_top_key(wchar_t *reg_path) { + if (!_wcsnicmp(reg_path, L"HKCR", 4) || !_wcsnicmp(reg_path, L"HKEY_CLASSES_ROOT", 17)) { + return HKEY_CLASSES_ROOT; + } + if (!_wcsnicmp(reg_path, L"HKLM", 4) || !wcsnicmp(reg_path, L"HKEY_LOCAL_MACHINE", 18)) { + return HKEY_LOCAL_MACHINE; + } + if (!_wcsnicmp(reg_path, L"HKCC", 4) || !_wcsnicmp(reg_path, L"HKEY_CURRENT_CONFIG", 19)) { + return HKEY_CURRENT_CONFIG; + } + if (!_wcsnicmp(reg_path, L"HKU", 3) || !_wcsnicmp(reg_path, L"HKEY_USERS", 10)) { + return HKEY_USERS; + } + if (!_wcsnicmp(reg_path, L"HKDD", 4) || !_wcsnicmp(reg_path, L"HKEY_DYN_DATA", 13)) { + return HKEY_DYN_DATA; + } + return nullptr; +} + +static inline bool create_dir_wrapper(wchar_t *folder) { + if (!CreateDirectory(folder, NULL)) { + DWORD err = GetLastError(); + if (err != ERROR_ALREADY_EXISTS) { + return false; + } + } + return true; +} + +static inline bool create_dir_recursive(wchar_t *path) { + wchar_t folder[MAX_PATH]; + wchar_t *end; + wmemset(folder, L'\0', MAX_PATH); + end = wcschr(path, L'\\'); + while (end != NULL) { + wcsncpy(folder, path, (size_t)(end - path + 1)); + if (!create_dir_wrapper(folder)) { + return false; + } + end = wcschr(++end, L'\\'); + } + if (!create_dir_wrapper(path)) { + return false; + } + return true; +} + +bool save_preferences(bool commit, + const wchar_t *deploy_key, + const wchar_t *output_dir, + const wchar_t *output_file, + int max_depth, + HKEY hk, + const wchar_t *specified_path) { + wchar_t full_output_dir[MAX_PATH]; + bool writing_to_stdout = !wcscmp(L"-", output_file); + if (!_wfullpath(full_output_dir, output_dir, MAX_PATH)) { + return false; + } + debug_print(L"Output directory: %ls\n", full_output_dir); + if (!writing_to_stdout && !create_dir_recursive(full_output_dir)) { + return false; + } + PathAppend(full_output_dir, output_file); + full_output_dir[MAX_PATH - 1] = L'\0'; + HANDLE out_fp = !writing_to_stdout ? CreateFile(full_output_dir, + GENERIC_READ | GENERIC_WRITE, + 0, + nullptr, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + nullptr) : + GetStdHandle(STD_OUTPUT_HANDLE); + if (out_fp == INVALID_HANDLE_VALUE) { + return false; + } + write_reg_commands(hk, + nullptr, + max_depth, + 0, + out_fp, + hk == HKEY_CLASSES_ROOT ? L"HKCR" : + hk == HKEY_CURRENT_CONFIG ? L"HKCC" : + hk == HKEY_CURRENT_USER ? L"HKCU" : + hk == HKEY_LOCAL_MACHINE ? L"HKLM" : + hk == HKEY_USERS ? L"HKU" : + hk == HKEY_DYN_DATA ? L"HKDD" : + specified_path); + if (!writing_to_stdout) { + CloseHandle(out_fp); + } + if (commit && !writing_to_stdout) { + git_commit(output_dir, deploy_key); + } + return true; +} + +bool export_single_value(const wchar_t *reg_path, HKEY top_key) { + wchar_t *first_backslash = wcschr(reg_path, L'\\'); + if (!first_backslash) { + return false; + } + wchar_t *subkey = first_backslash + 1; + HKEY starting_key = HKEY_CURRENT_USER; + wchar_t *last_backslash = wcsrchr(reg_path, '\\'); + wchar_t *value_name_p = last_backslash + 1; + size_t value_name_len = wcslen(value_name_p); + wchar_t *value_name = calloc(value_name_len, WL); + if (!value_name) { + return false; + } + wmemcpy(value_name, value_name_p, value_name_len); + *last_backslash = L'\0'; + if (RegOpenKeyEx(top_key, subkey, 0, KEY_READ, &starting_key) != ERROR_SUCCESS) { + free(value_name); + debug_print(L"Invalid subkey: '%ls'.\n", subkey); + return false; + } + size_t buf_size = 8192; + char *data = malloc(buf_size); + DWORD reg_type = REG_NONE; + LSTATUS ret = RegQueryValueEx( + starting_key, value_name, NULL, ®_type, (LPBYTE)data, (LPDWORD)&buf_size); + if (ret == ERROR_MORE_DATA) { + free(data); + debug_print(L"Value too large (%ls\\%ls).\n", subkey, value_name); + return false; + } + if (ret != ERROR_SUCCESS) { + free(data); + debug_print(L"Invalid value name '%ls'.\n", value_name); + return false; + } + if (!do_write_reg_command( + GetStdHandle(STD_OUTPUT_HANDLE), reg_path, value_name, data, buf_size, reg_type)) { + return false; + } + free(data); + return true; +} diff --git a/native/registry.h b/native/registry.h new file mode 100644 index 0000000..3ce9ebd --- /dev/null +++ b/native/registry.h @@ -0,0 +1,39 @@ +/** \file */ +#ifndef REGISTRY_H +#define REGISTRY_H + +#include + +//! Gets the `HKEY` pointer for the first part of a registry path. +/*! + \param reg_path Full registry path. + \return Pointer to root key handle. + */ +HKEY get_top_key(wchar_t *reg_path); +//! Starts the registry exporting process. +/*! + \param commit If the changes should be commited with Git. Does not apply if output directory is `-` + meaning standard output. + \param deploy_key Relative path to SSH deploy key. + \param output_dir Output directory. + \param output_file Output filename. + \param max_depth Maximum depth to traverse. + \param hk Starting registry key. + \param specified_path A direct path to a key (and not a value name). + \return Pointer to string with branch name. Must be freed on caller side. + */ +bool save_preferences(bool commit, + const wchar_t *deploy_key, + const wchar_t *output_dir, + const wchar_t *output_file, + int max_depth, + HKEY hk, + const wchar_t *specified_path); +//! Exports a single registry key to a `reg add` command. +/*! + \param reg_path Registry path to a key or a value name. + \param top_key Handle to the top key (such as `HKEY_CURRENT_USER`). + \return `true` if successful, `false` otherwise. + */ +bool export_single_value(const wchar_t *reg_path, HKEY top_key); +#endif // REGISTRY_H diff --git a/native/shell.c b/native/shell.c index 0f11081..6bced53 100644 --- a/native/shell.c +++ b/native/shell.c @@ -10,6 +10,7 @@ wchar_t *escape_for_batch(const wchar_t *input, size_t n_chars) { if (input == nullptr || n_chars == 0) { + errno = EINVAL; return nullptr; } unsigned i, j; @@ -24,7 +25,7 @@ wchar_t *escape_for_batch(const wchar_t *input, size_t n_chars) { } wchar_t *out = calloc(new_n_chars + 1, WL); if (!out) { - abort(); + return nullptr; } if (n_chars == new_n_chars) { wmemcpy(out, input, new_n_chars + 1); diff --git a/native/shell.h b/native/shell.h index f394cf6..858840d 100644 --- a/native/shell.h +++ b/native/shell.h @@ -1,8 +1,16 @@ +/** \file */ #ifndef SHELL_H #define SHELL_H #include -wchar_t *escape_for_batch(const wchar_t *input_str, size_t input_len); +//! Escapes a string for a batch file. +/*! + \param input Input string. + \param n_chars Length of the input string in word size. + \return Pointer to escaped string. Must be freed on caller side. + \ingroup A + */ +wchar_t *escape_for_batch(const wchar_t *input, size_t n_chars); #endif // SHELL_H diff --git a/native/tests/CMakeLists.txt b/native/tests/CMakeLists.txt index 65cbee2..971cfcb 100644 --- a/native/tests/CMakeLists.txt +++ b/native/tests/CMakeLists.txt @@ -40,8 +40,9 @@ add_executable( target_compile_definitions(winprefs-tests PRIVATE UNICODE _UNICODE) get_property(IS_64BIT GLOBAL PROPERTY FIND_LIBRARY_USE_LIB64_PATHS) target_compile_definitions(winprefs-tests PRIVATE _WIN32_WINNT=0x600) -target_compile_options(winprefs-tests PRIVATE $<$:${GCC_CLANG_DEBUG_C_FLAGS}> - ${GCC_CLANG_SHARED_C_FLAGS}) +target_compile_options( + winprefs-tests PRIVATE $<$:${GCC_CLANG_DEBUG_C_FLAGS}> + ${GCC_CLANG_SHARED_C_FLAGS}) target_include_directories(winprefs-tests PRIVATE ${CMAKE_SOURCE_DIR}/native) target_link_libraries(winprefs-tests PRIVATE cmocka::cmocka kernel32 shlwapi) target_link_options(winprefs-tests PRIVATE -municode) diff --git a/native/tests/calloc.c b/native/tests/calloc.c deleted file mode 100644 index 0d71b4a..0000000 --- a/native/tests/calloc.c +++ /dev/null @@ -1,8 +0,0 @@ -#include "calloc.h" - -void *__wrap_calloc(void *buf, size_t n_size) { - if (want_calloc_abort) { - return nullptr; - } - return __real_calloc(buf, n_size); -} diff --git a/native/tests/calloc.h b/native/tests/calloc.h deleted file mode 100644 index 4301fff..0000000 --- a/native/tests/calloc.h +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef CALLOC_H -#define CALLOC_H - -#include - -#include "macros.h" - -extern bool want_calloc_abort; -void *__real_calloc(void *buf, size_t n_size); - -#endif // CALLOC_H diff --git a/native/tests/main.c b/native/tests/main.c index 368129b..c131cf4 100644 --- a/native/tests/main.c +++ b/native/tests/main.c @@ -3,14 +3,11 @@ #include "tests.h" -bool want_calloc_abort = false; - const struct CMUnitTest shell_tests[] = { cmocka_unit_test(test_escape_returns_null_on_null_input_or_zero_chars), cmocka_unit_test(test_escape_handles_special_characters), cmocka_unit_test(test_escape_handles_reg_multi_sz), cmocka_unit_test(test_escape_returns_same_when_escaping_unnecessary), -// cmocka_unit_test(test_escape_aborts_if_calloc_fails), }; int wmain(int argc, wchar_t *argv[]) { diff --git a/native/tests/test_shell.c b/native/tests/test_shell.c index 1e04fa1..43b9da4 100644 --- a/native/tests/test_shell.c +++ b/native/tests/test_shell.c @@ -37,10 +37,3 @@ void test_escape_returns_same_when_escaping_unnecessary(void **state) { assert_memory_equal(L"abcdef", out, 6); free(out); } - -void test_escape_aborts_if_calloc_fails(void **state) { - (void)state; - want_calloc_abort = true; - assert_null(escape_for_batch(L"input", 1)); - want_calloc_abort = false; -} diff --git a/native/tests/tests.h b/native/tests/tests.h index 1ad506b..52c3357 100644 --- a/native/tests/tests.h +++ b/native/tests/tests.h @@ -1,7 +1,6 @@ #ifndef TESTS_H #define TESTS_H -void test_escape_aborts_if_calloc_fails(void **state); void test_escape_handles_reg_multi_sz(void **state); void test_escape_handles_special_characters(void **state); void test_escape_returns_null_on_null_input_or_zero_chars(void **state);