diff options
author | Jan200101 <sentrycraft123@gmail.com> | 2022-06-06 22:03:57 +0200 |
---|---|---|
committer | Jan200101 <sentrycraft123@gmail.com> | 2022-06-08 20:06:12 +0200 |
commit | d5d61d18c89af3f6743b7c56774eebdfdcc87b2c (patch) | |
tree | b4299c1af7e194e9083d4de1bce382102ea46e95 | |
download | OFQT-d5d61d18c89af3f6743b7c56774eebdfdcc87b2c.tar.gz OFQT-d5d61d18c89af3f6743b7c56774eebdfdcc87b2c.zip |
Release 0.1.00.1.0
43 files changed, 3740 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1b2211d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build* diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..de44080 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.12) + +if(CMAKE_POLICY_DEFAULT_CMP0017 OR CMAKE_POLICY_DEFAULT_CMP0020) + # touch these to remove warnings +endif() + +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Release" CACHE STRING + "Choose the type of build, options are: None Debug Release RelWithDebInfo MinSizeRel." FORCE) +endif() + +project(OFQT VERSION 0.1.0 LANGUAGES C) + +option(BUILD_CLI "Build the CLI client" ON) +option(BUILD_QT "Build the Qt GUI client" ON) + +set(TOAST_DEFAULT_REMOTE "http://toast.openfortress.fun/toast" CACHE STRING + "Default Mirror to get OpenFortress files from") + +add_compile_definitions(NAME="${CMAKE_PROJECT_NAME}") +add_compile_definitions(VERSION="${CMAKE_PROJECT_VERSION}") +add_compile_definitions(TOAST_DEFAULT_REMOTE="${TOAST_DEFAULT_REMOTE}") + +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") + +add_subdirectory(src) diff --git a/cmake/FindJsonC.cmake b/cmake/FindJsonC.cmake new file mode 100644 index 0000000..1d4386b --- /dev/null +++ b/cmake/FindJsonC.cmake @@ -0,0 +1,63 @@ +# +# JSONC_INCLUDE_DIRS +# JSONC_LIBRARIES +# JSONC_CFLAGS + +find_package(PkgConfig QUIET) +if (PKG_CONFIG_FOUND) + pkg_check_modules(_JSONC json-c) + + if (BUILD_STATIC AND NOT _JSONC_FOUND) + message(FATAL_ERROR "Cannot find static build information") + endif() +endif() + +if (_JSONC_FOUND) # we can rely on pkg-config + if (NOT BUILD_STATIC) + set(JSONC_LIBRARIES ${_JSONC_LIBRARIES}) + set(JSONC_INCLUDE_DIRS ${_JSONC_INCLUDE_DIRS}) + set(JSONC_CFLAGS ${_JSONC_CFLAGS_OTHER}) + else() + set(JSONC_LIBRARIES ${_JSONC_STATIC_LIBRARIES}) + set(JSONC_INCLUDE_DIRS ${_JSONC_STATIC_INCLUDE_DIRS}) + set(JSONC_CFLAGS ${_JSONC_STATIC_CFLAGS_OTHER}) + endif() +else() + if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(_lib_suffix 64) + else() + set(_lib_suffix 32) + endif() + + find_path(JSONC_INC + NAMES json.h + HINTS + ENV jsoncPath${_lib_suffix} + ENV jsoncPath + ${_JSONC_INCLUDE_DIRS} + PATHS + /usr/include/json-c /usr/local/include/json-c) + + find_library(JSONC_LIB + NAMES ${_JSONC_LIBRARIES} jsonc json-c + HINTS + ENV jsoncPath${_lib_suffix} + ENV jsoncPath + ${_JSONC_LIBRARY_DIRS} + ${_JSONC_STATIC_LIBRARY_DIRS} + PATHS + /usr/lib{_lib_suffix} /usr/local/lib{_lib_suffix} + /usr/lib /usr/local/lib) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(JsonC DEFAULT_MSG JSONC_LIB JSONC_INC) + mark_as_advanced(JSONC_INC JSONC_LIB) + + if(JSONC_FOUND) + set(JSONC_INCLUDE_DIRS ${JSONC_INC}) + set(JSONC_LIBRARIES ${JSONC_LIB}) + if (BUILD_STATIC) + set(JSONC_LIBRARIES ${JSONC_LIBRARIES} ${_JSONC_STATIC_LIBRARIES}) + endif() + endif() +endif()
\ No newline at end of file diff --git a/cmake/FindLibcurl.cmake b/cmake/FindLibcurl.cmake new file mode 100644 index 0000000..aa89105 --- /dev/null +++ b/cmake/FindLibcurl.cmake @@ -0,0 +1,58 @@ +# +# LIBCURL_INCLUDE_DIRS +# LIBCURL_LIBRARIES +# LIBCURL_CFLAGS + +find_package(PkgConfig QUIET) +if (PKG_CONFIG_FOUND) + pkg_check_modules(_CURL libcurl) + + if (BUILD_STATIC AND NOT _CURL_FOUND) + message(FATAL_ERROR "Cannot find static build information") + endif() +endif() + +if (_CURL_FOUND) # we can rely on pkg-config + if (NOT BUILD_STATIC) + set(LIBCURL_LIBRARIES ${_CURL_LIBRARIES}) + set(LIBCURL_INCLUDE_DIRS ${_CURL_INCLUDE_DIRS}) + set(LIBCURL_CFLAGS ${_CURL_CFLAGS_OTHER}) + else() + set(LIBCURL_LIBRARIES ${_CURL_STATIC_LIBRARIES}) + set(LIBCURL_INCLUDE_DIRS ${_CURL_STATIC_INCLUDE_DIRS}) + set(LIBCURL_CFLAGS ${_CURL_STATIC_CFLAGS_OTHER}) + endif() +else() + if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(_lib_suffix 64) + else() + set(_lib_suffix 32) + endif() + + find_path(CURL_INC + NAMES curl/curl.h + HINTS + ENV curlPath${_lib_suffix} + ENV curlPath + PATHS + /usr/include /usr/local/include) + + find_library(CURL_LIB + NAMES curl + HINTS + ENV curlPath${_lib_suffix} + ENV curlPath + PATHS + /usr/lib{_lib_suffix} /usr/local/lib{_lib_suffix} + /usr/lib /usr/local/lib) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(Libcurl DEFAULT_MSG CURL_LIB CURL_INC) + mark_as_advanced(CURL_INC CURL_LIB) + + if(LIBCURL_FOUND) + set(LIBCURL_INCLUDE_DIRS ${CURL_INC}) + set(LIBCURL_LIBRARIES ${CURL_LIB}) + set(LIBCURL_CFLAGS "") + endif() +endif()
\ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..e27aa46 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,44 @@ + +find_package(Libcurl REQUIRED) +find_package(JsonC REQUIRED) +add_subdirectory(hash) + +set(CFLAGS + -Wall -Wextra -pedantic + -Wconversion -Wshadow -Wstrict-aliasing + -Winit-self -Wcast-align -Wpointer-arith + -Wmissing-declarations -Wmissing-include-dirs + -Wno-unused-parameter -Wuninitialized + ${LIBCURL_CFLAGS} + ${JSONC_CFLAGS} +) + +list(APPEND + CORE_SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/fs.c + ${CMAKE_CURRENT_SOURCE_DIR}/fs.h + ${CMAKE_CURRENT_SOURCE_DIR}/net.c + ${CMAKE_CURRENT_SOURCE_DIR}/net.h + ${CMAKE_CURRENT_SOURCE_DIR}/steam.c + ${CMAKE_CURRENT_SOURCE_DIR}/steam.h + ${CMAKE_CURRENT_SOURCE_DIR}/toast.c + ${CMAKE_CURRENT_SOURCE_DIR}/toast.h +) + +add_library(libofqt OBJECT ${CORE_SOURCES}) +target_compile_options(libofqt PUBLIC ${CFLAGS}) + +target_include_directories(libofqt PUBLIC ${LIBCURL_INCLUDE_DIRS}) +target_include_directories(libofqt PUBLIC ${JSONC_INCLUDE_DIRS}) +target_include_directories(libofqt PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(libofqt LINK_PUBLIC ${LIBCURL_LIBRARIES}) +target_link_libraries(libofqt LINK_PUBLIC ${JSONC_LIBRARIES}) +target_link_libraries(libofqt LINK_PUBLIC md5) + +if(BUILD_CLI) + add_subdirectory(cli) +endif() + +if(BUILD_QT) + add_subdirectory(qt) +endif() diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt new file mode 100644 index 0000000..fc46a42 --- /dev/null +++ b/src/cli/CMakeLists.txt @@ -0,0 +1,12 @@ +SET(FRONTEND_NAME "OFCL") + +SET(CLI_SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/commands.c + ${CMAKE_CURRENT_SOURCE_DIR}/commands.h + ${CMAKE_CURRENT_SOURCE_DIR}/main.c + ${CMAKE_CURRENT_SOURCE_DIR}/updater.c + ${CMAKE_CURRENT_SOURCE_DIR}/updater.h +) + +add_executable(${FRONTEND_NAME} ${CLI_SOURCES}) +target_link_libraries(${FRONTEND_NAME} PRIVATE libofqt) diff --git a/src/cli/commands.c b/src/cli/commands.c new file mode 100644 index 0000000..4bac2f7 --- /dev/null +++ b/src/cli/commands.c @@ -0,0 +1,242 @@ +#include <stdlib.h> +#include <stdio.h> +#include <string.h> + +#include "steam.h" +#include "toast.h" + +#include "commands.h" +#include "updater.h" + +#define ARRAY_LEN(arr) sizeof(arr) / sizeof(arr[0]) + +static int install(int, char**); +static int update(int, char**); +static int run(int, char**); +static int info(int, char**); + +const struct Command commands[] = { + { .name = "install", .func = install, .description = "Install OpenFortress"}, + { .name = "update", .func = update, .description = "Update an existing install"}, + { .name = "run", .func = run, .description = "Run OpenFortress"}, + { .name = "info", .func = info, .description = "Show info about the current setup"}, +}; +const size_t commands_size = ARRAY_LEN(commands); + +static int install(int c, char** v) +{ + int exit_val = EXIT_SUCCESS; + char* of_dir = NULL; + char* remote = NULL; + for (int i = 1; i < c; ++i) + { + if (!strcmp(v[i], "--dir")) + { + if (!v[++i]) + { + puts("No Directory specified"); + return EXIT_FAILURE; + } + of_dir = strdup(v[i]); + printf("Using %s as the directory\n", of_dir); + } + else if (!strcmp(v[i], "--remote")) + { + if (!v[++i]) + { + puts("No URL specified"); + return EXIT_FAILURE; + } + remote = strdup(v[i]); + printf("Using %s as the server\n", remote); + } + else + { + if (strcmp(v[i], "--help")) + printf("%s is not a valid flag\n\n", v[i]); + + puts( + "OFCL install\n" + "\t--dir\t\tspecify where to download OpenFortress to\n" + "\t--remote\tspecify the server to use\n" + "\t--help\t\tshows this text" + ); + return EXIT_FAILURE; + } + } + if (!of_dir) + of_dir = getOpenFortressDir(); + + if (getLocalRevision(of_dir) >= 0) + { + puts("OpenFortress is already installed"); + exit_val = EXIT_FAILURE; + goto install_cleanup; + } + + if (!remote) + remote = getLocalRemote(of_dir); + + int remote_rev = getLatestRemoteRevision(remote); + + if (remote_rev != -1) + { + update_setup(of_dir, remote, 0, remote_rev); + } + else + { + puts("Failed to get the latest revision"); + exit_val = EXIT_FAILURE; + } + + install_cleanup: + if (remote) + free(remote); + free(of_dir); + + return exit_val; +} + +static int update(int c, char** v) +{ + int exit_val = EXIT_SUCCESS; + int force = 0; + char* of_dir = NULL; + char* remote = NULL; + for (int i = 1; i < c; ++i) + { + if (!strcmp(v[i], "--force")) + force = 1; + else if (!strcmp(v[i], "--dir")) + { + if (!v[++i]) + { + puts("No Directory specified"); + return EXIT_FAILURE; + } + of_dir = strdup(v[i]); + printf("Using %s as the directory\n", of_dir); + } + else if (!strcmp(v[i], "--remote")) + { + if (!v[++i]) + { + puts("No URL specified"); + return EXIT_FAILURE; + } + remote = strdup(v[i]); + printf("Using %s as the server\n", remote); + } + else + { + if (strcmp(v[i], "--help")) + printf("%s is not a valid flag\n\n", v[i]); + + puts( + "OFCL update\n" + "\t--force\t\tforce update\n" + "\t--dir\t\tspecify where to download OpenFortress to\n" + "\t--remote\tspecify the server to use\n" + "\t--help\t\tshows this text" + ); + return EXIT_FAILURE; + } + } + + if (!of_dir) + of_dir = getOpenFortressDir(); + + int local_rev = getLocalRevision(of_dir); + if (force) + { + local_rev = 0; + } + else if (0 > local_rev) + { + puts("OpenFortress is not installed"); + exit_val = EXIT_FAILURE; + goto update_cleanup; + } + + if (!remote) + remote = getLocalRemote(of_dir); + + if (!strlen(remote)) + { + puts("Remote is invalid"); + exit_val = EXIT_FAILURE; + goto update_cleanup; + } + + int remote_rev = getLatestRemoteRevision(remote); + + if (remote_rev == -1) + { + puts("Failed to get the latest revision"); + exit_val = EXIT_FAILURE; + goto update_cleanup; + } + else if (remote_rev <= local_rev) + { + puts("Already up to date"); + goto update_cleanup; + } + + update_setup(of_dir, remote, local_rev, remote_rev); + + update_cleanup: + if (remote) + free(remote); + free(of_dir); + return exit_val; +} + +static int run(int c, char** v) +{ + int exit_val = EXIT_SUCCESS; + char* of_dir = getOpenFortressDir(); + + int local_rev = getLocalRevision(of_dir); + if (0 > local_rev) + { + puts("OpenFortress is not installed"); + exit_val = EXIT_FAILURE; + goto run_cleanup; + } + + if (getSteamPID() == -1) + { + puts("Steam is not running"); + exit_val = EXIT_FAILURE; + goto run_cleanup; + } + + runOpenFortress(); + + run_cleanup: + free(of_dir); + return exit_val; +} + +static int info(int c, char** v) +{ + char* of_dir = getOpenFortressDir(); + if (!of_dir) + of_dir = strdup("Not Found"); + printf("Install Directory:\n\t%s\n", of_dir); + + char* remote = getLocalRemote(of_dir); + printf("Server:\n\t%s\n", remote); + free(remote); + + int local_rev = getLocalRevision(of_dir); + printf("Revision:\t\n"); + if (local_rev == -1) + puts("\tNot installed"); + else + printf("\t%i\n", local_rev); + + free(of_dir); + + return EXIT_SUCCESS; +} diff --git a/src/cli/commands.h b/src/cli/commands.h new file mode 100644 index 0000000..d55edb9 --- /dev/null +++ b/src/cli/commands.h @@ -0,0 +1,15 @@ +#ifndef COMMANDS_H +#define COMMANDS_H + +#include <stddef.h> + +struct Command { + char* name; + int (*func)(int, char**); + char* description; +}; + +extern const struct Command commands[]; +extern const size_t commands_size; + +#endif
\ No newline at end of file diff --git a/src/cli/main.c b/src/cli/main.c new file mode 100644 index 0000000..7971c7d --- /dev/null +++ b/src/cli/main.c @@ -0,0 +1,50 @@ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "net.h" +#include "commands.h" + +static void help() +{ + fprintf(stderr, "OFCL <command>\n"); \ + + size_t longestStr; + size_t length; + + if (commands_size) + { + longestStr = 0; + + for (size_t i = 0; i < commands_size; ++i) + { + length = strlen(commands[i].name); + + if (length > longestStr) longestStr = length; + } + + fprintf(stderr, "\nList of commands:\n"); + for (size_t i = 0; i < commands_size; ++i) + { + fprintf(stderr, "\t%-*s\t %s\n", (int)longestStr, commands[i].name, commands[i].description); + } + } +} + +int main(int argc, char** argv) +{ + net_init(); + atexit(net_deinit); + + for (int i = 1; i < argc; ++i) + { + for (size_t j = 0; j < commands_size; ++j) + { + if (!strcmp(commands[j].name, argv[i])) + return commands[j].func(argc-i, argv+i); + } + } + + help(); + return EXIT_SUCCESS; +}
\ No newline at end of file diff --git a/src/cli/updater.c b/src/cli/updater.c new file mode 100644 index 0000000..8b0cc03 --- /dev/null +++ b/src/cli/updater.c @@ -0,0 +1,143 @@ +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <pthread.h> + +#include "fs.h" +#include "toast.h" + +#include "updater.h" + +#include <assert.h> + +#define THREAD_COUNT 8 + +struct thread_object_info { + int working; + + char* of_dir; + char* remote; + struct revision_t* rev; + size_t index; +}; + +static void* thread_download(void* pinfo) +{ + struct thread_object_info* info = pinfo; + if (info) + { + char* of_dir = info->of_dir; + char* remote = info->remote; + struct revision_t* rev = info->rev; + size_t i = info->index; + + struct file_info* file = &rev->files[i]; + if (file->type == TYPE_WRITE) + { + fprintf(stderr, "\rChecking %lu/%lu (%s)", i+1, rev->file_count, file->object); + + if (verifyFileHash(of_dir, file)) + { + fprintf(stderr, "\rDownloading %lu/%lu (%s)", i+1, rev->file_count, file->object); + downloadObject(of_dir, remote, file); + } + } + } + + info->working = 0; + pthread_exit(0); + + return NULL; +} + + +void update_setup(char* of_dir, char* remote, int local_rev, int remote_rev) +{ + struct revision_t* rev = fastFowardRevisions(remote, local_rev, remote_rev); + + if (rev) + { + pthread_t download_threads[THREAD_COUNT] = {0}; + struct thread_object_info thread_info[THREAD_COUNT] = {0}; + size_t tindex = 0; + + for (size_t i = 0; i < rev->file_count; ++i) + { + while (thread_info[tindex].working) + { + tindex = (tindex+1) % THREAD_COUNT; + } + + pthread_t* thread = &download_threads[tindex]; + struct thread_object_info* info = &thread_info[tindex]; + + info->working = 1; + info->of_dir = of_dir; + info->remote = remote; + info->rev = rev; + info->index = i; + + pthread_create(thread, NULL, thread_download, info); + } + + for (size_t i = 0; i < THREAD_COUNT; ++i) + { + pthread_t* thread = &download_threads[i]; + if (*thread) + pthread_join(*thread, NULL); + } + + for (size_t i = 0; i < rev->file_count; ++i) + { + struct file_info* file = &rev->files[i]; + if (file->type != TYPE_MKDIR) + continue; + + size_t len = strlen(of_dir) + strlen(OS_PATH_SEP) + strlen(file->path) + 1; + char* buf = malloc(len); + snprintf(buf, len, "%s%s%s", of_dir, OS_PATH_SEP, file->path); + makeDir(buf); + free(buf); + } + + fprintf(stderr, "\rInstalling..."); + for (size_t i = 0; i < rev->file_count; ++i) + { + struct file_info* file = &rev->files[i]; + + switch (file->type) + { + case TYPE_WRITE: + case TYPE_MKDIR: + { + int k = applyObject(of_dir, file); + if (k) + { + printf("Failed to write %s\n", file->path); + } + } + break; + + case TYPE_DELETE: + { + size_t len = strlen(of_dir) + strlen(OS_PATH_SEP) + strlen(file->path) + 1; + char* buf = malloc(len); + snprintf(buf, len, "%s%s%s", of_dir, OS_PATH_SEP, file->path); + if (isFile(buf) && remove(buf)) + { + printf("Failed to delete %s\n", file->path); + } + free(buf); + } + break; + } + } + + removeObjects(of_dir); + setLocalRemote(of_dir, remote); + setLocalRevision(of_dir, remote_rev); + + fprintf(stderr, "\rUpdated OpenFortress\n"); + freeRevision(rev); + } +} diff --git a/src/cli/updater.h b/src/cli/updater.h new file mode 100644 index 0000000..71fc751 --- /dev/null +++ b/src/cli/updater.h @@ -0,0 +1,6 @@ +#ifndef UPDATER_H +#define UPDATER_H + +void update_setup(char*, char*, int, int); + +#endif
\ No newline at end of file diff --git a/src/fs.c b/src/fs.c new file mode 100644 index 0000000..ee7a39d --- /dev/null +++ b/src/fs.c @@ -0,0 +1,136 @@ +#include <stdlib.h> +#include <string.h> +#include <stdio.h> +#include <errno.h> +#include <limits.h> +#include <sys/stat.h> +#include <unistd.h> +#include <dirent.h> + +#include "fs.h" + +#ifdef _WIN32 +#define mkdir(path, perm) mkdir(path) +#endif + +static struct stat getStat(const char* path) +{ + // fill with 0s by default in the case stat fails + struct stat sb = {0}; + + // the return value signifies if stat failes (e.g. file not found) + // unimportant for us if it fails it won't touch sb + stat(path, &sb); + + return sb; +} + +int isFile(const char* path) +{ + struct stat sb = getStat(path); + +#ifndef _WIN32 + if (S_ISLNK(sb.st_mode)) + { + char buf[PATH_MAX]; + readlink(path, buf, sizeof(buf)); + + return isFile(buf); + } +#endif + return S_ISREG(sb.st_mode); +} + +int isDir(const char* path) +{ + struct stat sb = getStat(path); + +#ifndef _WIN32 + if (S_ISLNK(sb.st_mode)) + { + char buf[PATH_MAX]; + readlink(path, buf, sizeof(buf)); + + return isDir(buf); + } +#endif + return S_ISDIR(sb.st_mode); +} + +int makeDir(const char* path) +{ + char pathcpy[PATH_MAX]; + char *index; + + strncpy(pathcpy, path, PATH_MAX-1); // make a mutable copy of the path + + for(index = pathcpy+1; *index; ++index) + { + + if (*index == *OS_PATH_SEP) + { + *index = '\0'; + + if (mkdir(pathcpy, 0755) != 0) + { + if (errno != EEXIST) + return -1; + } + + *index = *OS_PATH_SEP; + } + } + + return mkdir(path, 0755); +} + +int removeDir(const char* path) +{ + DIR *d = opendir(path); + size_t path_len = strlen(path); + int r = -1; + + if (d) { + struct dirent *p; + + r = 0; + while (!r && (p = readdir(d))) { + char *buf; + size_t len; + + // Skip the names "." and ".." as we don't want to recurse on them. + if (!strcmp(p->d_name, ".") || !strcmp(p->d_name, "..")) + continue; + + len = path_len + strlen(p->d_name) + 2; + buf = malloc(len); + + if (buf) { + struct stat statbuf = {0}; + + snprintf(buf, len, "%s/%s", path, p->d_name); + if (!stat(buf, &statbuf)) { + if (S_ISDIR(statbuf.st_mode)) + r = removeDir(buf); +#ifndef _WIN32 + else if (S_ISLNK(statbuf.st_mode)) + r = unlink(buf); +#endif + else + r = remove(buf); + } + else // it is very likely that we found a dangling symlink which is not detected by stat + { + r = unlink(buf); + } + free(buf); + } + } + closedir(d); + } + + if (!r) + r = rmdir(path); + + return r; +} diff --git a/src/fs.h b/src/fs.h new file mode 100644 index 0000000..26cdd8b --- /dev/null +++ b/src/fs.h @@ -0,0 +1,24 @@ +#ifndef FS_H +#define FS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef _WIN32 +#define OS_PATH_SEP "\\" +#else +#define OS_PATH_SEP "/" +#endif + +int isFile(const char*); +int isDir(const char*); + +int makeDir(const char*); +int removeDir(const char*); + +#ifdef __cplusplus +} +#endif + +#endif
\ No newline at end of file diff --git a/src/hash/CMakeLists.txt b/src/hash/CMakeLists.txt new file mode 100644 index 0000000..7290091 --- /dev/null +++ b/src/hash/CMakeLists.txt @@ -0,0 +1,2 @@ + +add_subdirectory(md5)
\ No newline at end of file diff --git a/src/hash/md5/CMakeLists.txt b/src/hash/md5/CMakeLists.txt new file mode 100644 index 0000000..ff560b1 --- /dev/null +++ b/src/hash/md5/CMakeLists.txt @@ -0,0 +1,12 @@ + +list(APPEND + MD5_SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/global.h + ${CMAKE_CURRENT_SOURCE_DIR}/md5.h + ${CMAKE_CURRENT_SOURCE_DIR}/md5c.c +) + +add_library(md5 STATIC ${MD5_SOURCES}) +target_compile_options(md5 PUBLIC ${CFLAGS}) + +target_include_directories(md5 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/src/hash/md5/global.h b/src/hash/md5/global.h new file mode 100644 index 0000000..302b3aa --- /dev/null +++ b/src/hash/md5/global.h @@ -0,0 +1,32 @@ +/* GLOBAL.H - RSAREF types and constants + */ + +/* PROTOTYPES should be set to one if and only if the compiler supports + function argument prototyping. +The following makes PROTOTYPES default to 0 if it has not already + been defined with C compiler flags. + */ +#ifndef PROTOTYPES +#define PROTOTYPES 0 +#endif + +#include <stdint.h> + +/* POINTER defines a generic pointer type */ +typedef unsigned char *POINTER; + +/* UINT2 defines a two byte word */ +typedef uint16_t UINT2; + +/* UINT4 defines a four byte word */ +typedef uint32_t UINT4; + +/* PROTO_LIST is defined depending on how PROTOTYPES is defined above. +If using PROTOTYPES, then PROTO_LIST returns the list, otherwise it + returns an empty list. + */ +#if PROTOTYPES +#define PROTO_LIST(list) list +#else +#define PROTO_LIST(list) () +#endif
\ No newline at end of file diff --git a/src/hash/md5/md5.h b/src/hash/md5/md5.h new file mode 100644 index 0000000..11b3ddd --- /dev/null +++ b/src/hash/md5/md5.h @@ -0,0 +1,38 @@ +/* MD5.H - header file for MD5C.C + */ + +/* Copyright (C) 1991-2, RSA Data Security, Inc. Created 1991. All +rights reserved. + +License to copy and use this software is granted provided that it +is identified as the "RSA Data Security, Inc. MD5 Message-Digest +Algorithm" in all material mentioning or referencing this software +or this function. + +License is also granted to make and use derivative works provided +that such works are identified as "derived from the RSA Data +Security, Inc. MD5 Message-Digest Algorithm" in all material +mentioning or referencing the derived work. + +RSA Data Security, Inc. makes no representations concerning either +the merchantability of this software or the suitability of this +software for any particular purpose. It is provided "as is" +without express or implied warranty of any kind. + +These notices must be retained in any copies of any part of this +documentation and/or software. + */ + +#include "global.h" + +/* MD5 context. */ +typedef struct { + UINT4 state[4]; /* state (ABCD) */ + UINT4 count[2]; /* number of bits, modulo 2^64 (lsb first) */ + unsigned char buffer[64]; /* input buffer */ +} MD5_CTX; + +void MD5Init PROTO_LIST ((MD5_CTX *)); +void MD5Update PROTO_LIST + ((MD5_CTX *, unsigned char *, unsigned int)); +void MD5Final PROTO_LIST ((unsigned char [16], MD5_CTX *)); diff --git a/src/hash/md5/md5c.c b/src/hash/md5/md5c.c new file mode 100644 index 0000000..3682361 --- /dev/null +++ b/src/hash/md5/md5c.c @@ -0,0 +1,334 @@ +/* MD5C.C - RSA Data Security, Inc., MD5 message-digest algorithm + */ + +/* Copyright (C) 1991-2, RSA Data Security, Inc. Created 1991. All +rights reserved. + +License to copy and use this software is granted provided that it +is identified as the "RSA Data Security, Inc. MD5 Message-Digest +Algorithm" in all material mentioning or referencing this software +or this function. + +License is also granted to make and use derivative works provided +that such works are identified as "derived from the RSA Data +Security, Inc. MD5 Message-Digest Algorithm" in all material +mentioning or referencing the derived work. + +RSA Data Security, Inc. makes no representations concerning either +the merchantability of this software or the suitability of this +software for any particular purpose. It is provided "as is" +without express or implied warranty of any kind. + +These notices must be retained in any copies of any part of this +documentation and/or software. + */ + +#include "global.h" +#include "md5.h" + +/* Constants for MD5Transform routine. + */ +#define S11 7 +#define S12 12 +#define S13 17 +#define S14 22 +#define S21 5 +#define S22 9 +#define S23 14 +#define S24 20 +#define S31 4 +#define S32 11 +#define S33 16 +#define S34 23 +#define S41 6 +#define S42 10 +#define S43 15 +#define S44 21 + +static void MD5Transform PROTO_LIST ((UINT4 [4], unsigned char [64])); +static void Encode PROTO_LIST + ((unsigned char *, UINT4 *, unsigned int)); +static void Decode PROTO_LIST + ((UINT4 *, unsigned char *, unsigned int)); +static void MD5_memcpy PROTO_LIST ((POINTER, POINTER, unsigned int)); +static void MD5_memset PROTO_LIST ((POINTER, int, unsigned int)); + +static unsigned char PADDING[64] = { + 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +}; + +/* F, G, H and I are basic MD5 functions. + */ +#define F(x, y, z) (((x) & (y)) | ((~x) & (z))) +#define G(x, y, z) (((x) & (z)) | ((y) & (~z))) +#define H(x, y, z) ((x) ^ (y) ^ (z)) +#define I(x, y, z) ((y) ^ ((x) | (~z))) + +/* ROTATE_LEFT rotates x left n bits. + */ +#define ROTATE_LEFT(x, n) (((x) << (n)) | ((x) >> (32-(n)))) + +/* FF, GG, HH, and II transformations for rounds 1, 2, 3, and 4. +Rotation is separate from addition to prevent recomputation. + */ +#define FF(a, b, c, d, x, s, ac) { \ + (a) += F ((b), (c), (d)) + (x) + (UINT4)(ac); \ + (a) = ROTATE_LEFT ((a), (s)); \ + (a) += (b); \ + } +#define GG(a, b, c, d, x, s, ac) { \ + (a) += G ((b), (c), (d)) + (x) + (UINT4)(ac); \ + (a) = ROTATE_LEFT ((a), (s)); \ + (a) += (b); \ + } +#define HH(a, b, c, d, x, s, ac) { \ + (a) += H ((b), (c), (d)) + (x) + (UINT4)(ac); \ + (a) = ROTATE_LEFT ((a), (s)); \ + (a) += (b); \ + } +#define II(a, b, c, d, x, s, ac) { \ + (a) += I ((b), (c), (d)) + (x) + (UINT4)(ac); \ + (a) = ROTATE_LEFT ((a), (s)); \ + (a) += (b); \ + } + +/* MD5 initialization. Begins an MD5 operation, writing a new context. + */ +void MD5Init (context) +MD5_CTX *context; /* context */ +{ + context->count[0] = context->count[1] = 0; + /* Load magic initialization constants. +*/ + context->state[0] = 0x67452301; + context->state[1] = 0xefcdab89; + context->state[2] = 0x98badcfe; + context->state[3] = 0x10325476; +} + +/* MD5 block update operation. Continues an MD5 message-digest + operation, processing another message block, and updating the + context. + */ +void MD5Update (context, input, inputLen) +MD5_CTX *context; /* context */ +unsigned char *input; /* input block */ +unsigned int inputLen; /* length of input block */ +{ + unsigned int i, index, partLen; + + /* Compute number of bytes mod 64 */ + index = (unsigned int)((context->count[0] >> 3) & 0x3F); + + /* Update number of bits */ + if ((context->count[0] += ((UINT4)inputLen << 3)) + < ((UINT4)inputLen << 3)) + context->count[1]++; + context->count[1] += ((UINT4)inputLen >> 29); + + partLen = 64 - index; + + /* Transform as many times as possible. +*/ + if (inputLen >= partLen) { + MD5_memcpy + ((POINTER)&context->buffer[index], (POINTER)input, partLen); + MD5Transform (context->state, context->buffer); + + for (i = partLen; i + 63 < inputLen; i += 64) + MD5Transform (context->state, &input[i]); + + index = 0; + } + else + i = 0; + + /* Buffer remaining input */ + MD5_memcpy + ((POINTER)&context->buffer[index], (POINTER)&input[i], + inputLen-i); +} + +/* MD5 finalization. Ends an MD5 message-digest operation, writing the + the message digest and zeroizing the context. + */ +void MD5Final (digest, context) +unsigned char digest[16]; /* message digest */ +MD5_CTX *context; /* context */ +{ + unsigned char bits[8]; + unsigned int index, padLen; + + /* Save number of bits */ + Encode (bits, context->count, 8); + + /* Pad out to 56 mod 64. +*/ + index = (unsigned int)((context->count[0] >> 3) & 0x3f); + padLen = (index < 56) ? (56 - index) : (120 - index); + MD5Update (context, PADDING, padLen); + + /* Append length (before padding) */ + MD5Update (context, bits, 8); + + /* Store state in digest */ + Encode (digest, context->state, 16); + + /* Zeroize sensitive information. +*/ + MD5_memset ((POINTER)context, 0, sizeof (*context)); +} + +/* MD5 basic transformation. Transforms state based on block. + */ +static void MD5Transform (state, block) +UINT4 state[4]; +unsigned char block[64]; +{ + UINT4 a = state[0], b = state[1], c = state[2], d = state[3], x[16]; + + Decode (x, block, 64); + + /* Round 1 */ + FF (a, b, c, d, x[ 0], S11, 0xd76aa478); /* 1 */ + FF (d, a, b, c, x[ 1], S12, 0xe8c7b756); /* 2 */ + FF (c, d, a, b, x[ 2], S13, 0x242070db); /* 3 */ + FF (b, c, d, a, x[ 3], S14, 0xc1bdceee); /* 4 */ + FF (a, b, c, d, x[ 4], S11, 0xf57c0faf); /* 5 */ + FF (d, a, b, c, x[ 5], S12, 0x4787c62a); /* 6 */ + FF (c, d, a, b, x[ 6], S13, 0xa8304613); /* 7 */ + FF (b, c, d, a, x[ 7], S14, 0xfd469501); /* 8 */ + FF (a, b, c, d, x[ 8], S11, 0x698098d8); /* 9 */ + FF (d, a, b, c, x[ 9], S12, 0x8b44f7af); /* 10 */ + FF (c, d, a, b, x[10], S13, 0xffff5bb1); /* 11 */ + FF (b, c, d, a, x[11], S14, 0x895cd7be); /* 12 */ + FF (a, b, c, d, x[12], S11, 0x6b901122); /* 13 */ + FF (d, a, b, c, x[13], S12, 0xfd987193); /* 14 */ + FF (c, d, a, b, x[14], S13, 0xa679438e); /* 15 */ + FF (b, c, d, a, x[15], S14, 0x49b40821); /* 16 */ + + /* Round 2 */ + GG (a, b, c, d, x[ 1], S21, 0xf61e2562); /* 17 */ + GG (d, a, b, c, x[ 6], S22, 0xc040b340); /* 18 */ + GG (c, d, a, b, x[11], S23, 0x265e5a51); /* 19 */ + GG (b, c, d, a, x[ 0], S24, 0xe9b6c7aa); /* 20 */ + GG (a, b, c, d, x[ 5], S21, 0xd62f105d); /* 21 */ + GG (d, a, b, c, x[10], S22, 0x2441453); /* 22 */ + GG (c, d, a, b, x[15], S23, 0xd8a1e681); /* 23 */ + GG (b, c, d, a, x[ 4], S24, 0xe7d3fbc8); /* 24 */ + GG (a, b, c, d, x[ 9], S21, 0x21e1cde6); /* 25 */ + GG (d, a, b, c, x[14], S22, 0xc33707d6); /* 26 */ + GG (c, d, a, b, x[ 3], S23, 0xf4d50d87); /* 27 */ + GG (b, c, d, a, x[ 8], S24, 0x455a14ed); /* 28 */ + GG (a, b, c, d, x[13], S21, 0xa9e3e905); /* 29 */ + GG (d, a, b, c, x[ 2], S22, 0xfcefa3f8); /* 30 */ + GG (c, d, a, b, x[ 7], S23, 0x676f02d9); /* 31 */ + GG (b, c, d, a, x[12], S24, 0x8d2a4c8a); /* 32 */ + + /* Round 3 */ + HH (a, b, c, d, x[ 5], S31, 0xfffa3942); /* 33 */ + HH (d, a, b, c, x[ 8], S32, 0x8771f681); /* 34 */ + HH (c, d, a, b, x[11], S33, 0x6d9d6122); /* 35 */ + HH (b, c, d, a, x[14], S34, 0xfde5380c); /* 36 */ + HH (a, b, c, d, x[ 1], S31, 0xa4beea44); /* 37 */ + HH (d, a, b, c, x[ 4], S32, 0x4bdecfa9); /* 38 */ + HH (c, d, a, b, x[ 7], S33, 0xf6bb4b60); /* 39 */ + HH (b, c, d, a, x[10], S34, 0xbebfbc70); /* 40 */ + HH (a, b, c, d, x[13], S31, 0x289b7ec6); /* 41 */ + HH (d, a, b, c, x[ 0], S32, 0xeaa127fa); /* 42 */ + HH (c, d, a, b, x[ 3], S33, 0xd4ef3085); /* 43 */ + HH (b, c, d, a, x[ 6], S34, 0x4881d05); /* 44 */ + HH (a, b, c, d, x[ 9], S31, 0xd9d4d039); /* 45 */ + HH (d, a, b, c, x[12], S32, 0xe6db99e5); /* 46 */ + HH (c, d, a, b, x[15], S33, 0x1fa27cf8); /* 47 */ + HH (b, c, d, a, x[ 2], S34, 0xc4ac5665); /* 48 */ + + /* Round 4 */ + II (a, b, c, d, x[ 0], S41, 0xf4292244); /* 49 */ + II (d, a, b, c, x[ 7], S42, 0x432aff97); /* 50 */ + II (c, d, a, b, x[14], S43, 0xab9423a7); /* 51 */ + II (b, c, d, a, x[ 5], S44, 0xfc93a039); /* 52 */ + II (a, b, c, d, x[12], S41, 0x655b59c3); /* 53 */ + II (d, a, b, c, x[ 3], S42, 0x8f0ccc92); /* 54 */ + II (c, d, a, b, x[10], S43, 0xffeff47d); /* 55 */ + II (b, c, d, a, x[ 1], S44, 0x85845dd1); /* 56 */ + II (a, b, c, d, x[ 8], S41, 0x6fa87e4f); /* 57 */ + II (d, a, b, c, x[15], S42, 0xfe2ce6e0); /* 58 */ + II (c, d, a, b, x[ 6], S43, 0xa3014314); /* 59 */ + II (b, c, d, a, x[13], S44, 0x4e0811a1); /* 60 */ + II (a, b, c, d, x[ 4], S41, 0xf7537e82); /* 61 */ + II (d, a, b, c, x[11], S42, 0xbd3af235); /* 62 */ + II (c, d, a, b, x[ 2], S43, 0x2ad7d2bb); /* 63 */ + II (b, c, d, a, x[ 9], S44, 0xeb86d391); /* 64 */ + + state[0] += a; + state[1] += b; + state[2] += c; + state[3] += d; + + /* Zeroize sensitive information. +*/ + MD5_memset ((POINTER)x, 0, sizeof (x)); +} + +/* Encodes input (UINT4) into output (unsigned char). Assumes len is + a multiple of 4. + */ +static void Encode (output, input, len) +unsigned char *output; +UINT4 *input; +unsigned int len; +{ + unsigned int i, j; + + for (i = 0, j = 0; j < len; i++, j += 4) { + output[j] = (unsigned char)(input[i] & 0xff); + output[j+1] = (unsigned char)((input[i] >> 8) & 0xff); + output[j+2] = (unsigned char)((input[i] >> 16) & 0xff); + output[j+3] = (unsigned char)((input[i] >> 24) & 0xff); + } +} + +/* Decodes input (unsigned char) into output (UINT4). Assumes len is + a multiple of 4. + */ +static void Decode (output, input, len) +UINT4 *output; +unsigned char *input; +unsigned int len; +{ + unsigned int i, j; + + for (i = 0, j = 0; j < len; i++, j += 4) + output[i] = ((UINT4)input[j]) | (((UINT4)input[j+1]) << 8) | + (((UINT4)input[j+2]) << 16) | (((UINT4)input[j+3]) << 24); +} + +/* Note: Replace "for loop" with standard memcpy if possible. + */ + +static void MD5_memcpy (output, input, len) +POINTER output; +POINTER input; +unsigned int len; +{ + unsigned int i; + + for (i = 0; i < len; i++) + output[i] = input[i]; +} + +/* Note: Replace "for loop" with standard memset if possible. + */ +static void MD5_memset (output, value, len) +POINTER output; +int value; +unsigned int len; +{ + unsigned int i; + + for (i = 0; i < len; i++) + ((char *)output)[i] = (char)value; +} diff --git a/src/net.c b/src/net.c new file mode 100644 index 0000000..0ae8e23 --- /dev/null +++ b/src/net.c @@ -0,0 +1,128 @@ +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <stdint.h> +#include <unistd.h> +#include <curl/curl.h> +#include <json.h> + +#include "net.h" +#include "fs.h" + +#define USER_AGENT NAME "/" VERSION + +void net_init() +{ + curl_global_init(CURL_GLOBAL_ALL); +} + +void net_deinit() +{ + curl_global_cleanup(); +} + +static inline size_t memoryCallback(const void* contents, size_t size, size_t nmemb, void* userp) +{ + size_t realsize = size * nmemb; + struct MemoryStruct* mem = (struct MemoryStruct*)userp; + + uint8_t* ptr = realloc(mem->memory, mem->size + realsize + 1); + if (!ptr) + return 0; + + mem->memory = ptr; + memcpy(&(mem->memory[mem->size]), contents, realsize); + mem->size += realsize; + mem->memory[mem->size] = 0; + + return realsize; +} + +struct MemoryStruct* downloadToRam(const char* url) +{ + CURL* curl_handle; + CURLcode res = CURLE_OK; + + struct MemoryStruct* chunk = malloc(sizeof(struct MemoryStruct)); + + if (chunk) + { + chunk->memory = malloc(1); + if (!chunk->memory) + { + free(chunk); + return NULL; + } + chunk->memory[0] = 0; + chunk->size = 0; + + curl_handle = curl_easy_init(); + + curl_easy_setopt(curl_handle, CURLOPT_URL, url); + curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, memoryCallback); + curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, (void*)chunk); + curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, USER_AGENT); + curl_easy_setopt(curl_handle, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl_handle, CURLOPT_NOPROGRESS, 1L); + + res = curl_easy_perform(curl_handle); + + long http_code = 0; + curl_easy_getinfo(curl_handle, CURLINFO_RESPONSE_CODE, &http_code); + + if (res != CURLE_OK || http_code != 200) + { + freeDownload(chunk); + chunk = NULL; + } + + curl_easy_cleanup(curl_handle); + } + + return chunk; +} + +size_t downloadToFile(const char* url, const char* path) +{ + size_t out_write = 0; + struct MemoryStruct* chunk = downloadToRam(url); + + if (chunk) + { + FILE* fp = fopen(path, "wb"); + + if (fp) + { + out_write = fwrite(chunk->memory, sizeof(uint8_t), chunk->size, fp); + fclose(fp); + } + + freeDownload(chunk); + } + + return out_write; +} + +void freeDownload(struct MemoryStruct* chunk) +{ + if (chunk) + { + if (chunk->memory) + free(chunk->memory); + free(chunk); + } +} + +struct json_object* fetchJSON(const char* url) +{ + struct json_object* json = NULL; + struct MemoryStruct* chunk = downloadToRam(url); + + if (chunk) + { + json = json_tokener_parse((char*)chunk->memory); + freeDownload(chunk); + } + + return json; +} diff --git a/src/net.h b/src/net.h new file mode 100644 index 0000000..fc6d34a --- /dev/null +++ b/src/net.h @@ -0,0 +1,28 @@ +#ifndef NET_H +#define NET_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include <curl/curl.h> +#include <json.h> + +struct MemoryStruct { + uint8_t* memory; + size_t size; +}; + +void net_init(); +void net_deinit(); + +struct MemoryStruct* downloadToRam(const char* URL); +size_t downloadToFile(const char*, const char*); +void freeDownload(struct MemoryStruct* chunk); +struct json_object* fetchJSON(const char*); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/qt/CMakeLists.txt b/src/qt/CMakeLists.txt new file mode 100644 index 0000000..c0566d9 --- /dev/null +++ b/src/qt/CMakeLists.txt @@ -0,0 +1,34 @@ +SET(FRONTEND_NAME "OFQT") +enable_language(CXX) + +find_package(QT NAMES Qt6 Qt5 COMPONENTS Widgets Gui REQUIRED) +find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets Gui REQUIRED) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +list(APPEND + QT_SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/mainwindow.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/mainwindow.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/settings.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/settings.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/workers.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/workers.hpp + + ${CMAKE_CURRENT_SOURCE_DIR}/mainwindow.ui + ${CMAKE_CURRENT_SOURCE_DIR}/assets.qrc +) + +if(WIN32) + string(REPLACE "." "," CMAKE_PROJECT_COMMAVERSION ${CMAKE_PROJECT_VERSION}) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/assets/version.rc.in ${CMAKE_CURRENT_BINARY_DIR}/version.rc @ONLY) + list(APPEND QT_SOURCES ${CMAKE_CURRENT_BINARY_DIR}/version.rc) +endif() + +add_executable(${FRONTEND_NAME} WIN32 ${QT_SOURCES}) +target_link_libraries(${FRONTEND_NAME} PRIVATE libofqt) +target_link_libraries(${FRONTEND_NAME} PRIVATE Qt${QT_VERSION_MAJOR}::Widgets) +set_property(TARGET ${FRONTEND_NAME} PROPERTY CXX_STANDARD 11) diff --git a/src/qt/assets.qrc b/src/qt/assets.qrc new file mode 100644 index 0000000..10bca84 --- /dev/null +++ b/src/qt/assets.qrc @@ -0,0 +1,15 @@ +<RCC> + <qresource prefix="background"> + <file>assets/background.bmp</file> + </qresource> + <qresource prefix="icon"> + <file>assets/globe.png</file> + <file>assets/logo.png</file> + <file>assets/gear-solid.png</file> + <file>assets/discord.png</file> + <file>assets/game.ico</file> + </qresource> + <qresource prefix="font"> + <file>assets/tf2build.ttf</file> + </qresource> +</RCC> diff --git a/src/qt/assets/background.bmp b/src/qt/assets/background.bmp Binary files differnew file mode 100644 index 0000000..37156be --- /dev/null +++ b/src/qt/assets/background.bmp diff --git a/src/qt/assets/discord.png b/src/qt/assets/discord.png Binary files differnew file mode 100644 index 0000000..cc52539 --- /dev/null +++ b/src/qt/assets/discord.png diff --git a/src/qt/assets/game.ico b/src/qt/assets/game.ico Binary files differnew file mode 100644 index 0000000..7ebf98a --- /dev/null +++ b/src/qt/assets/game.ico diff --git a/src/qt/assets/gear-solid.png b/src/qt/assets/gear-solid.png Binary files differnew file mode 100644 index 0000000..8540b00 --- /dev/null +++ b/src/qt/assets/gear-solid.png diff --git a/src/qt/assets/globe.png b/src/qt/assets/globe.png Binary files differnew file mode 100644 index 0000000..98374e3 --- /dev/null +++ b/src/qt/assets/globe.png diff --git a/src/qt/assets/logo.png b/src/qt/assets/logo.png Binary files differnew file mode 100644 index 0000000..ad04eca --- /dev/null +++ b/src/qt/assets/logo.png diff --git a/src/qt/assets/tf2build.ttf b/src/qt/assets/tf2build.ttf Binary files differnew file mode 100644 index 0000000..d59eec9 --- /dev/null +++ b/src/qt/assets/tf2build.ttf diff --git a/src/qt/assets/version.rc.in b/src/qt/assets/version.rc.in new file mode 100644 index 0000000..06d3430 --- /dev/null +++ b/src/qt/assets/version.rc.in @@ -0,0 +1,21 @@ +IDI_ICON1 ICON DISCARDABLE "@CMAKE_CURRENT_SOURCE_DIR@/assets/game.ico" +1 VERSIONINFO +FILEVERSION @CMAKE_PROJECT_COMMAVERSION@ +PRODUCTVERSION @CMAKE_PROJECT_COMMAVERSION@ +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904E4" + BEGIN + VALUE "FileVersion", "1.0" + VALUE "InternalName", "@CMAKE_PROJECT_NAME@" + VALUE "OriginalFilename", "@FRONTEND_NAME@.exe" + VALUE "ProductName", "@CMAKE_PROJECT_NAME@" + VALUE "ProductVersion", "@CMAKE_PROJECT_VERSION@" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END
\ No newline at end of file diff --git a/src/qt/main.cpp b/src/qt/main.cpp new file mode 100644 index 0000000..9cf530b --- /dev/null +++ b/src/qt/main.cpp @@ -0,0 +1,12 @@ +#include "mainwindow.hpp" + +#include <QApplication> + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + MainWindow w; + w.show(); + + return a.exec(); +} diff --git a/src/qt/mainwindow.cpp b/src/qt/mainwindow.cpp new file mode 100644 index 0000000..35151ca --- /dev/null +++ b/src/qt/mainwindow.cpp @@ -0,0 +1,249 @@ +#include <QApplication> +#include <QMessageBox> +#include <QFontDatabase> +#include <QDesktopServices> +#include <QUrl> + +#include <limits.h> +#include <iostream> + +#include "steam.h" + +#include "mainwindow.hpp" +#include "./ui_mainwindow.h" +#include "workers.hpp" + + + +#define FONT "tf2build" + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) + , ui(new Ui::MainWindow) +{ + ui->setupUi(this); + + centralWidget()->layout()->setContentsMargins(0, 0, 0, 0); + + qRegisterMetaType<Worker::Tasks_t>("Task_t"); + qRegisterMetaType<Worker::Results_t>("Results_t"); + + QFontDatabase::addApplicationFont (":/font/assets/" FONT ".ttf"); + QFont playFont(FONT, 20, QFont::Bold); + QFont progressFont(FONT, 10, QFont::Normal); + + ui->mainButton->setFont(playFont); + ui->progressBar->setFont(progressFont); + ui->statusLabel->setFont(progressFont); + ui->infoLabel->setFont(progressFont); + + QPixmap bkgnd(":/background/assets/background.bmp"); + bkgnd = bkgnd.scaled(this->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); + + QPalette palette; + palette.setBrush(QPalette::Window, bkgnd); + this->setPalette(palette); + + worker = new Worker(); + worker->moveToThread(&thread); + + connect(&thread, &QThread::finished, worker, &QObject::deleteLater); + connect(this, &MainWindow::workerOperate, worker, &Worker::doWork); + connect(worker, &Worker::resultReady, this, &MainWindow::workerResult); + + thread.start(); + + //operateSVN(svnWorker::SVN_INSTALL); + + connect(ui->settingsButton, SIGNAL(clicked()), this, SLOT(settingsWindow())); + + settings = new Settings(worker, this); + settings->setModal(true); + //connect(settings, SIGNAL(visibleChanged()), this, SLOT(enable())); + + ui->mainButton->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(ui->mainButton, SIGNAL(clicked()), this, SLOT(updateButton())); + connect(ui->mainButton, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(showButtonContext(const QPoint&))); + connect(ui->buttonDiscord, SIGNAL(clicked()), this, SLOT(openDiscordInvite())); + connect(ui->buttonWebsite, SIGNAL(clicked()), this, SLOT(openWebsite())); + + ui->mainButton->setText("..."); + ui->statusLabel->setText("..."); + ui->infoLabel->setText("..."); + + in_progress = false; + installed = false; + uptodate = false; + workerOperate(Worker::TASK_INIT); +} + +void MainWindow::workerResult(const enum Worker::Results_t& result) +{ + switch (result) + { + + case Worker::RESULT_UNINSTALL_COMPLETE: + case Worker::RESULT_UNINSTALL_FAILURE: + // TODO find better use for these + case Worker::RESULT_NONE: + resetProgress(); + break; + + case Worker::RESULT_EXIT: + QCoreApplication::quit(); + break; + + case Worker::RESULT_UPDATE_TEXT: + ui->progressBar->setValue(worker->progress); + ui->infoLabel->setText(worker->infoText); + break; + + case Worker::RESULT_IS_INSTALLED: + installed = true; + workerOperate(Worker::TASK_IS_UPTODATE); + break; + + case Worker::RESULT_IS_NOT_INSTALLED: + ui->mainButton->setText("Install"); + installed = false; + break; + + case Worker::RESULT_IS_UPTODATE: + uptodate = true; + ui->mainButton->setText("Play"); + ui->statusLabel->setText("Up to Date"); + break; + + case Worker::RESULT_IS_OUTDATED: + uptodate = false; + ui->mainButton->setText("Update"); + ui->statusLabel->setText(QString("Revision %1 is available").arg(worker->getRemoteRevision())); + break; + + case Worker::RESULT_INIT_COMPLETE: + ui->statusLabel->setText(""); + ui->infoLabel->setText(""); + workerOperate(Worker::TASK_IS_INSTALLED); + break; + + case Worker::RESULT_INIT_FAILURE: + QMessageBox::information(this, windowTitle(), "Could not find install location.\nIs Steam installed?"); + QCoreApplication::quit(); + break; + + case Worker::RESULT_INSTALL_COMPLETE: + resetProgress(); + ui->statusLabel->setText("Installed"); + workerOperate(Worker::TASK_IS_INSTALLED); + break; + + case Worker::RESULT_INSTALL_FAILURE: + ui->progressBar->setFormat("Install failed"); + break; + + case Worker::RESULT_UPDATE_COMPLETE: + resetProgress(); + ui->statusLabel->setText("Updated"); + workerOperate(Worker::TASK_IS_UPTODATE); + break; + + case Worker::RESULT_UPDATE_RUN: + ui->statusLabel->setText("Launching"); + workerOperate(Worker::TASK_RUN); + break; + + case Worker::RESULT_UPDATE_FAILURE: + ui->statusLabel->setText("Update failed"); + break; + + case Worker::RESULT_NO_STEAM: + resetProgress(); + QMessageBox::information(this, windowTitle(), "Steam is not running" ); + break; + + } + + in_progress = false; +} + +void MainWindow::settingsWindow() +{ + settings->refresh(); + settings->show(); + //this->setEnabled(false); +} + +void MainWindow::setupButton() +{ + workerOperate(Worker::TASK_IS_INSTALLED); +} + +void MainWindow::updateButton() +{ + if (in_progress) + return; + + if (installed) + { + if (!uptodate) + { + workerOperate(Worker::TASK_UPDATE); + ui->statusLabel->setText("Updating (may take a while)"); + } + else + { + workerOperate(Worker::TASK_RUN); + } + } + else + { + workerOperate(Worker::TASK_INSTALL); + ui->statusLabel->setText("Installing (may take a while)"); + } + + in_progress = true; +} + +void MainWindow::showButtonContext(const QPoint& pos) +{ + QPoint absPos = ui->mainButton->mapToGlobal(pos); + + QMenu ctxMenu; + ctxMenu.addAction("Run without Update"); + + QAction* selectedItem = ctxMenu.exec(absPos); + + if (selectedItem) + { + workerOperate(Worker::TASK_RUN); + } +} + + +void MainWindow::openDiscordInvite() +{ + QDesktopServices::openUrl(QUrl("https://discord.gg/mKjW2ACCrm", QUrl::TolerantMode)); +} + +void MainWindow::openWebsite() +{ + QDesktopServices::openUrl(QUrl("https://openfortress.fun/", QUrl::TolerantMode)); +} + +void MainWindow::resetProgress() +{ + ui->progressBar->setFormat(""); + ui->progressBar->setValue(-1); +} + +MainWindow::~MainWindow() +{ + delete ui; + delete settings; + + worker->stop_work(); + thread.quit(); + thread.wait(); +} + diff --git a/src/qt/mainwindow.hpp b/src/qt/mainwindow.hpp new file mode 100644 index 0000000..0d21efc --- /dev/null +++ b/src/qt/mainwindow.hpp @@ -0,0 +1,50 @@ +#ifndef MAINWINDOW_HPP +#define MAINWINDOW_HPP + +#include <QThread> +#include <QMainWindow> +#include <QMenu> + +#include "settings.hpp" +#include "workers.hpp" + +QT_BEGIN_NAMESPACE +namespace Ui { class MainWindow; } +QT_END_NAMESPACE + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + MainWindow(QWidget* parent = nullptr); + ~MainWindow(); + +private: + Ui::MainWindow *ui; + Settings* settings; + QThread thread; + Worker* worker; + + bool in_progress; + bool installed; + bool uptodate; + + void setupButton(); + void resetProgress(); + +public slots: + void workerResult(const Worker::Results_t&); + +private slots: + void settingsWindow(); + void updateButton(); + void showButtonContext(const QPoint&); + void openDiscordInvite(); + void openWebsite(); + +signals: + void workerOperate(const Worker::Tasks_t&); + +}; +#endif // MAINWINDOW_HPP diff --git a/src/qt/mainwindow.ui b/src/qt/mainwindow.ui new file mode 100644 index 0000000..574129b --- /dev/null +++ b/src/qt/mainwindow.ui @@ -0,0 +1,349 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MainWindow</class> + <widget class="QMainWindow" name="MainWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>736</width> + <height>413</height> + </rect> + </property> + <property name="minimumSize"> + <size> + <width>736</width> + <height>413</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>736</width> + <height>413</height> + </size> + </property> + <property name="windowTitle"> + <string>OFQT</string> + </property> + <property name="windowIcon"> + <iconset resource="assets.qrc"> + <normaloff>:/icon/assets/game.ico</normaloff>:/icon/assets/game.ico</iconset> + </property> + <property name="styleSheet"> + <string notr="true"/> + </property> + <widget class="QWidget" name="centralwidget"> + <property name="styleSheet"> + <string notr="true"/> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <layout class="QVBoxLayout" name="layoutLeft"> + <item> + <widget class="QLabel" name="logoOF"> + <property name="styleSheet"> + <string notr="true">QLabel { + margin: 10px; +}</string> + </property> + <property name="text"> + <string><html><head/><body><p><img width="300" src=":/icon/assets/logo.png"/></p></body></html></string> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <layout class="QVBoxLayout" name="layoutRight"> + <item> + <layout class="QHBoxLayout" name="layoutSocial"> + <property name="leftMargin"> + <number>6</number> + </property> + <property name="topMargin"> + <number>6</number> + </property> + <property name="rightMargin"> + <number>6</number> + </property> + <item> + <widget class="QPushButton" name="buttonWebsite"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>45</width> + <height>45</height> + </size> + </property> + <property name="styleSheet"> + <string notr="true">QPushButton { + image: url(:/icon/assets/globe.png); + background-color: rgba(0, 0, 0, 0); + border: 0; +}</string> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="buttonDiscord"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>45</width> + <height>45</height> + </size> + </property> + <property name="styleSheet"> + <string notr="true">QPushButton { + image: url(:/icon/assets/discord.png); + background-color: rgba(0, 0, 0, 0); + border: 0; +}</string> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> + </item> + <item> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>0</number> + </property> + <property name="sizeConstraint"> + <enum>QLayout::SetMinimumSize</enum> + </property> + <item> + <layout class="QVBoxLayout" name="statusLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <item> + <widget class="QLabel" name="statusLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="styleSheet"> + <string notr="true">QLabel { + color: white; + margin-left: 5px; + margin-right: 5px; +}</string> + </property> + <property name="text"> + <string>STATUS</string> + </property> + <property name="alignment"> + <set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="infoLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="styleSheet"> + <string notr="true">QLabel { + color: white; + margin-left: 5px; + margin-right: 5px; +}</string> + </property> + <property name="text"> + <string>INFO</string> + </property> + <property name="alignment"> + <set>Qt::AlignBottom|Qt::AlignRight|Qt::AlignTrailing</set> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QProgressBar" name="progressBar"> + <property name="styleSheet"> + <string notr="true">QProgressBar +{ + text-align: center; + border: none; + background-color: rgba(0, 0, 0, 140); + color: white; +} + +QProgressBar::chunk +{ + background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0, + stop:0 rgba(42, 28, 77, 255), + stop:1 rgba(73, 43, 133, 255)); + border: none; +}</string> + </property> + <property name="minimum"> + <number>0</number> + </property> + <property name="value"> + <number>-1</number> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="invertedAppearance"> + <bool>false</bool> + </property> + <property name="textDirection"> + <enum>QProgressBar::TopToBottom</enum> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QPushButton" name="settingsButton"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>60</width> + <height>60</height> + </size> + </property> + <property name="autoFillBackground"> + <bool>false</bool> + </property> + <property name="styleSheet"> + <string notr="true">QPushButton { + padding: 10px; + image: url(:/icon/assets/gear-solid.png); + background-color: rgba(0, 0, 0, 140); + border: none; + color: white; +} + +QPushButton:pressed { + /*background-color: rgba(0, 0, 0, 200);*/ +} +</string> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="mainButton"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>175</width> + <height>60</height> + </size> + </property> + <property name="styleSheet"> + <string notr="true">QPushButton { + /*background-color: rgba(0, 0, 0, 140);*/ + background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0, stop:0 rgba(38, 33, 76, 255), stop:0.0534188 rgba(38, 33, 76, 255), stop:0.0566239 rgba(0, 0, 0, 140)); + border: none; + color: white; +} + +QPushButton:pressed { + /*background-color: rgba(0, 0, 0, 200);*/ + background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0, stop:0 rgba(38, 33, 76, 255), stop:0.0534188 rgba(38, 33, 76, 255), stop:0.0566239 rgba(0, 0, 0, 200)); +} +</string> + </property> + <property name="text"> + <string>MAIN_BUTTON</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </widget> + <tabstops> + <tabstop>mainButton</tabstop> + <tabstop>settingsButton</tabstop> + <tabstop>buttonDiscord</tabstop> + </tabstops> + <resources> + <include location="assets.qrc"/> + </resources> + <connections/> +</ui> diff --git a/src/qt/settings.cpp b/src/qt/settings.cpp new file mode 100644 index 0000000..507e8c8 --- /dev/null +++ b/src/qt/settings.cpp @@ -0,0 +1,44 @@ +#include <QMessageBox> +#include <iostream> + +#include "workers.hpp" +#include "settings.hpp" +#include "./ui_settings.h" + +Settings::Settings(Worker* pworker, QWidget *parent) + : QDialog(parent) + , ui(new Ui::Settings) +{ + this->worker = pworker; + ui->setupUi(this); + + connect(ui->buttonBox, SIGNAL(accepted()), this, SLOT(applySettings())); + connect(ui->verifyButton, SIGNAL(clicked()), this, SLOT(verify())); + connect(this, &Settings::workerOperate, worker, &Worker::doWork); +} + +void Settings::refresh() +{ + ui->serverEdit->setText(this->worker->getRemote()); + + ui->revisionLabel->setText(QString("%1").arg(this->worker->getRevision())); + ui->installLabel->setText(this->worker->getOfDir()); +} + +void Settings::applySettings() +{ + worker->setRemote(ui->serverEdit->text()); + this->hide(); +} + +void Settings::verify() +{ + workerOperate(Worker::TASK_INSTALL); + QMessageBox::information(this, windowTitle(), "Verification Started" ); +} + +Settings::~Settings() +{ + delete ui; +} + diff --git a/src/qt/settings.hpp b/src/qt/settings.hpp new file mode 100644 index 0000000..3718f06 --- /dev/null +++ b/src/qt/settings.hpp @@ -0,0 +1,35 @@ +#ifndef SETTINGS_HPP +#define SETTINGS_HPP + +#include <QWidget> +#include <QDialog> + +#include "workers.hpp" + +QT_BEGIN_NAMESPACE +namespace Ui { class Settings; } +QT_END_NAMESPACE + +class Settings : public QDialog +{ + Q_OBJECT + +public: + Settings(Worker* pworker, QWidget *parent = nullptr); + ~Settings(); + + void refresh(); + +private: + Ui::Settings *ui; + Worker* worker; + +public slots: + void applySettings(); + void verify(); + +signals: + void workerOperate(const Worker::Tasks_t &); + +}; +#endif // SETTINGS_HPP diff --git a/src/qt/settings.ui b/src/qt/settings.ui new file mode 100644 index 0000000..4fde446 --- /dev/null +++ b/src/qt/settings.ui @@ -0,0 +1,215 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>Settings</class> + <widget class="QWidget" name="Settings"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>375</width> + <height>259</height> + </rect> + </property> + <property name="windowTitle"> + <string>Settings</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QTabWidget" name="tabWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="serverTab"> + <attribute name="title"> + <string>Server</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QLabel" name="serverLabel"> + <property name="text"> + <string>Server</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLineEdit" name="serverEdit"/> + </item> + </layout> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWidget" name="infoTab"> + <attribute name="title"> + <string>Info</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <layout class="QHBoxLayout" name="revisionLayout"> + <item> + <widget class="QLabel" name="revisionText"> + <property name="text"> + <string>Current Revision</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_3"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="revisionLabel"> + <property name="text"> + <string>CURRENT_REVISION</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="installLayout"> + <item> + <widget class="QLabel" name="installText"> + <property name="text"> + <string>Install Path</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_4"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="installLabel"> + <property name="text"> + <string>INSTALL_PATH</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLabel" name="verifyLabel"> + <property name="text"> + <string>Verify Files</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="verifyButton"> + <property name="text"> + <string>Verify</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + <property name="centerButtons"> + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>Settings</receiver> + <slot>hide()</slot> + <hints> + <hint type="sourcelabel"> + <x>187</x> + <y>235</y> + </hint> + <hint type="destinationlabel"> + <x>187</x> + <y>129</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/src/qt/workers.cpp b/src/qt/workers.cpp new file mode 100644 index 0000000..3fce706 --- /dev/null +++ b/src/qt/workers.cpp @@ -0,0 +1,299 @@ +#include <iostream> +#include <limits.h> +#include <pthread.h> + +#include "net.h" +#include "steam.h" +#include "toast.h" + +#include "./ui_mainwindow.h" +#include "workers.hpp" + +#define THREAD_COUNT 4 + +struct thread_object_info { + int working; + + QString* infoText; + char* of_dir; + char* remote; + struct revision_t* rev; + size_t index; +}; + +static void* thread_download(void* pinfo) +{ + struct thread_object_info* info = (struct thread_object_info*)pinfo; + if (info) + { + QString* infoText = info->infoText; + char* of_dir = info->of_dir; + char* remote = info->remote; + struct revision_t* rev = info->rev; + size_t i = info->index; + + struct file_info* file = &rev->files[i]; + if (file->type == TYPE_WRITE) + { + *info->infoText = QString("Verifying %1").arg(file->object); + if (verifyFileHash(of_dir, file)) + { + *infoText = QString("Downloading %1").arg(file->object); + downloadObject(of_dir, remote, file); + } + } + } + + info->working = 0; + pthread_exit(0); + + return NULL; +} + +Worker::Worker() +{ + net_init(); + of_dir = NULL; + remote = NULL; +} + +Worker::~Worker() +{ + net_deinit(); + + if (of_dir) + free(of_dir); + + if (remote) + free(remote); +} + +QString Worker::getOfDir() +{ + return QString(of_dir); +} + +QString Worker::getRemote() +{ + return QString(remote); +} + +void Worker::setRemote(QString remotestr) +{ + if (remote) + free(remote); + + remote = strdup(remotestr.toStdString().c_str()); + setLocalRemote(of_dir, remote); +} + +int Worker::getRevision() +{ + return getLocalRevision(of_dir); +} + +int Worker::getRemoteRevision() +{ + return getLatestRemoteRevision(remote); +} + +bool Worker::isOutdated() +{ + return getRemoteRevision() > getRevision(); +} + +void Worker::stop_work() +{ + do_work = false; +} + +int Worker::update_setup(int local_rev, int remote_rev) +{ + if (!of_dir) return 1; + + int retval = 0; + + + struct revision_t* rev = fastFowardRevisions(remote, local_rev, remote_rev); + + if (rev) + { + pthread_t download_threads[THREAD_COUNT] = {0}; + struct thread_object_info thread_info[THREAD_COUNT] = {0, NULL, NULL, NULL, NULL, 0}; + size_t tindex = 0; + QString infoStrings[THREAD_COUNT]; + + for (size_t i = 0; i < rev->file_count && do_work; ++i) + { + while (thread_info[tindex].working) + { + tindex = (tindex+1) % THREAD_COUNT; + } + + pthread_t* thread = &download_threads[tindex]; + struct thread_object_info* info = &thread_info[tindex]; + QString* threadString = &infoStrings[tindex]; + + if (!threadString->isEmpty()) + { + infoText = *threadString; + emit resultReady(RESULT_UPDATE_TEXT); + } + + info->working = 1; + info->infoText = threadString; + info->of_dir = of_dir; + info->remote = remote; + info->rev = rev; + info->index = i; + progress = (int)(((i * 100) + 1) / rev->file_count); + + emit resultReady(RESULT_UPDATE_TEXT); + pthread_create(thread, NULL, thread_download, info); + } + + for (size_t i = 0; i < THREAD_COUNT; ++i) + { + pthread_t* thread = &download_threads[i]; + if (*thread) + pthread_join(*thread, NULL); + } + + progress = 0; + infoText = QString("Processing"); + emit resultReady(RESULT_UPDATE_TEXT); + + for (size_t i = 0; i < rev->file_count && do_work; ++i) + { + struct file_info* file = &rev->files[i]; + if (file->type != TYPE_MKDIR) + continue; + + progress = (int)(((i * 100) + 1) / rev->file_count); + emit resultReady(RESULT_UPDATE_TEXT); + + size_t len = strlen(of_dir) + strlen(OS_PATH_SEP) + strlen(file->path) + 1; + char* buf = (char*)malloc(len); + snprintf(buf, len, "%s%s%s", of_dir, OS_PATH_SEP, file->path); + makeDir(buf); + free(buf); + } + + for (size_t i = 0; i < rev->file_count && do_work; ++i) + { + struct file_info* file = &rev->files[i]; + + progress = (int)(((i * 100) + 1) / rev->file_count); + emit resultReady(RESULT_UPDATE_TEXT); + + switch (file->type) + { + case TYPE_WRITE: + case TYPE_MKDIR: + { + retval += applyObject(of_dir, file); + } + break; + + case TYPE_DELETE: + { + size_t len = strlen(of_dir) + strlen(OS_PATH_SEP) + strlen(file->path) + 1; + char* buf = (char*)malloc(len); + snprintf(buf, len, "%s%s%s", of_dir, OS_PATH_SEP, file->path); + if (isFile(buf)) + retval += remove(buf); + free(buf); + } + break; + } + } + + if (do_work) + { + removeObjects(of_dir); + setLocalRemote(of_dir, remote); + setLocalRevision(of_dir, remote_rev); + } + + progress = 0; + infoText = QString(""); + emit resultReady(RESULT_UPDATE_TEXT); + + freeRevision(rev); + } + + return retval; +} + + +void Worker::doWork(const enum Worker::Tasks_t ¶meter) { + Results_t result = RESULT_NONE; + + + switch (parameter) + { + case TASK_INVALID: + break; + + case TASK_INIT: + result = RESULT_INIT_FAILURE; + of_dir = getOpenFortressDir(); + + if (of_dir) + { + of_dir_len = strlen(of_dir); + remote = getLocalRemote(of_dir); + + if (remote) + { + remote_len = strlen(remote); + result = RESULT_INIT_COMPLETE; + } + else + { + free(of_dir); + of_dir = NULL; + remote = NULL; + } + } + else + { + of_dir = NULL; + remote = NULL; + } + + break; + + case TASK_IS_INSTALLED: + result = getRevision() > -1 ? RESULT_IS_INSTALLED : RESULT_IS_NOT_INSTALLED; + break; + + case TASK_IS_UPTODATE: + result = isOutdated() ? RESULT_IS_OUTDATED : RESULT_IS_UPTODATE; + break; + + case TASK_UNINSTALL: + result = RESULT_UNINSTALL_FAILURE; + //if (direxists) result = svn_delete(mod) ? RESULT_UNINSTALL_FAILURE : RESULT_UNINSTALL_COMPLETE; + break; + + case TASK_INSTALL: + result = update_setup(0, getRemoteRevision()) > 0 ? RESULT_INSTALL_FAILURE : RESULT_INSTALL_COMPLETE; + break; + + case TASK_UPDATE: + result = update_setup(getRevision(), getRemoteRevision()) > 0 ? RESULT_UPDATE_FAILURE : RESULT_UPDATE_COMPLETE; + break; + + case TASK_UPDATE_RUN: + result = update_setup(getRevision(), getRemoteRevision()) > 0 ? RESULT_UPDATE_FAILURE : RESULT_UPDATE_COMPLETE; + break; + + case TASK_RUN: + result = getSteamPID() > -1 ? RESULT_EXIT : RESULT_NO_STEAM; + if (result == RESULT_EXIT) runOpenFortress(); + break; + } + + emit resultReady(result); +}
\ No newline at end of file diff --git a/src/qt/workers.hpp b/src/qt/workers.hpp new file mode 100644 index 0000000..a31d9eb --- /dev/null +++ b/src/qt/workers.hpp @@ -0,0 +1,94 @@ +#ifndef WORKERS_HPP +#define WORKERS_HPP + +#include <QObject> +#include <limits.h> + +QT_BEGIN_NAMESPACE +namespace Ui { class MainWindow; } +QT_END_NAMESPACE + +class Worker : public QObject +{ + Q_OBJECT + +private: + char* of_dir; + size_t of_dir_len; + + char* remote; + size_t remote_len; + + bool do_work = true; + +public: + int progress = -1; + QString infoText; + + Worker(); + ~Worker(); + + QString getOfDir(); + QString getRemote(); + void setRemote(QString); + + int getRevision(); + int getRemoteRevision(); + bool isOutdated(); + + void stop_work(); + + int update_setup(int, int); + + enum Tasks_t + { + TASK_INVALID, + + TASK_IS_INSTALLED, + TASK_IS_UPTODATE, + + TASK_INIT, + TASK_INSTALL, + TASK_UNINSTALL, + TASK_UPDATE, + TASK_UPDATE_RUN, + TASK_RUN, + }; + Q_ENUM(Tasks_t) + + enum Results_t + { + RESULT_NONE, + RESULT_EXIT, + + RESULT_UPDATE_TEXT, + + RESULT_IS_INSTALLED, + RESULT_IS_NOT_INSTALLED, + RESULT_IS_UPTODATE, + RESULT_IS_OUTDATED, + + RESULT_INIT_COMPLETE, + RESULT_INIT_FAILURE, + RESULT_INSTALL_COMPLETE, + RESULT_INSTALL_FAILURE, + RESULT_UNINSTALL_COMPLETE, + RESULT_UNINSTALL_FAILURE, + RESULT_UPDATE_COMPLETE, + RESULT_UPDATE_FAILURE, + RESULT_UPDATE_RUN, + + RESULT_NO_STEAM + }; + Q_ENUM(Results_t) + +public slots: + void doWork(const Tasks_t &); + +signals: + void resultReady(const Results_t &); + +}; + + +#endif
\ No newline at end of file diff --git a/src/steam.c b/src/steam.c new file mode 100644 index 0000000..4213768 --- /dev/null +++ b/src/steam.c @@ -0,0 +1,178 @@ +#include <stdlib.h> +#include <string.h> +#include <limits.h> +#include <unistd.h> +#include <dirent.h> +#include <stdio.h> + +#include "steam.h" + +#ifdef _WIN32 +#include <windows.h> +#include <tlhelp32.h> +#endif + +/** + * Returns a heap allocated path to the main steam directory + * If a problem occurs returns NULL + */ +char* getSteamDir() +{ +#if defined(__linux__) + char* home = getenv("HOME"); + + if (!home || !isDir(home)) + return NULL; + + size_t len = strlen(home) + strlen(FLATPAK_DIR); + char* path = malloc(len+1); + + strncpy(path, home, len); + strncat(path, STEAM_DIR, len-strlen(path)); + + if (isDir(path)) + return path; + + strncpy(path, home, len); + strncat(path, FLATPAK_DIR, len-strlen(path)); + + if (isDir(path)) + return path; + + free(path); +#elif defined(_WIN32) + size_t size = PATH_MAX; + char* path = malloc(size+1); + if (!path) + return NULL; + + LSTATUS res = RegGetValueA(HKEY_LOCAL_MACHINE, REG_PATH, "InstallPath", RRF_RT_REG_SZ, NULL, path, (LPDWORD)&size); + + if (res == ERROR_SUCCESS && isDir(path)) + return path; + + strncpy(path, STEAM_PGRM_64, size); + path[size] = '\0'; + + if (isDir(path)) + return path; + + strncpy(path, STEAM_PGRM_86, size); + path[size] = '\0'; + + if (isDir(path)) + return path; + + free(path); + +#else + #error No Implementation +#endif + return NULL; +} + +/** + * Returns a heap allocated path to the sourcemod dirctory + * If a problem occurs returns NULL + */ +char* getSourcemodDir() +{ + char* steam = getSteamDir(); + if (!steam) + return NULL; + + steam = realloc(steam, strlen(steam) + strlen(OS_PATH_SEP) + strlen(SOURCEMOD_DIR) + 1); + strcat(steam, OS_PATH_SEP); + strcat(steam, SOURCEMOD_DIR); + + return steam; +} + +char* getOpenFortressDir() +{ + char* sm_dir = getSourcemodDir(); + if (!sm_dir) + return NULL; + + sm_dir = realloc(sm_dir, strlen(sm_dir) + strlen(OPEN_FORTRESS_DIR) + 1); + strcat(sm_dir, OPEN_FORTRESS_DIR); + + return sm_dir; +} + +/** + * function to fetch the PID of a running Steam process. + * If none were found returns -1 + */ +long getSteamPID() +{ +#ifdef __linux__ + long pid; + char buf[PATH_MAX]; + struct dirent* ent; + DIR* proc = opendir("/proc"); + FILE* stat; + + if (proc) + { + while ((ent = readdir(proc)) != NULL) + { + long lpid = atol(ent->d_name); + if (!lpid) continue; + + snprintf(buf, sizeof(buf), "/proc/%ld/stat", lpid); + stat = fopen(buf, "r"); + + if (stat && (fscanf(stat, "%li (%[^)])", &pid, buf)) == 2) + { + if (!strcmp(buf, STEAM_PROC)) + { + fclose(stat); + closedir(proc); + return pid; + } + fclose(stat); + } + } + + closedir(proc); + } + +#elif _WIN32 + HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + PROCESSENTRY32 pe32 = {0}; + pe32.dwSize = sizeof(PROCESSENTRY32); + Process32First(hSnap,&pe32); + + while(Process32Next(hSnap,&pe32)) + { + if (!strcmp(pe32.szExeFile, STEAM_PROC)) + return (long)pe32.th32ProcessID; + } +#else + #error No Implementation +#endif + return -1; +} + +#define STEAM_LAUNCH + +int runOpenFortress() +{ +#ifdef STEAM_LAUNCH + #ifdef _WIN32 + return system("start steam://rungameid/11677091221058336806"); + #else + return system("xdg-open steam://rungameid/11677091221058336806"); + #endif + +#else + char* of_dir = getOpenFortressDir(); + char* steam = getSteamDir(); + steam = realloc(steam, strlen(steam) + strlen(OS_PATH_SEP) + strlen(STEAM_BIN) + 1); + strcat(steam, OS_PATH_SEP); + strcat(steam, STEAM_BIN); + + return execl(steam, steam, "-applaunch", "243750", "-game", of_dir, "-secure", "-steam", NULL); +#endif +} diff --git a/src/steam.h b/src/steam.h new file mode 100644 index 0000000..9c24337 --- /dev/null +++ b/src/steam.h @@ -0,0 +1,47 @@ +#ifndef STEAM_H +#define STEAM_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include <stddef.h> +#include <stdbool.h> + +#include "fs.h" + +#ifdef _WIN32 +#define STEAM_PROC "steam.exe" +#define STEAM_BIN OS_PATH_SEP STEAM_PROC +#else +#define STEAM_PROC "steam" +#define STEAM_BIN "steam.sh" +#endif + +#define STEAM_APPID "243750" + +#ifdef _WIN32 +#define _STEAM_NAME "Steam" +#define STEAM_PGRM_64 "C:\\Program Files (x86)\\" _STEAM_NAME +#define STEAM_PGRM_86 "C:\\Program Files\\" _STEAM_NAME +// TODO check if this is the right registry path for x86 +#define REG_PATH "SOFTWARE\\Wow6432Node\\Valve\\Steam" +#else // _WIN32 +#define STEAM_DIR "/.local/share/Steam" +#define FLATPAK_DIR "/.var/app/com.valvesoftware.Steam" STEAM_DIR +#endif + +#define SOURCEMOD_DIR "steamapps" OS_PATH_SEP "sourcemods" OS_PATH_SEP +#define OPEN_FORTRESS_DIR "open_fortress" + +char* getSteamDir(); +char* getSourcemodDir(); +char* getOpenFortressDir(); +long getSteamPID(); +int runOpenFortress(); + +#ifdef __cplusplus +} +#endif + +#endif
\ No newline at end of file diff --git a/src/toast.c b/src/toast.c new file mode 100644 index 0000000..34d28b9 --- /dev/null +++ b/src/toast.c @@ -0,0 +1,651 @@ +#include <stdlib.h> +#include <stddef.h> +#include <string.h> +#include <stdio.h> +#include <assert.h> +#include <json.h> +#include <md5.h> + +#if defined(_WIN32) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include <windows.h> +#endif + +#include "fs.h" +#include "net.h" +#include "toast.h" + +#ifdef TOAST_DEFAULT_REMOTE +#define TOAST_DEFAULT_REMOTE "http://toast.openfortress.fun/toast" +#endif + +#define TVN_DIR ".tvn" +#define LOCAL_REMOTE "remote" +#define LOCAL_REVISION "revision" +#define LOCAL_OBJECTS "objects" +#define LOCAL_REMOTE_PATH TVN_DIR OS_PATH_SEP LOCAL_REMOTE +#define LOCAL_REVISION_PATH TVN_DIR OS_PATH_SEP LOCAL_REVISION +#define LOCAL_OBJECTS_PATH TVN_DIR OS_PATH_SEP LOCAL_OBJECTS +#define OLD_LOCAL_REVISION_PATH ".revision" + +#define OBJECTS_ENDPOINT "objects" +#define REVISIONS_ENDPOINT "revisions" +#define LATEST_ENDPOINT REVISIONS_ENDPOINT "/latest" + +const char* TYPE_STRINGS[] = { + "Add/Modify", + "Create Directory", + "Delete" +}; + +static int fileHash(char*, char*); + +char* getToastDir(char* dir) +{ + if (!dir) + return NULL; + + size_t len = strlen(dir) + strlen(OS_PATH_SEP) + strlen(TVN_DIR) + 1; + char* tvn_dir = malloc(len + 1); + if (!tvn_dir) + return NULL; + + tvn_dir[0] = '\0'; + snprintf(tvn_dir, len, "%s%s%s", dir, OS_PATH_SEP, TVN_DIR); + + if (!isDir(tvn_dir)) + { + makeDir(tvn_dir); +#if defined(_WIN32) + SetFileAttributesA(tvn_dir, FILE_ATTRIBUTE_HIDDEN); +#endif + } + + + return tvn_dir; +} + +/** + * Returns a the latest Revision available on the server. + * If none could be fetched returns -1. + */ +int getLatestRemoteRevision(char* url) +{ + assert(url); + int revision = -1; + size_t len = strlen(url) + 1 + strlen(LATEST_ENDPOINT)+1; + + char* latest_url = malloc(len); + if (!latest_url) + return revision; + + snprintf(latest_url, len, "%s/%s", url, LATEST_ENDPOINT); + + struct MemoryStruct* latest_data = downloadToRam(latest_url); + free(latest_url); + + if (latest_data) + { + latest_data->memory = realloc(latest_data->memory, latest_data->size+1); + latest_data->memory[latest_data->size] = '\0'; + + revision = atoi((char*)latest_data->memory); + freeDownload(latest_data); + } + + return revision; +} + +/** + * Returns the current revision the local install is on + * If none could be found returns -1. + */ +int getLocalRevision(char* dir) +{ + int revision = -1; + + if (!dir || !isDir(dir)) + return revision; + + size_t len = strlen(dir) + strlen(OS_PATH_SEP) + strlen(LOCAL_REVISION_PATH); + char* revision_path = malloc(len + 1); + if (!revision_path) + return revision; + + // LEGACY BEHAVIOR + strncpy(revision_path, dir, len); + strncat(revision_path, OS_PATH_SEP, len - strlen(revision_path)); + strncat(revision_path, OLD_LOCAL_REVISION_PATH, len - strlen(revision_path)); + + if (!isFile(revision_path)) + { + revision_path[len-strlen(LOCAL_REVISION_PATH)] = '\0'; + strncat(revision_path, LOCAL_REVISION_PATH, len - strlen(revision_path)); + if (!isFile(revision_path)) + { + free(revision_path); + return revision; + } + } + + FILE* fd = fopen(revision_path, "r"); + free(revision_path); + + if (!fd) + return revision; + + fseek(fd, 0L, SEEK_END); + size_t fd_size = (size_t)ftell(fd); + fseek(fd, 0L, SEEK_SET); + + char* buf = malloc(fd_size+1); + fread(buf, sizeof(char), fd_size, fd); + buf[fd_size] = '\0'; + + fclose(fd); + + revision = atoi(buf); + free(buf); + + return revision; +} + +/** + * Write a revision number to disk + */ +void setLocalRevision(char* dir, int rev) +{ + char* revision_path = getToastDir(dir); + if (!revision_path) + return; + + // cleanup legacy behavior + { + char* old_revision_path = malloc(strlen(revision_path) + strlen(OLD_LOCAL_REVISION_PATH)); + strcpy(old_revision_path, revision_path); + + char* op = old_revision_path + strlen(old_revision_path); + while (*op != *OS_PATH_SEP) --op; + *++op = '\0'; + + strcat(old_revision_path, OLD_LOCAL_REVISION_PATH); + + if (isFile(old_revision_path)) + remove(old_revision_path); + + free(old_revision_path); + } + + size_t len = strlen(revision_path) + strlen(OS_PATH_SEP) + strlen(LOCAL_REVISION) + 1; + revision_path = realloc(revision_path, len); + + if (!isDir(revision_path)) + makeDir(revision_path); + + strncat(revision_path, OS_PATH_SEP, len - strlen(revision_path)); + strncat(revision_path, LOCAL_REVISION, len - strlen(revision_path)); + + size_t rev_len = 1 + 1; + { + + int rev_copy = rev; + while (rev_copy >= 10) + { + rev_copy /= 10; + rev_len++; + } + } + char* revision_string = malloc(rev_len); + if (!revision_string) + return; + + snprintf(revision_string, rev_len, "%i", rev); + + FILE* fd = fopen(revision_path, "w"); + free(revision_path); + if (!fd) + return; + + fwrite(revision_string, sizeof(char), rev_len-1, fd); + fclose(fd); + + free(revision_string); +} + +/** + * Get the remote URL for a local install + * If remote cannot be found returns TOAST_DEFAULT_REMOTE + * + * Results need to be freed + */ +char* getLocalRemote(char* dir) +{ + if (!dir || !isDir(dir)) + return strdup(TOAST_DEFAULT_REMOTE); + + size_t len = strlen(dir) + strlen(OS_PATH_SEP) + strlen(LOCAL_REMOTE_PATH); + char* remote_path = malloc(len + 1); + if (!remote_path) + return strdup(TOAST_DEFAULT_REMOTE); + + strncpy(remote_path, dir, len); + strncat(remote_path, OS_PATH_SEP, len - strlen(remote_path)); + strncat(remote_path, LOCAL_REMOTE_PATH, len - strlen(remote_path)); + + if (!isFile(remote_path)) + { + free(remote_path); + return strdup(TOAST_DEFAULT_REMOTE); + } + + FILE* fd = fopen(remote_path, "rb"); + free(remote_path); + + if (!fd) + return strdup(TOAST_DEFAULT_REMOTE); + + fseek(fd, 0L, SEEK_END); + size_t fd_size = (size_t)ftell(fd); + fseek(fd, 0L, SEEK_SET); + + char* buf = malloc(fd_size+1); + fread(buf, sizeof(char), fd_size, fd); + buf[fd_size] = '\0'; + + char* bufp = buf; + while (*bufp != '\0') + { + if (*bufp == '\n') + *bufp = '\0'; + ++bufp; + } + if (buf[strlen(buf)] == '/') + buf[strlen(buf)] = '\0'; + + fclose(fd); + + return buf; +} + +/** + * Write a remote URL to disk + */ +void setLocalRemote(char* dir, char* remote) +{ + char* remote_path = getToastDir(dir); + if (!remote_path) + return; + + size_t len = strlen(remote_path) + strlen(OS_PATH_SEP) + strlen(LOCAL_REMOTE) + 1; + remote_path = realloc(remote_path, len); + + if (!isDir(remote_path)) + makeDir(remote_path); + + strncat(remote_path, OS_PATH_SEP, len - strlen(remote_path)); + strncat(remote_path, LOCAL_REMOTE, len - strlen(remote_path)); + + FILE* fd = fopen(remote_path, "w"); + free(remote_path); + + fwrite(remote, sizeof(char), strlen(remote), fd); + + fclose(fd); +} + +/** + * Gets revision data at the specified API url for the specified revision number + * Returns NULL on error + * + * Results need to be freed + */ +struct revision_t* getRevisionData(char* url, int rev) +{ + if (!url) + return NULL; + + size_t rev_len = 1; + { + + int rev_copy = rev; + while (rev_copy > 10) + { + rev_copy /= 10; + rev_len++; + } + } + + size_t len = strlen(url) + 1 + strlen(REVISIONS_ENDPOINT) + 1 + rev_len + 1; + char* buf = malloc(len); + snprintf(buf, len, "%s/%s/%i", url, REVISIONS_ENDPOINT, rev); + + struct json_object* revision_list = fetchJSON(buf); + free(buf); + + if (!revision_list) + return NULL; + + struct revision_t* revision = malloc(sizeof(struct revision_t)); + if (!revision) + { + json_object_put(revision_list); + return NULL; + } + + revision->file_count = (size_t)json_object_array_length(revision_list); + revision->files = malloc(sizeof(struct file_info) * revision->file_count); + + struct json_object* temp; + for (size_t i = 0; i < revision->file_count; ++i) + { + struct json_object* file = json_object_array_get_idx(revision_list, i); + + json_object_object_get_ex(file, "type", &temp); + assert(temp); + revision->files[i].type = json_object_get_int(temp); + + json_object_object_get_ex(file, "path", &temp); + assert(temp); + revision->files[i].path = strdup(json_object_get_string(temp)); + + json_object_object_get_ex(file, "hash", &temp); + if (temp) + revision->files[i].hash = strdup(json_object_get_string(temp)); + else + revision->files[i].hash = NULL; + + json_object_object_get_ex(file, "object", &temp); + if (temp) + revision->files[i].object = strdup(json_object_get_string(temp)); + else + revision->files[i].object = NULL; + } + + json_object_put(revision_list); + + return revision; +} + +/** + * Get all revisions between two numbers and merges them together as far as possible + * Returns NULL on error + * + * Results need to be freed + */ +struct revision_t* fastFowardRevisions(char* url, int from, int to) +{ + struct revision_t* rev = NULL; + + for (int rev_num = from; rev_num <= to; ++rev_num) + { + struct revision_t* cur_rev = getRevisionData(url, rev_num); + + if (!rev) + { + rev = cur_rev; + continue; + } + + for (size_t j = 0; j < cur_rev->file_count; ++j) + { + for (size_t i = 0; i < rev->file_count; ++i) + { + if (!strcmp(rev->files[i].path, cur_rev->files[j].path)) + { + rev->files[i].type = cur_rev->files[j].type; + + if (rev->files[i].hash) + free(rev->files[i].hash); + + if (cur_rev->files[j].hash) + rev->files[i].hash = strdup(cur_rev->files[j].hash); + else + rev->files[i].hash = NULL; + + if (rev->files[i].object) + free(rev->files[i].object); + + if (cur_rev->files[j].object) + rev->files[i].object = strdup(cur_rev->files[j].object); + else + rev->files[i].object = NULL; + } + } + + rev->files = realloc(rev->files, sizeof(struct file_info) * (rev->file_count+1)); + + rev->files[rev->file_count].type = cur_rev->files[j].type; + + if (cur_rev->files[j].path) + rev->files[rev->file_count].path = strdup(cur_rev->files[j].path); + else + rev->files[rev->file_count].path = NULL; + + + if (cur_rev->files[j].hash) + rev->files[rev->file_count].hash = strdup(cur_rev->files[j].hash); + else + rev->files[rev->file_count].hash = NULL; + + if (cur_rev->files[j].object) + rev->files[rev->file_count].object = strdup(cur_rev->files[j].object); + else + rev->files[rev->file_count].object = NULL; + + rev->file_count++; + } + + freeRevision(cur_rev); + } + return rev; +} + +/** + * Frees a revision from the heap + */ +void freeRevision(struct revision_t* rev) +{ + if (!rev) return; + + for (size_t i = 0; i < rev->file_count; ++i) + { + free(rev->files[i].path); + free(rev->files[i].hash); + free(rev->files[i].object); + } + free(rev->files); + free(rev); +} + + +/** + * Downloads an object from the specified API url to the specified game dir + * + * Returns bytes written + */ +size_t downloadObject(char* dir, char* url, struct file_info* info) +{ + size_t retval = 0; + if (!info) + return retval; + + char* object = info->object; + + char* buf_path = getToastDir(dir); + if (!buf_path) + return retval; + + size_t len = strlen(buf_path) + strlen(OS_PATH_SEP) + strlen(LOCAL_OBJECTS) + strlen(OS_PATH_SEP) + strlen(object) + 1; + buf_path = realloc(buf_path, len); + + strncat(buf_path, OS_PATH_SEP, len - strlen(buf_path)); + strncat(buf_path, LOCAL_OBJECTS, len - strlen(buf_path)); + strncat(buf_path, OS_PATH_SEP, len - strlen(buf_path)); + + if (!isDir(buf_path)) + makeDir(buf_path); + + strcat(buf_path, object); + + // the object has the right hash so we ignore + if (isFile(buf_path) && !fileHash(buf_path, info->hash)) + { + free(buf_path); + return 0; + } + + len = strlen(url) + 1 + strlen(OBJECTS_ENDPOINT) + 1 + strlen(object) + 1; + char* buf_url = malloc(len); + snprintf(buf_url, len, "%s/%s/%s", url, OBJECTS_ENDPOINT, object); + + retval = downloadToFile(buf_url, buf_path); + + free(buf_url); + free(buf_path); + + return retval; +} + +/** + * Moves an object from the temporary object directory to its proper place + * + * Returns + * 0 on success + * 1 on missing object + * 2 on general failure + */ +int applyObject(char* path, struct file_info* info) +{ + if (!info) + return 2; + + char* file = info->path; + char* object = info->object; + + if (!path || !isDir(path)) + return 2; + else if (!object) + return 0; + + size_t len = strlen(path) + strlen(OS_PATH_SEP) + strlen(LOCAL_OBJECTS_PATH) + strlen(OS_PATH_SEP) + strlen(object) + 1; + char* buf_obj = malloc(len); + snprintf(buf_obj, len, "%s%s%s%s%s", path, OS_PATH_SEP, LOCAL_OBJECTS_PATH, OS_PATH_SEP, object); + + if (!isFile(buf_obj)) + { + free(buf_obj); + len = strlen(path) + strlen(OS_PATH_SEP) + strlen(info->path) + 1; + buf_obj = malloc(len); + snprintf(buf_obj, len, "%s%s%s", path, OS_PATH_SEP, info->path); + + int exists = isFile(buf_obj); + free(buf_obj); + + return !exists; + } + + len = strlen(path) + strlen(OS_PATH_SEP) + strlen(file) + 1; + char* buf_file = malloc(len); + snprintf(buf_file, len, "%s%s%s", path, OS_PATH_SEP, file); + + rename(buf_obj, buf_file); + + free(buf_obj); + free(buf_file); + + return 0; +} + +/** + * Removes temporarily stored objects + */ +void removeObjects(char* path) +{ + if (!path || !isDir(path)) + return; + + size_t len = strlen(path) + strlen(OS_PATH_SEP) + strlen(LOCAL_OBJECTS_PATH) + strlen(OS_PATH_SEP) + 1; + char* buf = malloc(len); + snprintf(buf, len, "%s%s%s%s", path, OS_PATH_SEP, LOCAL_OBJECTS_PATH, OS_PATH_SEP); + + removeDir(buf); + + free(buf); +} + +/** + * Internal function to generate check if a file matches a hash + */ +static int fileHash(char* path, char* hash) +{ + FILE* fd = fopen(path, "rb"); + if (!fd) + return 1; + + fseek(fd, 0L, SEEK_END); + size_t fd_size = (size_t)ftell(fd); + fseek(fd, 0L, SEEK_SET); + + MD5_CTX context; + MD5Init(&context); + + if (fd_size) + { + char* buf = malloc(fd_size); + if (!buf) + { + fclose(fd); + return 1; + } + + fread(buf, sizeof(char), fd_size, fd); + + MD5Update(&context, buf, fd_size); + free(buf); + } + fclose(fd); + + unsigned char digest[16]; + MD5Final(digest, &context); + + char md5string[33]; + for(int i = 0; i < 16; ++i) + snprintf(&md5string[i*2], 3, "%02x", (unsigned int)digest[i]); + + if (!strncmp(md5string, hash, sizeof(md5string))) + return 0; + + return 1; +} + +/** + * Verifies the (md5) hash of a given file + * + * Returns + * 0 on success + * 1 on failure + */ +int verifyFileHash(char* path, struct file_info* info) +{ + if (!info) + return 1; + + char* file = info->path; + char* hash = info->hash; + + if (!path || !isDir(path)) + return 1; + + size_t len = strlen(path) + strlen(OS_PATH_SEP) + strlen(file) + 1; + char* buf = malloc(len); + if (!buf) + return 1; + snprintf(buf, len, "%s%s%s", path, OS_PATH_SEP, file); + + int hash_matches = fileHash(buf, hash); + free(buf); + + return hash_matches; +} diff --git a/src/toast.h b/src/toast.h new file mode 100644 index 0000000..5a99fbb --- /dev/null +++ b/src/toast.h @@ -0,0 +1,51 @@ +#ifndef TOAST_H +#define TOAST_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include <stddef.h> + +enum TYPE_ENUM { + TYPE_WRITE = 0, + TYPE_MKDIR = 1, + TYPE_DELETE = 2, +}; + +extern const char* TYPE_STRINGS[]; + +struct file_info { + enum TYPE_ENUM type; + char* path; + char* hash; + char* object; +}; + +struct revision_t { + struct file_info* files; + size_t file_count; +}; + +char* getToastDir(char* dir); +int getLatestRemoteRevision(char*); +int getLocalRevision(char*); +void setLocalRevision(char*, int); +char* getLocalRemote(char*); +void setLocalRemote(char*, char*); + +struct revision_t* getRevisionData(char*, int); +struct revision_t* fastFowardRevisions(char* url, int from, int to); +void freeRevision(struct revision_t* rev); + +size_t downloadObject(char*, char*, struct file_info*); +int applyObject(char*, struct file_info*); +void removeObjects(char*); + +int verifyFileHash(char*, struct file_info*); + +#ifdef __cplusplus +} +#endif + +#endif
\ No newline at end of file |