diff --git a/.gitignore b/.gitignore index d49fcf1..305b6fa 100644 --- a/.gitignore +++ b/.gitignore @@ -36,9 +36,11 @@ x64/ x32/ Documentation/ .vs/ -src/nStaller/nStaller.dir/ -src/nUpdater/nUpdater.dir/ +src/Installer/Installer.dir/ src/nSuite/nSuite.dir/ +src/Uninstaller/Uninstaller.dir/ +src/Unpacker/Unpacker.dir/ +src/Updater/Updater.dir/ app/ # Other diff --git a/CMakeLists.txt b/CMakeLists.txt index 50e38fb..e261141 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,3 +1,6 @@ +#################### +### nSuite Tools ### +#################### cmake_minimum_required(VERSION 3.0) project(nStallerTools) @@ -8,19 +11,29 @@ set (PROJECT_BIN ${CMAKE_SOURCE_DIR}) set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP") # Include directories of dependencies for entire project -include_directories ( ${LZ4_DIR}/lib/ - ${CORE_DIR} ) +include_directories( + ${LZ4_DIR}/lib/ + ${CORE_DIR} +) + +# Add libraries common throughout entire project +link_libraries( + debug ${LZ4_DIR}/x64_Debug/liblz4_static.lib + optimized ${LZ4_DIR}/x64_Release/liblz4_static.lib + Gdiplus.lib +) -link_libraries ( debug ${LZ4_DIR}/x64_Debug/liblz4_static.lib - optimized ${LZ4_DIR}/x64_Release/liblz4_static.lib ) - # add all sub-projects and plugins here -add_subdirectory( "src/nStaller" ) -add_subdirectory( "src/nUpdater" ) +add_subdirectory( "src/Installer" ) +add_subdirectory( "src/Uninstaller" ) +add_subdirectory( "src/Unpacker" ) +add_subdirectory( "src/Updater" ) add_subdirectory( "src/nSuite" ) + +# Visual studio specific setting: make nSuite the startup project set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT nSuite) # Enable folder structure -set_property (GLOBAL PROPERTY USE_FOLDERS ON) +set_property(GLOBAL PROPERTY USE_FOLDERS ON) # ALL_BUILD, ZERO_CHECK, and other build functions' folder -set_property (GLOBAL PROPERTY PREDEFINED_TARGETS_FOLDER Build_Functions) \ No newline at end of file +set_property(GLOBAL PROPERTY PREDEFINED_TARGETS_FOLDER Build_Functions) \ No newline at end of file diff --git a/README.md b/README.md index 66ee323..7e8fae1 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,36 @@ -# nStaller Tools +# nSuite Directory Tools -This project is both a library and a toolset that allows developers to generate and distribute portable installers for a given input directory, as well as diff the contents of 2 input directories into a single patch file. -The library includes functions to compress/decompress and diff/patch both files and directoires. -The toolset wraps this functionality into a few example programs. +This toolset provides users with the ability to package and diff directories and files. -## nSuite.exe -The nSuite tool is intended to be used by developers or those who wish to package/diff/distribute one or many files. It is run by command-line, and requires one of the following sets of arguments to be fulfilled: -- #### `-installer -src= -dst=` - - Packages + compress an entire **source** directory into a **destionation** installer *.exe file* (filename optional) - -- #### `-pack -src= -dst=` - - Same as the installer, though doesn't embed into an installer, just a *.npack* file (filename optional) - - Package files can be read-through by the diffing command, so many versions of a directory can be easily stored on disk in single snapshots. + +## Packaging +nSuite can package directories in 3 ways: +- A fully fledged installer with a GUI (Windows) + - Customizable by writing attributes into a manifest file + - Generates an uninstaller (adds it to the registry) + - .npack file embedded within -- #### `-unpack -src= -dst=` - - Decompresses the files held in the **source** *.npack* file, dumping into the **destination** directory. +- A lightweight portable package/installer + - Extracts to a folder in the directory it runs from + - Runs in a terminal, no user input + - Doesn't modify registry - no uninstaller + - .npack file embedded within -- #### `-diff -old= -new= -dst=` - - Finds all the common, new, and old files between the **old** and **new** directories. All common files are analyzed bytewise for their differences, and patch instructions are generated. All instructions are compressed and stored in a **destination** *.ndiff* file. ***All*** files are hashed, to ensure the right versions of files are consumed at patch-time. - - Can use *.npack* files as the **old** ***or*** **new** directories (serving as snapshots). Diffing packages means storing less files and folders across multiple versions on disk -> just 1 snapshot per version. +- A .npack file + - Can be unpacked using nSuite + - - #### `-patch -src= -dst=` - - Uses a **source** *.ndiff* file and executes all the instructions contained within, patching the **destination** directory. All files within the directory are hashed, and must match the hashes found in the patch. Additionally, the post-patch results must match what's expected in the hash. If any of the strict conditions aren't met, it will halt prior to any files being modified (preventing against file corruption). - - -## Installer -The installer tool is a portable version of the unpack command, and is generated by nSuite. Each one will have a custom *.npack* file embedded within. The installer is very basic and installs to the directory it runs from. +## Diffing +nSuite can also be used to generate patch files. These can be applied to a directory using nSuite, or by using our stand-alone updater tool (also provided). + +The updater tool automatically applies all .ndiff files it can find next to it, and if successfull, deletes them afterwards. This tool is a naiive implementation of an updater, and would ideally be expanded on by other developers for real-world use. -## Updater -The updater tool is a portable version of the patch command. It automatically applies all *.ndiff* files it can find, and if successfull, deletes them after. This tool is a naiive implementation of an updater, and would ideally be expanded on by other developers. For instance, if patches were found that would modify an app from v.1 -> v.2 -> v.3 -> v.1, the updater won't try to stop at v.3 (as there are no version headers applied to patches). Further, this updater cannot connect to any servers to fetch patch data, but that would be the next logical step after implementing versioning. +The tool and diff files should be kept at the root of an affected directory. It will attempt to apply all patches it can find, even if the patched version is technically 'older'. # Dependencies/Requirements - - 64-bit only - - Might only work in Windows + - C++ 17 + - 64-bit + - Windows 7/8/10 - Uses [CMake](https://cmake.org/) - Requires the [LZ4 - Compression Library](https://github.com/lz4/lz4) to build, but **does not** come bundled with it - - Using BSD-3-Clause license + - Using BSD-3-Clause license \ No newline at end of file diff --git a/src/BufferTools.cpp b/src/BufferTools.cpp index a62a165..e0759fb 100644 --- a/src/BufferTools.cpp +++ b/src/BufferTools.cpp @@ -1,4 +1,5 @@ #include "BufferTools.h" +#include "Common.h" #include "Instructions.h" #include "Threader.h" #include "lz4.h" @@ -7,32 +8,40 @@ bool BFT::CompressBuffer(char * sourceBuffer, const size_t & sourceSize, char ** destinationBuffer, size_t & destinationSize) { - // Allocate enough room for the compressed buffer (2 size_t bigger than source buffer) - destinationSize = (sourceSize * 2ull) + size_t(sizeof(size_t)); - *destinationBuffer = new char[destinationSize]; - - // First chunk of data = the total uncompressed size - *reinterpret_cast(*destinationBuffer) = sourceSize; + // Pre-allocate a huge buffer to allow for compression OPS + char * compressedBuffer = new char[sourceSize * 2ull]; - // Increment pointer so that the compression works on the remaining part of the buffer - *destinationBuffer = reinterpret_cast(*destinationBuffer) + size_t(sizeof(size_t)); - - // Compress the buffer + // Try to compress the source buffer auto result = LZ4_compress_default( sourceBuffer, - *destinationBuffer, + compressedBuffer, int(sourceSize), - int(destinationSize - size_t(sizeof(size_t))) + int(sourceSize * 2ull) ); - // Decrement pointer - *destinationBuffer = reinterpret_cast(*destinationBuffer) - size_t(sizeof(size_t)); - destinationSize = size_t(result) + sizeof(size_t); + // Create the final buffer (done separate b/c we now know the final reduced buffer size) + constexpr size_t HEADER_SIZE = size_t(sizeof(size_t)); + destinationSize = HEADER_SIZE + size_t(result); + *destinationBuffer = new char[destinationSize]; + char * HEADER_ADDRESS = *destinationBuffer; + char * DATA_ADDRESS = HEADER_ADDRESS + HEADER_SIZE; + + // Header = the total uncompressed size + *reinterpret_cast(HEADER_ADDRESS) = sourceSize; + + // Data = compressed source buffer + std::memcpy(DATA_ADDRESS, compressedBuffer, size_t(result)); + delete[] compressedBuffer; + return (result > 0); } bool BFT::DecompressBuffer(char * sourceBuffer, const size_t & sourceSize, char ** destinationBuffer, size_t & destinationSize) { + // Ensure buffer at least *exists* + if (sourceSize <= size_t(sizeof(size_t)) || sourceBuffer == nullptr) + return false; + destinationSize = *reinterpret_cast(sourceBuffer); *destinationBuffer = new char[destinationSize]; auto result = LZ4_decompress_safe( @@ -48,7 +57,7 @@ bool BFT::DecompressBuffer(char * sourceBuffer, const size_t & sourceSize, char bool BFT::DiffBuffers(char * buffer_old, const size_t & size_old, char * buffer_new, const size_t & size_new, char ** buffer_diff, size_t & size_diff, size_t * instructionCount) { std::vector instructions; - instructions.reserve(std::max(size_old, size_new) / 8ull); + instructions.reserve(std::max(size_old, size_new) / 8ull); std::mutex instructionMutex; Threader threader; constexpr size_t amount(4096); @@ -186,7 +195,7 @@ bool BFT::DiffBuffers(char * buffer_old, const size_t & size_old, char * buffer_ // We only care about repeats larger than 36 bytes. if (inst->newData.size() > 36ull) { // Upper limit (mx and my) reduced by 36, since we only care about matches that exceed 36 bytes - size_t max = std::min(inst->newData.size(), inst->newData.size() - 37ull); + size_t max = std::min(inst->newData.size(), inst->newData.size() - 37ull); for (size_t x = 0ull; x < max; ++x) { const auto & value_at_x = inst->newData[x]; if (inst->newData[x + 36ull] != value_at_x) @@ -227,7 +236,7 @@ bool BFT::DiffBuffers(char * buffer_old, const size_t & size_old, char * buffer_ writeGuard.release(); x = ULLONG_MAX; // require overflow, because we want next itteration for x == 0 - max = std::min(inst->newData.size(), inst->newData.size() - 37ull); + max = std::min(inst->newData.size(), inst->newData.size() - 37ull); break; } x = y - 1; diff --git a/src/BufferTools.h b/src/BufferTools.h index 8e0e63c..5202ec4 100644 --- a/src/BufferTools.h +++ b/src/BufferTools.h @@ -5,21 +5,30 @@ /** Namespace to keep buffer-related operations grouped together. */ namespace BFT { - /** Compresses a source buffer into an equal or smaller sized destination buffer. + /** Compresses a source buffer into an equal or smaller-sized destination buffer. + After compression, it applies a small header describing how large the uncompressed buffer is. + --------------- + | buffer data | + --------------- + v + ----------------------------------------- + | compression header | compressed data | + ----------------------------------------- @param sourceBuffer the original buffer to read from. - @param sourceSize the size in bytes of the source buffer. + @param sourceSize the size of the source buffer in bytes. @param destinationBuffer pointer to the destination buffer, which will hold compressed contents. @param destinationSize reference updated with the size in bytes of the compressed destinationBuffer. @return true if compression success, false otherwise. */ bool CompressBuffer(char * sourceBuffer, const size_t & sourceSize, char ** destinationBuffer, size_t & destinationSize); - /** Decompressess a source buffer into an equal or larger sized destination buffer. + /** Decompressess a source buffer into an equal or larger-sized destination buffer. @param sourceBuffer the original buffer to read from. - @param sourceSize the size in bytes of the source buffer. + @param sourceSize the size of the source buffer in bytes. @param destinationBuffer pointer to the destination buffer, which will hold decompressed contents. @param destinationSize reference updated with the size in bytes of the decompressed destinationBuffer. @return true if decompression success, false otherwise. */ bool DecompressBuffer(char * sourceBuffer, const size_t & sourceSize, char ** destinationBuffer, size_t & destinationSize); - /** Processes both input buffers, differentiating them, generating an output (compressed) diff-buffer, ready to be written to disk. + /** Processes two input buffers, diffing them. + Generates a compressed instruction set dictating how to get from the old buffer to the new buffer. @note caller expected to clean-up buffer_diff on their own @param buffer_old the older of the 2 buffers. @param size_old the size of the old buffer. @@ -27,21 +36,21 @@ namespace BFT { @param size_new the size of the new buffer. @param buffer_diff pointer to store the diff buffer at. @param size_diff reference updated with the size of the compressed diff buffer. - @param instructionCount optional pointer to update with the number of instructions processed. + @param instructionCount (optional) pointer to update with the number of instructions processed. @return true if diff success, false otherwise. */ bool DiffBuffers(char * buffer_old, const size_t & size_old, char * buffer_new, const size_t & size_new, char ** buffer_diff, size_t & size_diff, size_t * instructionCount = nullptr); - /** Uses a compressed diff buffer to patch a source buffer into an updated destination buffer + /** Reads from a compressed instruction set, uses it to patch the 'older' buffer into the 'newer' buffer @note caller expected to clean-up buffer_new on their own @param buffer_old the older of the 2 buffers. @param size_old the size of the old buffer. @param buffer_new pointer to store the newer of the 2 buffers. @param size_new reference updated with the size of the new buffer. - @param buffer_diff the compressed diff buffer. + @param buffer_diff the compressed diff buffer (instruction set). @param size_diff the size of the compressed diff buffer. - @param instructionCount optional pointer to update with the number of instructions processed. + @param instructionCount (optional) pointer to update with the number of instructions processed. @return true if patch success, false otherwise. */ bool PatchBuffer(char * buffer_old, const size_t & size_old, char ** buffer_new, size_t & size_new, char * buffer_diff, const size_t & size_diff, size_t * instructionCount = nullptr); - /** Generate a hash value for the buffer provided. + /** Generates a hash value for the buffer provided, using the buffers' contents. @param buffer pointer to the buffer to hash. @param size the size of the buffer. @return hash value for the buffer. */ diff --git a/src/Common.h b/src/Common.h index 1cb04a3..3df359f 100644 --- a/src/Common.h +++ b/src/Common.h @@ -2,14 +2,89 @@ #ifndef COMMON_H #define COMMON_H +#include "TaskLogger.h" #include #include #include #include +#include #include +#include #include +#include +/** Changes an input string to lower case, and returns it. +@param string the input string. +@return lower case version of the string. */ +inline static std::string string_to_lower(const std::string & string) +{ + std::string input = string; + std::transform(input.begin(), input.end(), input.begin(), [](const int & character){ return static_cast(::tolower(character)); }); + return input; +} + +/** Converts the input string into a wide string. +@param str the input string. +@return wide string version of the string. */ +inline static std::wstring to_wideString(const std::string & str) +{ + if (!str.empty()) { + if (const auto size_needed = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, NULL, 0)) { + std::wstring wstr(size_needed, 0); + MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, &wstr[0], size_needed); + return wstr; + } + } + return std::wstring(); +} + +/** Converts the input wide string into a string. +@param str the input wide string. +@return string version of the wide string. */ +inline static std::string from_wideString(const std::wstring & wstr) +{ + if (!wstr.empty()) { + if (const auto size_needed = WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), NULL, 0, NULL, NULL)) { + std::string str(size_needed, 0); + WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), &str[0], size_needed, NULL, NULL); + return str; + } + } + return std::string(); +} + +/** Creates a shortcut file for the paths chosen. +@param srcPath path to the target file that the shortcut will link to. +@param wrkPath path to the working directory for the shortcut. +@param dstPath path to where the shortcut should be placed. */ +inline static void create_shortcut(const std::string & srcPath, const std::string & wrkPath, const std::string & dstPath) +{ + IShellLink* psl; + if (SUCCEEDED(CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&psl))) { + IPersistFile* ppf; + + // Set the path to the shortcut target and add the description. + psl->SetPath(srcPath.c_str()); + psl->SetWorkingDirectory(wrkPath.c_str()); + psl->SetIconLocation(srcPath.c_str(), 0); + + // Query IShellLink for the IPersistFile interface, used for saving the + // shortcut in persistent storage. + if (SUCCEEDED(psl->QueryInterface(IID_IPersistFile, (LPVOID*)&ppf))) { + WCHAR wsz[MAX_PATH]; + + // Ensure that the string is Unicode. + MultiByteToWideChar(CP_ACP, 0, (dstPath + ".lnk").c_str(), -1, wsz, MAX_PATH); + + // Save the link by calling IPersistFile::Save. + ppf->Save(wsz, TRUE); + ppf->Release(); + } + psl->Release(); + } +} + /** Increment a pointer's address by the offset provided. @param ptr the pointer to increment by the offset amount. @param offset the offset amount to apply to the pointer's address. @@ -19,14 +94,17 @@ inline static void * PTR_ADD(void *const ptr, const size_t & offset) return static_cast(ptr) + offset; }; -/** Cleans up a target string representing a file path, removing leading and trailing quotes. -@param path reference to the path to be sanitized. */ -inline static void sanitize_path(std::string & path) +/** Cleans up a target string representing a file path +@param path reference to the path to be sanitized. +@return sanitized version of path. */ +inline static std::string sanitize_path(const std::string & path) { - if (path.front() == '"' || path.front() == '\'') - path.erase(0ull, 1ull); - if (path.back() == '"' || path.back() == '\'') - path.erase(path.size() - 1ull); + std::string cpy(path); + while (cpy.front() == '"' || cpy.front() == '\'' || cpy.front() == '\"' || cpy.front() == '\\') + cpy.erase(0ull, 1ull); + while (cpy.back() == '"' || cpy.back() == '\'' || cpy.back() == '\"' || cpy.back() == '\\') + cpy.erase(cpy.size() - 1ull); + return cpy; } /** Return file-info for all files within the directory specified. @@ -35,12 +113,35 @@ inline static void sanitize_path(std::string & path) inline static auto get_file_paths(const std::string & directory) { std::vector paths; - for (const auto & entry : std::filesystem::recursive_directory_iterator(directory)) - if (entry.is_regular_file()) - paths.emplace_back(entry); + if (std::filesystem::is_directory(directory)) + for (const auto & entry : std::filesystem::recursive_directory_iterator(directory)) + if (entry.is_regular_file()) + paths.emplace_back(entry); return paths; } +/** Retrieve the start menu directory. +@return path to the user's start menu. */ +inline static std::string get_users_startmenu() +{ + // Get the running directory + char cPath[FILENAME_MAX]; + if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_COMMON_PROGRAMS, NULL, 0, cPath))) + return std::string(cPath); + return std::string(); +} + +/** Retrieve the desktop directory. +@return path to the user's desktop. */ +inline static std::string get_users_desktop() +{ + // Get the running directory + char cPath[FILENAME_MAX]; + if (SHGetSpecialFolderPathA(HWND_DESKTOP, cPath, CSIDL_DESKTOP, FALSE)) + return std::string(cPath); + return std::string(); +} + /** Retrieve the directory this executable is running out-of. @return the current directory of this program. */ inline static std::string get_current_directory() @@ -56,7 +157,7 @@ inline static std::string get_current_directory() @param message the message to write-out.*/ inline static void exit_program(const char * message) { - std::cout << message; + TaskLogger::PushText(message); system("pause"); exit(EXIT_FAILURE); } @@ -65,7 +166,7 @@ inline static void exit_program(const char * message) @param message pause message to show the user. */ inline static void pause_program(const char * message) { - std::cout << message << " "; + TaskLogger::PushText(std::string(message) + ' '); system("pause"); std::printf("\033[A\33[2K\r"); std::printf("\033[A\33[2K\r\n"); diff --git a/src/DirectoryTools.cpp b/src/DirectoryTools.cpp index 373e5ea..3ea696d 100644 --- a/src/DirectoryTools.cpp +++ b/src/DirectoryTools.cpp @@ -2,14 +2,16 @@ #include "BufferTools.h" #include "Common.h" #include "Instructions.h" +#include "Resource.h" #include "Threader.h" +#include "TaskLogger.h" #include #include #include #include -bool DRT::CompressDirectory(const std::string & srcDirectory, char ** packBuffer, size_t & packSize, size_t & fileCount) +bool DRT::CompressDirectory(const std::string & srcDirectory, char ** packBuffer, size_t & packSize, size_t * byteCount, size_t * fileCount, const std::vector & exclusions) { // Variables Threader threader; @@ -23,45 +25,63 @@ bool DRT::CompressDirectory(const std::string & srcDirectory, char ** packBuffer files.reserve(directoryArray.size()); // Get path name - const auto srcPath = std::filesystem::path(srcDirectory); - if (!std::filesystem::is_directory(srcPath) || !srcPath.has_stem()) { - std::cout << "Critical failure: the source path specified \"" << srcDirectory << "\" is not a (useable) directory.\n"; + if (!directoryArray.size()) { + TaskLogger::PushText("Critical failure: the source path specified \"" + srcDirectory + "\" is not a (useable) directory.\r\n"); return false; } - const auto folderName = srcPath.stem().string(); + std::filesystem::path srcPath = std::filesystem::path(srcDirectory); + std::string folderName = srcPath.stem().string(); + while (folderName.empty()) { + srcPath = srcPath.parent_path(); + folderName = srcPath.stem().string(); + }; // Calculate final file size using all the files in this directory, // and make a list containing all relevant files and their attributes - size_t archiveSize = sizeof(size_t) + folderName.size(); // include size of the root folder name + size_t archiveSize(0ull); for each (const auto & entry in directoryArray) { + const auto extension = std::filesystem::path(entry).extension(); auto path = entry.path().string(); path = path.substr(absolute_path_length, path.size() - absolute_path_length); - const auto pathSize = path.size(); - - const size_t unitSize = - size_t(sizeof(size_t)) + // size of path size variable in bytes - pathSize + // the actual path data - size_t(sizeof(size_t)) + // size of the file size variable in bytes - entry.file_size(); // the actual file data - archiveSize += unitSize; - files.push_back({ entry.path().string(), path, entry.file_size(), unitSize }); + bool useEntry = true; + for each (const auto & excl in exclusions) { + if (excl.empty()) + continue; + // Compare Paths + if (path == excl) { + useEntry = false; + break; + } + // Compare Extensions + else if (extension == excl) { + useEntry = false; + break; + } + } + if (useEntry) { + const auto pathSize = path.size(); + + const size_t unitSize = + size_t(sizeof(size_t)) + // size of path size variable in bytes + pathSize + // the actual path data + size_t(sizeof(size_t)) + // size of the file size variable in bytes + entry.file_size(); // the actual file data + archiveSize += unitSize; + files.push_back({ entry.path().string(), path, entry.file_size(), unitSize }); + } } - fileCount = files.size(); + + // Update optional parameters + if (byteCount != nullptr) + *byteCount = archiveSize; + if (fileCount != nullptr) + *fileCount = files.size(); // Create buffer for final file data char * filebuffer = new char[archiveSize]; - // Begin writing data into buffer - void * pointer = filebuffer; - - // Write root folder name and size - auto pathSize = folderName.size(); - memcpy(pointer, reinterpret_cast(&pathSize), size_t(sizeof(size_t))); - pointer = PTR_ADD(pointer, size_t(sizeof(size_t))); - memcpy(pointer, folderName.data(), pathSize); - pointer = PTR_ADD(pointer, pathSize); - // Write file data into the buffer + void * pointer = filebuffer; for each (const auto & file in files) { threader.addJob([file, pointer]() { // Write the total number of characters in the path string, into the archive @@ -95,39 +115,57 @@ bool DRT::CompressDirectory(const std::string & srcDirectory, char ** packBuffer threader.shutdown(); // Compress the archive - if (!BFT::CompressBuffer(filebuffer, archiveSize, packBuffer, packSize)) { - std::cout << "Critical failure: cannot perform compression operation on the set of joined files.\n"; + char * compBuffer(nullptr); + size_t compSize(0ull); + if (!BFT::CompressBuffer(filebuffer, archiveSize, &compBuffer, compSize)) { + TaskLogger::PushText("Critical failure: cannot perform compression operation on the set of joined files.\r\n"); return false; } - - // Clean up delete[] filebuffer; + + // Move compressed buffer data to a new buffer that has a special header + packSize = compSize + sizeof(size_t) + folderName.size(); + *packBuffer = new char[packSize]; + memcpy(&(*packBuffer)[sizeof(size_t) + folderName.size()], compBuffer, compSize); + delete[] compBuffer; + + // Write the header (root folder name and size) + pointer = *packBuffer; + auto pathSize = folderName.size(); + memcpy(pointer, reinterpret_cast(&pathSize), size_t(sizeof(size_t))); + pointer = PTR_ADD(pointer, size_t(sizeof(size_t))); + memcpy(pointer, folderName.data(), pathSize); return true; } -bool DRT::DecompressDirectory(const std::string & dstDirectory, char * packBuffer, const size_t & packSize, size_t & byteCount, size_t & fileCount) +bool DRT::DecompressDirectory(const std::string & dstDirectory, char * packBuffer, const size_t & packSize, size_t * byteCount, size_t * fileCount) { Threader threader; + if (packSize <= 0ull) { + TaskLogger::PushText("Critical failure: package buffer has no content.\r\n"); + return false; + } + + // Read the directory header + char * packBufferOffset = packBuffer; + const auto folderSize = *reinterpret_cast(packBufferOffset); + packBufferOffset = reinterpret_cast(PTR_ADD(packBufferOffset, size_t(sizeof(size_t)))); + const char * folderArray = reinterpret_cast(packBufferOffset); + const auto finalDestionation = sanitize_path(dstDirectory + "\\" + std::string(folderArray, folderSize)); + packBufferOffset = reinterpret_cast(PTR_ADD(packBufferOffset, folderSize)); + char * decompressedBuffer(nullptr); size_t decompressedSize(0ull); - if (!BFT::DecompressBuffer(packBuffer, packSize, &decompressedBuffer, decompressedSize)) { - std::cout << "Critical failure: cannot decompress package file.\n"; + if (!BFT::DecompressBuffer(packBufferOffset, packSize - (size_t(sizeof(size_t)) + folderSize), &decompressedBuffer, decompressedSize)) { + TaskLogger::PushText("Critical failure: cannot decompress package file.\r\n"); return false; } // Begin reading the archive void * readingPtr = decompressedBuffer; - - // Read the root folder name and size - const auto folderSize = *reinterpret_cast(readingPtr); - readingPtr = PTR_ADD(readingPtr, size_t(sizeof(size_t))); - const char * folderArray = reinterpret_cast(readingPtr); - const auto finalDestionation = dstDirectory + "\\" + std::string(folderArray, folderSize); - readingPtr = PTR_ADD(readingPtr, folderSize); - - // Read the archive - size_t bytesRead = sizeof(size_t) + folderSize; - std::atomic_size_t filesWritten(0ull), bytesWritten(0ull); + size_t bytesRead(0ull); + std::atomic_size_t filesWritten(0ull); + TaskLogger::SetRange(decompressedSize + 100); while (bytesRead < decompressedSize) { // Read the total number of characters from the path string, from the archive const auto pathSize = *reinterpret_cast(readingPtr); @@ -135,7 +173,7 @@ bool DRT::DecompressDirectory(const std::string & dstDirectory, char * packBuffe // Read the file path string, from the archive const char * path_array = reinterpret_cast(readingPtr); - const auto path = finalDestionation + std::string(path_array, pathSize); + std::string fullPath = finalDestionation + std::string(path_array, pathSize); readingPtr = PTR_ADD(readingPtr, pathSize); // Read the file size in bytes, from the archive @@ -144,35 +182,44 @@ bool DRT::DecompressDirectory(const std::string & dstDirectory, char * packBuffe // Write file out to disk, from the archive void * ptrCopy = readingPtr; // needed for lambda, since readingPtr gets incremented - threader.addJob([ptrCopy, path, fileSize, &filesWritten, &bytesWritten]() { + threader.addJob([ptrCopy, fullPath, fileSize, &filesWritten]() { // Write-out the file if it doesn't exist yet, if the size is different, or if it's older - std::filesystem::create_directories(std::filesystem::path(path).parent_path()); - std::ofstream file(path, std::ios::binary | std::ios::out); - if (file.is_open()) + std::filesystem::create_directories(std::filesystem::path(fullPath).parent_path()); + std::ofstream file(fullPath, std::ios::binary | std::ios::out); + if (!file.is_open()) + TaskLogger::PushText("Error writing file: \"" + fullPath + "\" to disk"); + else { file.write(reinterpret_cast(ptrCopy), (std::streamsize)fileSize); - file.close(); - bytesWritten += fileSize; + file.close(); + } filesWritten++; }); readingPtr = PTR_ADD(readingPtr, fileSize); bytesRead += size_t(sizeof(size_t)) + pathSize + size_t(sizeof(size_t)) + fileSize; + TaskLogger::PushText("Writing file: \"" + std::string(path_array, pathSize) + "\"\r\n"); + TaskLogger::SetProgress(bytesRead); } // Wait for threaded operations to complete threader.prepareForShutdown(); while (!threader.isFinished()) - continue; + continue; threader.shutdown(); + TaskLogger::SetProgress(bytesRead + 100); + + // Update optional parameters + if (byteCount != nullptr) + *byteCount = bytesRead; + if (fileCount != nullptr) + *fileCount = filesWritten; // Success - fileCount = filesWritten; - byteCount = bytesWritten; delete[] decompressedBuffer; return true; } -bool DRT::DiffDirectory(const std::string & oldDirectory, const std::string & newDirectory, char ** diffBuffer, size_t & diffSize, size_t & instructionCount) +bool DRT::DiffDirectories(const std::string & oldDirectory, const std::string & newDirectory, char ** diffBuffer, size_t & diffSize, size_t * instructionCount) { // Declarations that will only be used here struct File { @@ -213,52 +260,75 @@ bool DRT::DiffDirectory(const std::string & oldDirectory, const std::string & ne size += srcFile.file_size(); files.push_back(new File(path, getRelativePath(path, directory), srcFile.file_size())); } + return true; + } + // See if the file is actually a program with an embedded archive + char * packBuffer(nullptr); + size_t packSize(0ull); + bool canLoad = LoadLibraryA(directory.c_str()); + auto handle = GetModuleHandle(directory.c_str()); + Resource fileResource(IDR_ARCHIVE, "ARCHIVE", handle); + if (canLoad && handle != NULL && fileResource.exists()) { + // Extract the archive + packBuffer = reinterpret_cast(fileResource.getPtr()); + packSize = fileResource.getSize(); } else { - // Treat as a snapshot file if it isn't a directory + // Last resort, treat as a snapshot file if it isn't a directory // Open diff file std::ifstream packFile(directory, std::ios::binary | std::ios::beg); - const size_t packSize = std::filesystem::file_size(directory); + packSize = std::filesystem::file_size(directory); if (!packFile.is_open()) { - std::cout << "Critical failure: cannot read package file.\n"; + TaskLogger::PushText("Critical failure: cannot read package file.\r\n"); return false; } - char * compBuffer = new char[packSize]; - packFile.read(compBuffer, std::streamsize(packSize)); + packBuffer = new char[packSize]; + packFile.read(packBuffer, std::streamsize(packSize)); packFile.close(); + } + // We don't care about the package's header (folder name + name size) + char * packBufferOffset = packBuffer; + const auto folderSize = *reinterpret_cast(packBuffer); + packBufferOffset = reinterpret_cast(PTR_ADD(packBufferOffset, size_t(sizeof(size_t)))); + packBufferOffset = reinterpret_cast(PTR_ADD(packBufferOffset, folderSize)); + + // Decompress + size_t snapSize(0ull); + if (!BFT::DecompressBuffer(packBufferOffset, packSize - (size_t(sizeof(size_t)) + folderSize), snapshot, snapSize)) { + TaskLogger::PushText("Critical failure: cannot decompress package file.\r\n"); + return false; + } - // Decompress - size_t snapSize(0ull); - if (!BFT::DecompressBuffer(compBuffer, packSize, snapshot, snapSize)) { - std::cout << "Critical failure: cannot decompress package file.\n"; - return false; - } - delete[] compBuffer; - - // Get lists of all files involved - size_t bytesRead(0ull); - void * ptr = *snapshot; - while (bytesRead < snapSize) { - // Read the total number of characters from the path string, from the archive - const auto pathSize = *reinterpret_cast(ptr); - ptr = PTR_ADD(ptr, size_t(sizeof(size_t))); - - // Read the file path string, from the archive - const char * path_array = reinterpret_cast(ptr); - const auto path = std::string(path_array, pathSize); - ptr = PTR_ADD(ptr, pathSize); - - // Read the file size in bytes, from the archive - const auto fileSize = *reinterpret_cast(ptr); - ptr = PTR_ADD(ptr, size_t(sizeof(size_t))); - - files.push_back(new FileMem(path, path, fileSize, ptr)); - ptr = PTR_ADD(ptr, fileSize); - bytesRead += size_t(sizeof(size_t)) + pathSize + size_t(sizeof(size_t)) + fileSize; - size += fileSize; - } + // Check if we need to delete the packBuffer, or if it is an embedded resource + // If it's an embedded resource, the fileResource's destructor will satisfy, else do below + if (!canLoad || handle == NULL || !fileResource.exists()) + delete[] packBuffer; + else if (canLoad && handle != NULL) + FreeLibrary(handle); + + // Get lists of all files involved + size_t bytesRead(0ull); + void * ptr = *snapshot; + while (bytesRead < snapSize) { + // Read the total number of characters from the path string, from the archive + const auto pathSize = *reinterpret_cast(ptr); + ptr = PTR_ADD(ptr, size_t(sizeof(size_t))); + + // Read the file path string, from the archive + const char * path_array = reinterpret_cast(ptr); + const auto path = std::string(path_array, pathSize); + ptr = PTR_ADD(ptr, pathSize); + + // Read the file size in bytes, from the archive + const auto fileSize = *reinterpret_cast(ptr); + ptr = PTR_ADD(ptr, size_t(sizeof(size_t))); + + files.push_back(new FileMem(path, path, fileSize, ptr)); + ptr = PTR_ADD(ptr, fileSize); + bytesRead += size_t(sizeof(size_t)) + pathSize + size_t(sizeof(size_t)) + fileSize; + size += fileSize; } - return true; + return true; }; static constexpr auto getFileLists = [](const std::string & oldDirectory, char ** oldSnapshot, const std::string & newDirectory, char ** newSnapshot, size_t & reserveSize, PathPairList & commonFiles, PathList & addFiles, PathList & delFiles) { // Get files @@ -342,6 +412,7 @@ bool DRT::DiffDirectory(const std::string & oldDirectory, const std::string & ne vecBuffer.reserve(reserveSize); // These files are common, maybe some have changed + size_t instructionNum(0ull); for each (const auto & cFiles in commonFiles) { char * oldBuffer(nullptr), * newBuffer(nullptr); size_t oldHash(0ull), newHash(0ull); @@ -350,8 +421,8 @@ bool DRT::DiffDirectory(const std::string & oldDirectory, const std::string & ne // Files are different versions char * buffer(nullptr); size_t size(0ull); - if (BFT::DiffBuffers(oldBuffer, cFiles.first->size, newBuffer, cFiles.second->size, &buffer, size, &instructionCount)) { - std::cout << "diffing file \"" << cFiles.first->relative << "\"\n"; + if (BFT::DiffBuffers(oldBuffer, cFiles.first->size, newBuffer, cFiles.second->size, &buffer, size, &instructionNum)) { + TaskLogger::PushText("Diffing file: \"" + cFiles.first->relative + "\"\r\n"); writeInstructions(cFiles.first->relative, oldHash, newHash, buffer, size, 'U', vecBuffer); } delete[] buffer; @@ -371,8 +442,8 @@ bool DRT::DiffDirectory(const std::string & oldDirectory, const std::string & ne if (nFile->open(&newBuffer, newHash)) { char * buffer(nullptr); size_t size(0ull); - if (BFT::DiffBuffers(nullptr, 0ull, newBuffer, nFile->size, &buffer, size, &instructionCount)) { - std::cout << "adding file \"" << nFile->relative << "\"\n"; + if (BFT::DiffBuffers(nullptr, 0ull, newBuffer, nFile->size, &buffer, size, &instructionNum)) { + TaskLogger::PushText("Adding file: \"" + nFile->relative + "\"\r\n"); writeInstructions(nFile->relative, 0ull, newHash, buffer, size, 'N', vecBuffer); } delete[] buffer; @@ -388,8 +459,8 @@ bool DRT::DiffDirectory(const std::string & oldDirectory, const std::string & ne char * oldBuffer(nullptr); size_t oldHash(0ull); if (oFile->open(&oldBuffer, oldHash)) { - instructionCount++; - std::cout << "removing file \"" << oFile->relative << "\"\n"; + instructionNum++; + TaskLogger::PushText("Removing file: \"" + oFile->relative + "\"\r\n"); writeInstructions(oFile->relative, oldHash, 0ull, nullptr, 0ull, 'D', vecBuffer); } delete[] oldBuffer; @@ -400,18 +471,22 @@ bool DRT::DiffDirectory(const std::string & oldDirectory, const std::string & ne // Compress final buffer if (!BFT::CompressBuffer(vecBuffer.data(), vecBuffer.size(), diffBuffer, diffSize)) { - std::cout << "Critical failure: cannot compress diff file.\n"; + TaskLogger::PushText("Critical failure: cannot compress diff file.\r\n"); return false; } + + // Update optional parameters + if (instructionCount != nullptr) + *instructionCount = instructionNum; return true; } -bool DRT::PatchDirectory(const std::string & dstDirectory, char * diffBufferCompressed, const size_t & diffSizeCompressed, size_t & bytesWritten, size_t & instructionsUsed) +bool DRT::PatchDirectory(const std::string & dstDirectory, char * diffBufferCompressed, const size_t & diffSizeCompressed, size_t * bytesWritten, size_t * instructionsUsed) { char * diffBuffer(nullptr); size_t diffSize(0ull); if (!BFT::DecompressBuffer(diffBufferCompressed, diffSizeCompressed, &diffBuffer, diffSize)) { - std::cout << "Critical failure: cannot decompress diff file.\n"; + TaskLogger::PushText("Critical failure: cannot decompress diff file.\r\n"); return false; } @@ -486,6 +561,7 @@ bool DRT::PatchDirectory(const std::string & dstDirectory, char * diffBufferComp } // Patch all files first + size_t byteNum(0ull), instructionNum(0ull); for each (const auto & file in diffFiles) { // Try to read the target file char *oldBuffer(nullptr), *newBuffer(nullptr); @@ -493,39 +569,43 @@ bool DRT::PatchDirectory(const std::string & dstDirectory, char * diffBufferComp // Try to read source file if (!readFile(file.fullPath, oldSize, &oldBuffer, oldHash)) { - std::cout << "Critical failure: Cannot read source file from disk.\n"; + TaskLogger::PushText("Critical failure: Cannot read source file from disk.\r\n"); return false; } // Patch if this source file hasn't been patched yet if (oldHash == file.diff_newHash) - std::cout << "The file \"" << file.path << "\" is already up to date, skipping...\n"; + TaskLogger::PushText("The file \"" + file.path + "\" is already up to date, skipping...\r\n"); else if (oldHash != file.diff_oldHash) { - std::cout << "Critical failure: the file \"" << file.path << "\" is of an unexpected version. \n"; + TaskLogger::PushText("Critical failure: the file \"" + file.path + "\" is of an unexpected version. \r\n"); return false; } else { // Patch buffer - std::cout << "patching file \"" << file.path << "\"\n"; + TaskLogger::PushText("patching file \"" + file.path + "\"\r\n"); size_t newSize(0ull); - BFT::PatchBuffer(oldBuffer, oldSize, &newBuffer, newSize, file.instructionSet, file.instructionSize, &instructionsUsed); + if (!BFT::PatchBuffer(oldBuffer, oldSize, &newBuffer, newSize, file.instructionSet, file.instructionSize, &instructionNum)) { + TaskLogger::PushText("Critical failure: patching failed!.\r\n"); + return false; + } + const size_t newHash = BFT::HashBuffer(newBuffer, newSize); // Confirm new hashes match if (newHash != file.diff_newHash) { - std::cout << "Critical failure: patched file is corrupted (hash mismatch).\n"; + TaskLogger::PushText("Critical failure: patched file is corrupted (hash mismatch).\r\n"); return false; } // Write patched buffer to disk std::ofstream newFile(file.fullPath, std::ios::binary | std::ios::out); if (!newFile.is_open()) { - std::cout << "Critical failure: cannot write patched file to disk.\n"; + TaskLogger::PushText("Critical failure: cannot write patched file to disk.\r\n"); return false; } newFile.write(newBuffer, std::streamsize(newSize)); newFile.close(); - bytesWritten += newSize; + byteNum += newSize; } // Cleanup and finish @@ -537,30 +617,30 @@ bool DRT::PatchDirectory(const std::string & dstDirectory, char * diffBufferComp // By this point all files matched, safe to add new ones for each (const auto & file in addedFiles) { std::filesystem::create_directories(std::filesystem::path(file.fullPath).parent_path()); - std::cout << "adding file \"" << file.path << "\"\n"; + TaskLogger::PushText("Writing file: \"" + file.path + "\"\r\n"); // Write the 'insert' instructions // Remember that we use the diff/patch function to add new files too char * newBuffer(nullptr); size_t newSize(0ull); - BFT::PatchBuffer(nullptr, 0ull, &newBuffer, newSize, file.instructionSet, file.instructionSize, &instructionsUsed); + BFT::PatchBuffer(nullptr, 0ull, &newBuffer, newSize, file.instructionSet, file.instructionSize, &instructionNum); const size_t newHash = BFT::HashBuffer(newBuffer, newSize); // Confirm new hashes match if (newHash != file.diff_newHash) { - std::cout << "Critical failure: new file is corrupted (hash mismatch).\n"; + TaskLogger::PushText("Critical failure: new file is corrupted (hash mismatch).\r\n"); return false; } // Write new file to disk std::ofstream newFile(file.fullPath, std::ios::binary | std::ios::out); if (!newFile.is_open()) { - std::cout << "Critical failure: cannot write new file to disk.\n"; + TaskLogger::PushText("Critical failure: cannot write new file to disk.\r\n"); return false; } newFile.write(newBuffer, std::streamsize(newSize)); newFile.close(); - bytesWritten += newSize; + byteNum += newSize; // Cleanup and finish delete[] file.instructionSet; @@ -575,14 +655,14 @@ bool DRT::PatchDirectory(const std::string & dstDirectory, char * diffBufferComp // Try to read source file if (!readFile(file.fullPath, oldSize, &oldBuffer, oldHash)) - std::cout << "The file \"" << file.path << "\" has already been removed, skipping...\n"; + TaskLogger::PushText("The file \"" + file.path + "\" has already been removed, skipping...\r\n"); else { // Only remove source files if they match entirely if (oldHash == file.diff_oldHash) if (!std::filesystem::remove(file.fullPath)) - std::cout << "Error: cannot delete file \"" << file.path << "\" from disk, delete this file manually if you can. \n"; + TaskLogger::PushText("Error: cannot delete file \"" + file.path + "\" from disk, delete this file manually if you can. \r\n"); else - std::cout << "removing file \"" << file.path << "\"\n"; + TaskLogger::PushText("Removing file: \"" + file.path + "\"\r\n"); } // Cleanup and finish @@ -590,5 +670,10 @@ bool DRT::PatchDirectory(const std::string & dstDirectory, char * diffBufferComp delete[] oldBuffer; } + // Update optional parameters + if (bytesWritten != nullptr) + *bytesWritten = byteNum; + if (instructionsUsed != nullptr) + *instructionsUsed = instructionNum; return true; } \ No newline at end of file diff --git a/src/DirectoryTools.h b/src/DirectoryTools.h index 23075ab..2050d23 100644 --- a/src/DirectoryTools.h +++ b/src/DirectoryTools.h @@ -3,43 +3,51 @@ #define DIRECTORY_TOOLS_H #include +#include /** Namespace to keep directory-related operations grouped together. */ namespace DRT { - /** Compresses a source directory into an equal or smaller sized destination buffer. + /** Compresses all disk contents found within a source directory into an .npack - package formatted buffer. + After compression, it applies a small header dictating packaged folders' name. + ------------------------------------------------------ + | Directory name header | compressed directory data | + ------------------------------------------------------ @note caller is responsible for cleaning-up packBuffer. @param srcDirectory the absolute path to the directory to compress. @param packBuffer pointer to the destination buffer, which will hold compressed contents. - @param packSize reference updated with the size in bytes of the compressed destinationBuffer. - @param fileCount reference updated with the number of files compressed into the package. + @param packSize reference updated with the size in bytes of the compressed packBuffer. + @param byteCount (optional) pointer updated with the number of bytes written into the package + @param fileCount (optional) pointer updated with the number of files written into the package. + @param exclusions (optional) list of filenames/types to skip. "string" match relative path, ".ext" match extension. @return true if compression success, false otherwise. */ - bool CompressDirectory(const std::string & srcDirectory, char ** packBuffer, size_t & packSize, size_t & fileCount); - /** Decompresses a packaged buffer into a destination directory. - @param dstDirectory the absolute path to the directory to compress. - @param packBuffer the buffer containing the compressed contents. + bool CompressDirectory(const std::string & srcDirectory, char ** packBuffer, size_t & packSize, size_t * byteCount = nullptr, size_t * fileCount = nullptr, const std::vector & exclusions = std::vector()); + /** Decompresses an .npack - package formatted buffer into its component files in the destination directory. + @param dstDirectory the absolute path to the directory to decompress. + @param packBuffer the buffer containing the compressed package contents. @param packSize the size of the buffer in bytes. - @param byteCount reference updated with the number of bytes written to disk. - @param fileCount reference updated with the number of files written to disk. + @param byteCount (optional) pointer updated with the number of bytes written to disk. + @param fileCount (optional) pointer updated with the number of files written to disk. @return true if decompression success, false otherwise. */ - bool DecompressDirectory(const std::string & dstDirectory, char * packBuffer, const size_t & packSize, size_t & byteCount, size_t & fileCount); + bool DecompressDirectory(const std::string & dstDirectory, char * packBuffer, const size_t & packSize, size_t * byteCount = nullptr, size_t * fileCount = nullptr); /** Processes two input directories and generates a compressed instruction set for transforming the old directory into the new directory. @note caller is responsible for cleaning-up diffBuffer. - @param oldDirectory the older directory. - @param newDirectory the newer directory. + @param oldDirectory the older directory or path to an .npack file. + @param newDirectory the newer directory or path to an .npack file. @param diffBuffer pointer to the diff buffer, which will hold compressed diff instructions. @param diffSize reference updated with the size in bytes of the diff buffer. - @param instructionCount reference updated with the number of instructions compressed into the diff buffer. + @param instructionCount (optional) pointer updated with the number of instructions compressed into the diff buffer. @return true if diff success, false otherwise. */ - bool DiffDirectory(const std::string & oldDirectory, const std::string & newDirectory, char ** diffBuffer, size_t & diffSize, size_t & instructionCount); - /** Using a previously-generated diff file, applies the patching procedure to transform an input directory to the 'newer' state. + bool DiffDirectories(const std::string & oldDirectory, const std::string & newDirectory, char ** diffBuffer, size_t & diffSize, size_t * instructionCount = nullptr); + /** Decompresses and executes the instructions contained within a previously - generated diff buffer. + Transforms the contents of an 'old' directory into that of the 'new' directory. @param dstDirectory the destination directory to transform. - @param diffBufferComp the buffer containing the compressed diff instructions. - @param diffSizeComp the size in bytes of the compressed diff buffer. - @param bytesWritten reference updated with the number of bytes written to disk. - @param instructionsUsed reference updated with the number of instructions executed. + @param diffBuffer the buffer containing the compressed diff instructions. + @param diffSize the size in bytes of the compressed diff buffer. + @param bytesWritten (optional) pointer updated with the number of bytes written to disk. + @param instructionsUsed (optional) pointer updated with the number of instructions executed. @return true if patch success, false otherwise. */ - bool PatchDirectory(const std::string & dstDirectory, char * diffBufferComp, const size_t & diffSizeComp, size_t & bytesWritten, size_t & instructionsUsed); + bool PatchDirectory(const std::string & dstDirectory, char * diffBuffer, const size_t & diffSize, size_t * bytesWritten = nullptr, size_t * instructionsUsed = nullptr); }; #endif // DIRECTORY_TOOLS_H \ No newline at end of file diff --git a/src/Installer/CMakeLists.txt b/src/Installer/CMakeLists.txt new file mode 100644 index 0000000..4cd1239 --- /dev/null +++ b/src/Installer/CMakeLists.txt @@ -0,0 +1,61 @@ +################# +### Installer ### +################# +set (Module Installer) + +# Get source files +file (GLOB_RECURSE ROOT RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "*.cpp" "*.c" "*.h" "*.rc") +# Mimic the file-folder structure +foreach(source IN LISTS ROOT) + get_filename_component(source_path "${source}" PATH) + string(REPLACE "/" "\\" source_path_msvc "${source_path}") + source_group("${source_path_msvc}" FILES "${source}") +endforeach() + +# Add source files +add_executable(${Module} + ${ROOT} + ${CORE_DIR}/Common.h + ${CORE_DIR}/Resource.h + ${CORE_DIR}/Threader.h + ${CORE_DIR}/Instructions.h + ${CORE_DIR}/Instructions.cpp + ${CORE_DIR}/BufferTools.h + ${CORE_DIR}/BufferTools.cpp + ${CORE_DIR}/DirectoryTools.h + ${CORE_DIR}/DirectoryTools.cpp + ${CORE_DIR}/TaskLogger.h +) + +# Add libraries +target_link_libraries(${Module} + "Comctl32.lib" + "propsys.lib" + "Shlwapi.lib" +) + +# This module requires the uninstaller to be built first +add_dependencies(Installer Uninstaller) + +# Set windows/visual studio settings +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /SUBSYSTEM:WINDOWS") +target_compile_Definitions(${Module} PRIVATE $<$:DEBUG>) +set_target_properties(${Module} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + PDB_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + VS_DEBUGGER_WORKING_DIRECTORY "$(SolutionDir)app" + LINK_FLAGS "/MANIFESTUAC:\"level='requireAdministrator' uiAccess='false'\"" +) + +# Force highest c++ version supported +if (MSVC_VERSION GREATER_EQUAL "1900") + include(CheckCXXCompilerFlag) + CHECK_CXX_COMPILER_FLAG("/std:c++latest" _cpp_latest_flag_supported) + if (_cpp_latest_flag_supported) + add_compile_options("/std:c++latest") + set_target_properties(${Module} PROPERTIES CXX_STANDARD 17) + set_target_properties(${Module} PROPERTIES CXX_STANDARD_REQUIRED ON) + endif() +endif() \ No newline at end of file diff --git a/src/Installer/Installer.cpp b/src/Installer/Installer.cpp new file mode 100644 index 0000000..fb88f1b --- /dev/null +++ b/src/Installer/Installer.cpp @@ -0,0 +1,350 @@ +#include "Installer.h" +#include "Common.h" +#include "DirectoryTools.h" +#include "TaskLogger.h" +#include +#include +#include +#include +#include +#pragma warning(push) +#pragma warning(disable:4458) +#include +#pragma warning(pop) + +// States used in this GUI application +#include "Screens/Welcome.h" +#include "Screens/Agreement.h" +#include "Screens/Directory.h" +#include "Screens/Install.h" +#include "Screens/Finish.h" +#include "Screens/Fail.h" + + +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_ HINSTANCE, _In_ LPSTR, _In_ int) +{ + CoInitialize(NULL); + Gdiplus::GdiplusStartupInput gdiplusStartupInput; + ULONG_PTR gdiplusToken; + Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL); + Installer installer(hInstance); + + // Main message loop: + MSG msg; + while (GetMessage(&msg, NULL, 0, 0)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Close + CoUninitialize(); + return (int)msg.wParam; +} + +Installer::Installer() + : m_archive(IDR_ARCHIVE, "ARCHIVE"), m_manifest(IDR_MANIFEST, "MANIFEST"), m_threader(1ull) +{ + // Process manifest + if (m_manifest.exists()) { + // Create a string stream of the manifest file + std::wstringstream ss; + ss << reinterpret_cast(m_manifest.getPtr()); + + // Cycle through every line, inserting attributes into the manifest map + std::wstring attrib, value; + while (ss >> attrib && ss >> std::quoted(value)) { + wchar_t * k = new wchar_t[attrib.length() + 1]; + wcscpy_s(k, attrib.length() + 1, attrib.data()); + m_mfStrings[k] = value; + } + } +} + +Installer::Installer(const HINSTANCE hInstance) : Installer() +{ + bool success = true; + // Get user's program files directory + TCHAR pf[MAX_PATH]; + SHGetSpecialFolderPath(0, pf, CSIDL_PROGRAM_FILES, FALSE); + setDirectory(std::string(pf)); + + // Check archive integrity + if (!m_archive.exists()) { + TaskLogger::PushText("Critical failure: archive doesn't exist!\r\n"); + success = false; + } + else { + // Read directory header data + void * pointer = m_archive.getPtr(); + const auto folderSize = *reinterpret_cast(pointer); + pointer = PTR_ADD(pointer, size_t(sizeof(size_t))); + m_packageName = std::string(reinterpret_cast(pointer), folderSize); + pointer = PTR_ADD(pointer, folderSize); + // Read compressed package header + m_maxSize = *reinterpret_cast(reinterpret_cast(pointer)); + + // If no name is found, use the package name (if available) + if (m_mfStrings[L"name"].empty() && !m_packageName.empty()) + m_mfStrings[L"name"] = to_wideString(m_packageName); + } + // Create window class + WNDCLASSEX wcex; + wcex.cbSize = sizeof(WNDCLASSEX); + wcex.style = CS_HREDRAW | CS_VREDRAW; + wcex.lpfnWndProc = WndProc; + wcex.cbClsExtra = 0; + wcex.cbWndExtra = 0; + wcex.hInstance = hInstance; + wcex.hIcon = LoadIcon(hInstance, (LPCSTR)IDI_ICON1); + wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + wcex.lpszMenuName = NULL; + wcex.lpszClassName = "Installer"; + wcex.hIconSm = LoadIcon(wcex.hInstance, IDI_APPLICATION); + if (!RegisterClassEx(&wcex)) { + TaskLogger::PushText("Critical failure: could not create main window.\r\n"); + success = false; + } + else { + // Create window + m_hwnd = CreateWindowW( + L"Installer",(m_mfStrings[L"name"] + L" Installer").c_str(), + WS_OVERLAPPED | WS_VISIBLE | WS_BORDER | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, + CW_USEDEFAULT, CW_USEDEFAULT, + 800, 500, + NULL, NULL, hInstance, NULL + ); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + auto dwStyle = (DWORD)GetWindowLongPtr(m_hwnd, GWL_STYLE); + auto dwExStyle = (DWORD)GetWindowLongPtr(m_hwnd, GWL_EXSTYLE); + RECT rc = { 0, 0, 800, 500 }; + ShowWindow(m_hwnd, true); + UpdateWindow(m_hwnd); + AdjustWindowRectEx(&rc, dwStyle, false, dwExStyle); + SetWindowPos(m_hwnd, NULL, 0, 0, rc.right - rc.left, rc.bottom - rc.top, SWP_NOZORDER | SWP_NOMOVE); + + // The portions of the screen that change based on input + m_screens[WELCOME_SCREEN] = new Welcome(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); + m_screens[AGREEMENT_SCREEN] = new Agreement(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); + m_screens[DIRECTORY_SCREEN] = new Directory(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); + m_screens[INSTALL_SCREEN] = new Install(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); + m_screens[FINISH_SCREEN] = new Finish(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); + m_screens[FAIL_SCREEN] = new Fail(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); + setScreen(WELCOME_SCREEN); + } + +#ifndef DEBUG + if (!success) + invalidate(); +#endif +} + +void Installer::invalidate() +{ + setScreen(FAIL_SCREEN); + m_valid = false; +} + +void Installer::setScreen(const ScreenEnums & screenIndex) +{ + if (m_valid) { + m_screens[m_currentIndex]->setVisible(false); + m_screens[screenIndex]->enact(); + m_screens[screenIndex]->setVisible(true); + m_currentIndex = screenIndex; + RECT rc = { 0, 0, 160, 500 }; + RedrawWindow(m_hwnd, &rc, NULL, RDW_INVALIDATE); + } +} + +std::string Installer::getDirectory() const +{ + return m_directory; +} + +void Installer::setDirectory(const std::string & directory) +{ + m_directory = directory; + try { + const auto spaceInfo = std::filesystem::space(std::filesystem::path(getDirectory()).root_path()); + m_capacity = spaceInfo.capacity; + m_available = spaceInfo.available; + } + catch (std::filesystem::filesystem_error &) { + m_capacity = 0ull; + m_available = 0ull; + } +} + +size_t Installer::getDirectorySizeCapacity() const +{ + return m_capacity; +} + +size_t Installer::getDirectorySizeAvailable() const +{ + return m_available; +} + +size_t Installer::getDirectorySizeRequired() const +{ + return m_maxSize; +} + +std::string Installer::getPackageName() const +{ + return m_packageName; +} + +void Installer::beginInstallation() +{ + m_threader.addJob([&]() { + // Acquire the uninstaller resource + Resource uninstaller(IDR_UNINSTALLER, "UNINSTALLER"), manifest(IDR_MANIFEST, "MANIFEST"); + if (!uninstaller.exists()) { + TaskLogger::PushText("Cannot access installer resource, aborting...\r\n"); + setScreen(Installer::FAIL_SCREEN); + } + else { + // Unpackage using the rest of the resource file + auto directory = sanitize_path(getDirectory()); + if (!DRT::DecompressDirectory(directory, reinterpret_cast(m_archive.getPtr()), m_archive.getSize())) + invalidate(); + else { + // Write uninstaller to disk + const auto fullDirectory = directory + "\\" + m_packageName; + const auto uninstallerPath = fullDirectory + "\\uninstaller.exe"; + std::filesystem::create_directories(std::filesystem::path(uninstallerPath).parent_path()); + std::ofstream file(uninstallerPath, std::ios::binary | std::ios::out); + if (!file.is_open()) { + TaskLogger::PushText("Cannot write uninstaller to disk, aborting...\r\n"); + invalidate(); + } + TaskLogger::PushText("Uninstaller: \"" + uninstallerPath + "\"\r\n"); + file.write(reinterpret_cast(uninstaller.getPtr()), (std::streamsize)uninstaller.getSize()); + file.close(); + + // Update uninstaller's resources + std::string newDir = std::regex_replace(fullDirectory, std::regex("\\\\"), "\\\\"); + const std::string newManifest( + std::string(reinterpret_cast(manifest.getPtr()), manifest.getSize()) + + "\r\ndirectory \"" + newDir + "\"" + ); + auto handle = BeginUpdateResource(uninstallerPath.c_str(), false); + if (!(bool)UpdateResource(handle, "MANIFEST", MAKEINTRESOURCE(IDR_MANIFEST), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPVOID)newManifest.c_str(), (DWORD)newManifest.size())) { + TaskLogger::PushText("Cannot write manifest contents to the uninstaller, aborting...\r\n"); + invalidate(); + } + EndUpdateResource(handle, FALSE); + + // Add uninstaller to system registry + HKEY hkey; + if (RegCreateKeyExW(HKEY_LOCAL_MACHINE, (L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" + m_mfStrings[L"name"]).c_str(), 0, NULL, REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, &hkey, NULL) == ERROR_SUCCESS) { + auto + name = from_wideString(m_mfStrings[L"name"]), + version = from_wideString(m_mfStrings[L"version"]), + publisher = from_wideString(m_mfStrings[L"publisher"]), + icon = from_wideString(m_mfStrings[L"icon"]); + DWORD ONE = 1, SIZE = (DWORD)(m_maxSize/1024ull); + if (icon.empty()) + icon = uninstallerPath; + else + icon = fullDirectory + icon; + RegSetKeyValueA(hkey, 0, "UninstallString", REG_SZ, (LPCVOID)uninstallerPath.c_str(), (DWORD)uninstallerPath.size()); + RegSetKeyValueA(hkey, 0, "DisplayIcon", REG_SZ, (LPCVOID)icon.c_str(), (DWORD)icon.size()); + RegSetKeyValueA(hkey, 0, "DisplayName", REG_SZ, (LPCVOID)name.c_str(), (DWORD)name.size()); + RegSetKeyValueA(hkey, 0, "DisplayVersion", REG_SZ, (LPCVOID)version.c_str(), (DWORD)version.size()); + RegSetKeyValueA(hkey, 0, "InstallLocation", REG_SZ, (LPCVOID)fullDirectory.c_str(), (DWORD)fullDirectory.size()); + RegSetKeyValueA(hkey, 0, "Publisher", REG_SZ, (LPCVOID)publisher.c_str(), (DWORD)publisher.size()); + RegSetKeyValueA(hkey, 0, "NoModify", REG_DWORD, (LPCVOID)&ONE, (DWORD)sizeof(DWORD)); + RegSetKeyValueA(hkey, 0, "NoRepair", REG_DWORD, (LPCVOID)&ONE, (DWORD)sizeof(DWORD)); + RegSetKeyValueA(hkey, 0, "EstimatedSize", REG_DWORD, (LPCVOID)&SIZE, (DWORD)sizeof(DWORD)); + } + RegCloseKey(hkey); + } + } + }); +} + +void Installer::dumpErrorLog() +{ + // Dump error log to disk + const auto dir = get_current_directory() + "\\error_log.txt"; + const auto t = std::time(0); + char dateData[127]; + ctime_s(dateData, 127, &t); + std::string logData(""); + + // If the log doesn't exist, add header text + if (!std::filesystem::exists(dir)) + logData += "Installer error log:\r\n"; + + // Add remaining log data + logData += std::string(dateData) + TaskLogger::PullText() + "\r\n"; + + // Try to create the file + std::filesystem::create_directories(std::filesystem::path(dir).parent_path()); + std::ofstream file(dir, std::ios::binary | std::ios::out | std::ios::app); + if (!file.is_open()) + TaskLogger::PushText("Cannot dump error log to disk...\r\n"); + else + file.write(logData.c_str(), (std::streamsize)logData.size()); + file.close(); +} + +void Installer::paint() +{ + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(m_hwnd, &ps)); + + // Draw Background + const LinearGradientBrush backgroundGradient1( + Point(0, 0), + Point(0, 500), + Color(255, 25, 25, 25), + Color(255, 75, 75, 75) + ); + graphics.FillRectangle(&backgroundGradient1, 0, 0, 170, 500); + + // Draw Steps + const SolidBrush lineBrush(Color(255, 100, 100, 100)); + graphics.FillRectangle(&lineBrush, 28, 0, 5, 500); + constexpr static wchar_t* step_labels[] = { L"Welcome", L"EULA", L"Directory", L"Install", L"Finish" }; + FontFamily fontFamily(L"Segoe UI"); + Font font(&fontFamily, 15, FontStyleBold, UnitPixel); + REAL vertical_offset = 15; + const auto frameIndex = (int)m_currentIndex; + for (int x = 0; x < 5; ++x) { + // Draw Circle + auto color = x < frameIndex ? Color(255, 100, 100, 100) : x == frameIndex ? Color(255, 25, 225, 125) : Color(255, 255, 255, 255); + if (x == 4 && frameIndex == 5) + color = Color(255, 225, 25, 75); + const SolidBrush brush(color); + Pen pen(color); + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawEllipse(&pen, 20, (int)vertical_offset, 20, 20); + graphics.FillEllipse(&brush, 20, (int)vertical_offset, 20, 20); + + // Draw Text + graphics.DrawString(step_labels[x], -1, &font, PointF{ 50, vertical_offset }, &brush); + + if (x == 3) + vertical_offset = 460; + else + vertical_offset += 50; + } + + EndPaint(m_hwnd, &ps); +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + const auto ptr = (Installer*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + if (message == WM_PAINT) + ptr->paint(); + else if (message == WM_DESTROY) + PostQuitMessage(0); + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/Installer/Installer.h b/src/Installer/Installer.h new file mode 100644 index 0000000..cabc8b2 --- /dev/null +++ b/src/Installer/Installer.h @@ -0,0 +1,87 @@ +#pragma once +#ifndef INSTALLER_H +#define INSTALLER_H + +#include "Resource.h" +#include "Threader.h" +#include +#include +#include + + +class Screen; + +/** Encapsulates the logical features of the installer. */ +class Installer { +public: + // Public (de)Constructors + ~Installer() = default; + Installer(const HINSTANCE hInstance); + + + // Public Enumerations + const enum ScreenEnums { + WELCOME_SCREEN, AGREEMENT_SCREEN, DIRECTORY_SCREEN, INSTALL_SCREEN, FINISH_SCREEN, FAIL_SCREEN, + SCREEN_COUNT + }; + + + // Public Methods + /** When called, invalidates the installer, halting it from progressing. */ + void invalidate(); + /** Make the screen identified by the supplied enum as active, deactivating the previous screen. + @param screenIndex the new screen to use. */ + void setScreen(const ScreenEnums & screenIndex); + /** Retrieves the current directory chosen for installation. + @return active installation directory. */ + std::string getDirectory() const; + /** Sets a new installation directory. + @param directory new installation directory. */ + void setDirectory(const std::string & directory); + /** Retrieves the size of the drive used in the current directory. + @return the drive capacity. */ + size_t getDirectorySizeCapacity() const; + /** Retrieves the remaining size of the drive used in the current directory. + @return the available size. */ + size_t getDirectorySizeAvailable() const; + /** Retrieves the required size of the uncompressed package to be installed. + @return the (uncompressed) package size. */ + size_t getDirectorySizeRequired() const; + /** Retrieves the package name. + @return the package name. */ + std::string getPackageName() const; + /** Install the installer's package contents to the directory previously chosen. */ + void beginInstallation(); + /** Dumps error log to disk. */ + static void dumpErrorLog(); + /** Render this window. */ + void paint(); + + + // Public manifest strings + struct compare_string { + bool operator()(const wchar_t * a, const wchar_t * b) const { + return wcscmp(a, b) < 0; + } + }; + std::map m_mfStrings; + + +private: + // Private Constructors + Installer(); + + + // Private Attributes + Threader m_threader; + Resource m_archive, m_manifest; + std::string m_directory = "", m_packageName = ""; + bool m_valid = true; + size_t m_maxSize = 0ull, m_capacity = 0ull, m_available = 0ull; + ScreenEnums m_currentIndex = WELCOME_SCREEN; + Screen * m_screens[SCREEN_COUNT]; + HWND m_hwnd = nullptr; +}; + + +#endif // INSTALLER_H \ No newline at end of file diff --git a/src/Installer/Installer.rc b/src/Installer/Installer.rc new file mode 100644 index 0000000..b7ae4c7 Binary files /dev/null and b/src/Installer/Installer.rc differ diff --git a/src/Installer/README.md b/src/Installer/README.md new file mode 100644 index 0000000..3d28054 --- /dev/null +++ b/src/Installer/README.md @@ -0,0 +1,28 @@ +# Installer +This program is a fully-fledged installation application, made to run on Windows. It generates an uninstaller, and links it up in the user's registry. + +It uses the Windows GDI library for rendering. + +This application has 3 (three) resources embedded within it: + - IDI_ICON1 the application icon + - IDR_ARCHIVE the package to be installed + - IDR_MANIFEST the installer manifest (attributes, strings, instructions) + +The raw version of this application is useless on its own, and is intended to be fullfilled by nSuite using the `-installer` command. +The following is how nSuite uses this application: + - writes out a copy of this application to disk + - packages a directory, embedding the package resource into **this application's** IDR_ARCHIVE resource + - tries to find an installer manifest, embedding it into **this application's** IDR_MANIFEST resource + +This installer has several screens it displays to the user. It starts off with a welcome screen, and requires that the user accept a EULA in order to proceed. +If at any point an error occurs, the program enters a failure state and dumps its entire operation log to disk (next to the program, error_log.txt). + +The installer manifest has optional strings the developer can implement to customize the installation process. *Quotes are required* + - name "string" + - version "string" + - description "string" + - eula "string" + - shortcut "\\relative path within installation directory" + - icon "\\relative path within installation directory" + +Any and all of these manifest values can be omitted. \ No newline at end of file diff --git a/src/Installer/Screens/Agreement.cpp b/src/Installer/Screens/Agreement.cpp new file mode 100644 index 0000000..bc2db9d --- /dev/null +++ b/src/Installer/Screens/Agreement.cpp @@ -0,0 +1,143 @@ +#include "Agreement.h" +#include "../Installer.h" + + +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +Agreement::~Agreement() +{ + UnregisterClass("AGREEMENT_SCREEN", m_hinstance); + DestroyWindow(m_hwnd); + DestroyWindow(m_checkYes); + DestroyWindow(m_btnPrev); + DestroyWindow(m_btnNext); + DestroyWindow(m_btnCancel); +} + +Agreement::Agreement(Installer * installer, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size) + : Screen(installer, pos, size) +{ + // Create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = "AGREEMENT_SCREEN"; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("AGREEMENT_SCREEN", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); + + // Create eula + m_log = CreateWindowExW(WS_EX_CLIENTEDGE, L"edit", m_installer->m_mfStrings[L"eula"].c_str(), WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 75, size.x - 20, size.y - 125, m_hwnd, NULL, hInstance, NULL); + if (m_installer->m_mfStrings[L"eula"].empty()) + SetWindowTextW(m_log, + L"nSuite installers can be created freely by anyone, as such those who generate them are responsible for its contents, not the developers.\r\n" + L"This software is provided as - is, use it at your own risk." + ); + + // Create checkboxes + m_checkYes = CreateWindow("Button", "I accept the terms of this license agreement", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, size.y - 35, 310, 15, m_hwnd, (HMENU)1, hInstance, NULL); + CheckDlgButton(m_hwnd, 1, BST_UNCHECKED); + + // Create Buttons + constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; + m_btnPrev = CreateWindow("BUTTON", "< Back", BUTTON_STYLES | BS_DEFPUSHBUTTON, size.x - 290, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); + m_btnNext = CreateWindow("BUTTON", "Next >", BUTTON_STYLES | BS_DEFPUSHBUTTON, size.x - 200, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); + m_btnCancel = CreateWindow("BUTTON", "Cancel", BUTTON_STYLES, size.x - 95, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); +} + +void Agreement::enact() +{ + EnableWindow(m_btnNext, m_agrees); +} + +void Agreement::paint() +{ + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(m_hwnd, &ps)); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, m_size.y), + Color(50, 25, 125, 225), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, m_size.x, m_size.y); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + SolidBrush blackBrush(Color(255, 0, 0, 0)); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"License Agreement", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + graphics.DrawString(L"Please read the following license agreement:", -1, ®Font, PointF{ 10, 50 }, &blackBrush); + + EndPaint(m_hwnd, &ps); +} + +void Agreement::checkYes() +{ + m_agrees = IsDlgButtonChecked(m_hwnd, 1); + CheckDlgButton(m_hwnd, 1, m_agrees ? BST_CHECKED : BST_UNCHECKED); + EnableWindow(m_btnNext, m_agrees); +} + +void Agreement::goPrevious() +{ + m_installer->setScreen(Installer::ScreenEnums::WELCOME_SCREEN); +} + +void Agreement::goNext() +{ + m_installer->setScreen(Installer::ScreenEnums::DIRECTORY_SCREEN); +} + +void Agreement::goCancel() +{ + PostQuitMessage(0); +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + const auto ptr = (Agreement*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + const auto controlHandle = HWND(lParam); + if (message == WM_PAINT) + ptr->paint(); + else if (message == WM_CTLCOLORSTATIC) { + // Make checkbox text background color transparent + if (controlHandle == ptr->m_checkYes) { + SetBkMode(HDC(wParam), TRANSPARENT); + return (LRESULT)GetStockObject(NULL_BRUSH); + } + SetBkColor(HDC(wParam), RGB(255, 255, 255)); + return (LRESULT)GetStockObject(WHITE_BRUSH); + } + else if (message == WM_COMMAND) { + const auto notification = HIWORD(wParam); + if (notification == BN_CLICKED) { + if (controlHandle == ptr->m_checkYes) + ptr->checkYes(); + else if (controlHandle == ptr->m_btnPrev) + ptr->goPrevious(); + else if (controlHandle == ptr->m_btnNext) + ptr->goNext(); + else if (controlHandle == ptr->m_btnCancel) + ptr->goCancel(); + } + } + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/Installer/Screens/Agreement.h b/src/Installer/Screens/Agreement.h new file mode 100644 index 0000000..a9364f7 --- /dev/null +++ b/src/Installer/Screens/Agreement.h @@ -0,0 +1,37 @@ +#pragma once +#ifndef AGREEMENT_H +#define AGREEMENT_H + +#include "Screen.h" + + +/** This state encapuslates the "Accept the license agreement" - Screen" state. */ +class Agreement : public Screen { +public: + // Public (de)Constructors + ~Agreement(); + Agreement(Installer * installer, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size); + + + // Public Interface Implementations + virtual void enact() override; + virtual void paint() override; + + + // Public Methods + /** Check the 'I agree' button. */ + void checkYes(); + /** Switch to the previous state. */ + void goPrevious(); + /** Switch to the next state. */ + void goNext(); + /** Switch to the cancel state. */ + void goCancel(); + + + // Public Attributes + HWND m_log = nullptr, m_checkYes = nullptr, m_btnPrev = nullptr, m_btnNext = nullptr, m_btnCancel = nullptr; + bool m_agrees = false; +}; + +#endif // AGREEMENT_H \ No newline at end of file diff --git a/src/Installer/Screens/Directory.cpp b/src/Installer/Screens/Directory.cpp new file mode 100644 index 0000000..d9f2f77 --- /dev/null +++ b/src/Installer/Screens/Directory.cpp @@ -0,0 +1,292 @@ +#include "Directory.h" +#include "../Installer.h" +#include +#include +#include +#include +#include + + +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); +static HRESULT CreateDialogEventHandler(REFIID, void **); +static HRESULT OpenFileDialog(std::string &); + +Directory::~Directory() +{ + UnregisterClass("DIRECTORY_SCREEN", m_hinstance); + DestroyWindow(m_hwnd); + DestroyWindow(m_directoryField); + DestroyWindow(m_packageField); + DestroyWindow(m_browseButton); + DestroyWindow(m_btnPrev); + DestroyWindow(m_btnInst); + DestroyWindow(m_btnCancel); +} + +Directory::Directory(Installer * installer, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size) + : Screen(installer, pos, size) +{ + // Create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = "DIRECTORY_SCREEN"; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("DIRECTORY_SCREEN", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); + + // Create directory lookup fields + m_directoryField = CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", m_installer->getDirectory().c_str(), WS_VISIBLE | WS_CHILD | WS_BORDER | ES_AUTOHSCROLL, 10, 150, 400, 25, m_hwnd, NULL, hInstance, NULL); + m_packageField = CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", ("\\" + m_installer->getPackageName()).c_str(), WS_VISIBLE | WS_CHILD | WS_BORDER | ES_LEFT | ES_READONLY, 410, 150, 100, 25, m_hwnd, NULL, hInstance, NULL); + m_browseButton = CreateWindow("BUTTON", "Browse", WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON, 520, 149, 100, 25, m_hwnd, NULL, hInstance, NULL); + + // Create Buttons + constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; + m_btnPrev = CreateWindow("BUTTON", "< Back", BUTTON_STYLES | BS_DEFPUSHBUTTON, size.x - 290, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); + m_btnInst = CreateWindow("BUTTON", "Install >", BUTTON_STYLES | BS_DEFPUSHBUTTON, size.x - 200, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); + m_btnCancel = CreateWindow("BUTTON", "Cancel", BUTTON_STYLES, size.x - 95, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); +} + +void Directory::enact() +{ + // Does nothing +} + +void Directory::paint() +{ + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(m_hwnd, &ps)); + + // Draw Background + SolidBrush solidWhiteBackground(Color(255, 255, 255, 255)); + graphics.FillRectangle(&solidWhiteBackground, 0, 0, 630, 500); + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, m_size.y), + Color(50, 25, 125, 225), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, m_size.x, m_size.y); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + SolidBrush blackBrush(Color(255, 0, 0, 0)); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"Where would you like to install to?", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + graphics.DrawString(L"Choose a folder by pressing the 'Browse' button.", -1, ®Font, PointF{ 10, 100 }, &blackBrush); + graphics.DrawString(L"Alternatively, type a specific directory into the box below.", -1, ®Font, PointF{ 10, 115 }, &blackBrush); + + constexpr static auto readableFileSize = [](const size_t & size) -> std::wstring { + auto remainingSize = (double)size; + constexpr static wchar_t * units[] = { L" B", L" KB", L" MB", L" GB", L" TB", L" PB", L"EB" }; + int i = 0; + while (remainingSize > 1024.00) { + remainingSize /= 1024.00; + ++i; + } + std::wstringstream stream; + stream << std::fixed << std::setprecision(2) << remainingSize; + return stream.str() + units[i] + L" (" + std::to_wstring(size) + L" bytes )"; + }; + graphics.DrawString(L"Disk Space", -1, ®Font, PointF{ 10, 200 }, &blackBrush); + graphics.DrawString((L" Capacity:\t\t\t" + readableFileSize(m_installer->getDirectorySizeCapacity())).c_str(), -1, ®Font, PointF{ 10, 225 }, &blackBrush); + graphics.DrawString((L" Available:\t\t\t" + readableFileSize(m_installer->getDirectorySizeAvailable())).c_str(), -1, ®Font, PointF{ 10, 240 }, &blackBrush); + graphics.DrawString((L" Required:\t\t\t" + readableFileSize(m_installer->getDirectorySizeRequired())).c_str(), -1, ®Font, PointF{ 10, 255 }, &blackBrush); + + + EndPaint(m_hwnd, &ps); +} + +void Directory::browse() +{ + std::string directory(""); + if (SUCCEEDED(OpenFileDialog(directory))) { + if (directory != "" && directory.length() > 2ull) { + m_installer->setDirectory(directory); + SetWindowTextA(m_directoryField, directory.c_str()); + RECT rc = { 10, 200, 600, 300 }; + RedrawWindow(m_hwnd, &rc, NULL, RDW_INVALIDATE); + } + } +} + +void Directory::goPrevious() +{ + m_installer->setScreen(Installer::ScreenEnums::AGREEMENT_SCREEN); +} + +void Directory::goInstall() +{ + const auto directory = m_installer->getDirectory(); + if (directory == "" || directory == " " || directory.length() < 3) + MessageBox( + NULL, + "Please enter a valid directory before proceeding.", + "Invalid path!", + MB_OK | MB_ICONERROR | MB_TASKMODAL + ); + else + m_installer->setScreen(Installer::INSTALL_SCREEN); +} + +void Directory::goCancel() +{ + PostQuitMessage(0); +} + +static HRESULT CreateDialogEventHandler(REFIID riid, void **ppv) +{ + /** File Dialog Event Handler */ + class DialogEventHandler : public IFileDialogEvents, public IFileDialogControlEvents { + private: + ~DialogEventHandler() { }; + long _cRef; + + + public: + // Constructor + DialogEventHandler() : _cRef(1) { }; + + + // IUnknown methods + IFACEMETHODIMP QueryInterface(REFIID riid, void** ppv) { + static const QITAB qit[] = { + QITABENT(DialogEventHandler, IFileDialogEvents), + QITABENT(DialogEventHandler, IFileDialogControlEvents), + { 0 }, + }; + return QISearch(this, qit, riid, ppv); + } + IFACEMETHODIMP_(ULONG) AddRef() { + return InterlockedIncrement(&_cRef); + } + IFACEMETHODIMP_(ULONG) Release() { + long cRef = InterlockedDecrement(&_cRef); + if (!cRef) + delete this; + return cRef; + } + + + // IFileDialogEvents methods + IFACEMETHODIMP OnFileOk(IFileDialog *) { return S_OK; }; + IFACEMETHODIMP OnFolderChange(IFileDialog *) { return S_OK; }; + IFACEMETHODIMP OnFolderChanging(IFileDialog *, IShellItem *) { return S_OK; }; + IFACEMETHODIMP OnHelp(IFileDialog *) { return S_OK; }; + IFACEMETHODIMP OnSelectionChange(IFileDialog *) { return S_OK; }; + IFACEMETHODIMP OnShareViolation(IFileDialog *, IShellItem *, FDE_SHAREVIOLATION_RESPONSE *) { return S_OK; }; + IFACEMETHODIMP OnTypeChange(IFileDialog *) { return S_OK; }; + IFACEMETHODIMP OnOverwrite(IFileDialog *, IShellItem *, FDE_OVERWRITE_RESPONSE *) { return S_OK; }; + + + // IFileDialogControlEvents methods + IFACEMETHODIMP OnItemSelected(IFileDialogCustomize *, DWORD, DWORD) { return S_OK; }; + IFACEMETHODIMP OnButtonClicked(IFileDialogCustomize *, DWORD) { return S_OK; }; + IFACEMETHODIMP OnCheckButtonToggled(IFileDialogCustomize *, DWORD, BOOL) { return S_OK; }; + IFACEMETHODIMP OnControlActivating(IFileDialogCustomize *, DWORD) { return S_OK; }; + }; + + *ppv = NULL; + DialogEventHandler *pDialogEventHandler = new (std::nothrow) DialogEventHandler(); + HRESULT hr = pDialogEventHandler ? S_OK : E_OUTOFMEMORY; + if (SUCCEEDED(hr)) { + hr = pDialogEventHandler->QueryInterface(riid, ppv); + pDialogEventHandler->Release(); + } + return hr; +} + +static HRESULT OpenFileDialog(std::string & directory) +{ + // CoCreate the File Open Dialog object. + IFileDialog *pfd = NULL; + IFileDialogEvents *pfde = NULL; + DWORD dwCookie, dwFlags; + HRESULT hr = S_FALSE; + if ( + SUCCEEDED(CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pfd))) && + SUCCEEDED(CreateDialogEventHandler(IID_PPV_ARGS(&pfde))) && + SUCCEEDED(pfd->Advise(pfde, &dwCookie)) && + SUCCEEDED(pfd->GetOptions(&dwFlags)) && + SUCCEEDED(pfd->SetOptions(dwFlags | FOS_PICKFOLDERS | FOS_OVERWRITEPROMPT | FOS_CREATEPROMPT)) && + SUCCEEDED(pfd->Show(NULL)) + ) + { + // The result is an IShellItem object. + IShellItem *psiResult; + PWSTR pszFilePath = NULL; + if (SUCCEEDED(pfd->GetResult(&psiResult)) && SUCCEEDED(psiResult->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath))) { + std::wstringstream ss; + ss << pszFilePath; + auto ws = ss.str(); + typedef std::codecvt converter_type; + const std::locale locale(""); + const converter_type& converter = std::use_facet(locale); + std::vector to(ws.length() * converter.max_length()); + std::mbstate_t state; + const wchar_t* from_next; + char* to_next; + const converter_type::result result = converter.out(state, ws.data(), ws.data() + ws.length(), from_next, &to[0], &to[0] + to.size(), to_next); + if (result == converter_type::ok || result == converter_type::noconv) { + directory = std::string(&to[0], to_next); + hr = S_OK; + } + CoTaskMemFree(pszFilePath); + psiResult->Release(); + } + + // Unhook the event handler. + pfd->Unadvise(dwCookie); + pfde->Release(); + pfd->Release(); + } + return hr; +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + const auto ptr = (Directory*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + const auto controlHandle = HWND(lParam); + if (message == WM_PAINT) + ptr->paint(); + else if (message == WM_COMMAND) { + const auto notification = HIWORD(wParam); + if (notification == BN_CLICKED) { + if (controlHandle == ptr->m_browseButton) + ptr->browse(); + else if (controlHandle == ptr->m_btnPrev) + ptr->goPrevious(); + else if (controlHandle == ptr->m_btnInst) + ptr->goInstall(); + else if (controlHandle == ptr->m_btnCancel) + ptr->goCancel(); + } + else if (notification == EN_CHANGE) { + if (controlHandle == ptr->m_directoryField) { + // Redraw 'disk space data' region of window when the text field changes + std::vector data(GetWindowTextLength(controlHandle) + 1ull); + GetWindowTextA(controlHandle, &data[0], (int)data.size()); + ptr->m_installer->setDirectory(std::string(data.data())); + RECT rc = { 10, 200, 600, 300 }; + RedrawWindow(hWnd, &rc, NULL, RDW_INVALIDATE); + return S_OK; + } + } + } + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/Installer/Screens/Directory.h b/src/Installer/Screens/Directory.h new file mode 100644 index 0000000..4683357 --- /dev/null +++ b/src/Installer/Screens/Directory.h @@ -0,0 +1,36 @@ +#pragma once +#ifndef DIRECTORY_H +#define DIRECTORY_H + +#include "Screen.h" + + +/** This state encapuslates the "Choose a directory - Screen" state. */ +class Directory : public Screen { +public: + // Public (de)Constructors + ~Directory(); + Directory(Installer * installer, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size); + + + // Public Interface Implementations + virtual void enact() override; + virtual void paint() override; + + + // Public Methods + /** Browse for an installation directory. */ + void browse(); + /** Switch to the previous state. */ + void goPrevious(); + /** Switch to the next state. */ + void goInstall(); + /** Switch to the cancel state. */ + void goCancel(); + + + // Public Attributes + HWND m_directoryField = nullptr, m_packageField = nullptr, m_browseButton = nullptr, m_btnPrev = nullptr, m_btnInst = nullptr, m_btnCancel = nullptr; +}; + +#endif // DIRECTORY_H \ No newline at end of file diff --git a/src/Installer/Screens/Fail.cpp b/src/Installer/Screens/Fail.cpp new file mode 100644 index 0000000..0863fbc --- /dev/null +++ b/src/Installer/Screens/Fail.cpp @@ -0,0 +1,112 @@ +#include "Fail.h" +#include "Common.h" +#include "TaskLogger.h" +#include "../Installer.h" +#include +#include +#include +#include + + +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +Fail::~Fail() +{ + UnregisterClass("FAIL_SCREEN", m_hinstance); + DestroyWindow(m_hwnd); + DestroyWindow(m_hwndLog); + DestroyWindow(m_btnClose); + TaskLogger::RemoveCallback_TextAdded(m_logIndex); +} + +Fail::Fail(Installer * installer, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size) + : Screen(installer, pos, size) +{ + // Create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = "FAIL_SCREEN"; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("FAIL_SCREEN", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); + + // Create error log + m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 75, size.x - 20, size.y - 125, m_hwnd, NULL, hInstance, NULL); + SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)"Error Log:\r\n"); + m_logIndex = TaskLogger::AddCallback_TextAdded([&](const std::string & message) { + SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)message.c_str()); + }); + + // Create Buttons + constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; + m_btnClose = CreateWindow("BUTTON", "Close", BUTTON_STYLES, size.x - 95, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); +} + +void Fail::enact() +{ + Installer::dumpErrorLog(); +} + +void Fail::paint() +{ + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(m_hwnd, &ps)); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, m_size.y), + Color(50, 225, 25, 75), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, m_size.x, m_size.y); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"Installation Incomplete", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + + EndPaint(m_hwnd, &ps); +} + +void Fail::goClose() +{ + PostQuitMessage(0); +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + const auto ptr = (Fail*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + const auto controlHandle = HWND(lParam); + if (message == WM_PAINT) + ptr->paint(); + else if (message == WM_CTLCOLORSTATIC) { + // Make log color white + SetBkColor(HDC(wParam), RGB(255, 255, 255)); + return (LRESULT)GetStockObject(WHITE_BRUSH); + } + else if (message == WM_COMMAND) { + const auto notification = HIWORD(wParam); + if (notification == BN_CLICKED) { + if (controlHandle == ptr->m_btnClose) + ptr->goClose(); + } + } + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/Installer/Screens/Fail.h b/src/Installer/Screens/Fail.h new file mode 100644 index 0000000..7b24453 --- /dev/null +++ b/src/Installer/Screens/Fail.h @@ -0,0 +1,31 @@ +#pragma once +#ifndef FAIL_H +#define FAIL_H + +#include "Screen.h" + + +/** This state encapuslates the "Failure - Screen" state. */ +class Fail: public Screen { +public: + // Public (de)Constructors + ~Fail(); + Fail(Installer * installer, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size); + + + // Public Interface Implementations + virtual void enact() override; + virtual void paint() override; + + + // Public Methods + /** Switch to the next state. */ + void goClose(); + + + // Public Attributes + HWND m_hwndLog = nullptr, m_btnClose = nullptr; + size_t m_logIndex = 0ull; +}; + +#endif // FAIL_H \ No newline at end of file diff --git a/src/Installer/Screens/Finish.cpp b/src/Installer/Screens/Finish.cpp new file mode 100644 index 0000000..8a580a9 --- /dev/null +++ b/src/Installer/Screens/Finish.cpp @@ -0,0 +1,204 @@ +#include "Finish.h" +#include "Common.h" +#include "../Installer.h" +#include +#include +#include +#include + + +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +Finish::~Finish() +{ + UnregisterClass("FINISH_SCREEN", m_hinstance); + DestroyWindow(m_hwnd); + DestroyWindow(m_checkbox); + DestroyWindow(m_btnClose); + for each (auto checkboxHandle in m_shortcutCheckboxes) + DestroyWindow(checkboxHandle); +} + +Finish::Finish(Installer * installer, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size) + : Screen(installer, pos, size) +{ + // Create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = "FINISH_SCREEN"; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("FINISH_SCREEN", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); + + // Create checkboxes + m_checkbox = CreateWindow("Button", "Show installation directory on close", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, 150, size.x, 15, m_hwnd, (HMENU)1, hInstance, NULL); + CheckDlgButton(m_hwnd, 1, BST_CHECKED); + + // Shortcuts + const auto desktopStrings = m_installer->m_mfStrings[L"shortcut"], startmenuStrings = m_installer->m_mfStrings[L"startmenu"]; + size_t numD = std::count(desktopStrings.begin(), desktopStrings.end(), L',') + 1ull, numS = std::count(startmenuStrings.begin(), startmenuStrings.end(), L',') + 1ull; + m_shortcutCheckboxes.reserve(numD + numS); + m_shortcuts_d.reserve(numD + numS); + m_shortcuts_s.reserve(numD + numS); + size_t last = 0; + if (!desktopStrings.empty()) + for (size_t x = 0; x < numD; ++x) { + // Find end of shortcut + auto nextComma = desktopStrings.find(L',', last); + if (nextComma == std::wstring::npos) + nextComma = desktopStrings.size(); + + // Find demarkation point where left half is the shortcut path, right half is the shortcut name + m_shortcuts_d.push_back(desktopStrings.substr(last, nextComma - last)); + + // Skip whitespace, find next element + last = nextComma + 1ull; + while (last < desktopStrings.size() && (desktopStrings[last] == L' ' || desktopStrings[last] == L'\r' || desktopStrings[last] == L'\t' || desktopStrings[last] == L'\n')) + last++; + } + last = 0; + if (!startmenuStrings.empty()) + for (size_t x = 0; x < numS; ++x) { + // Find end of shortcut + auto nextComma = startmenuStrings.find(L',', last); + if (nextComma == std::wstring::npos) + nextComma = startmenuStrings.size(); + + // Find demarkation point where left half is the shortcut path, right half is the shortcut name + m_shortcuts_s.push_back(startmenuStrings.substr(last, nextComma - last)); + + // Skip whitespace, find next element + last = nextComma + 1ull; + while (last < startmenuStrings.size() && (startmenuStrings[last] == L' ' || startmenuStrings[last] == L'\r' || startmenuStrings[last] == L'\t' || startmenuStrings[last] == L'\n')) + last++; + } + int vertical = 170, checkIndex = 2; + for each (const auto & shortcut in m_shortcuts_d) { + const auto name = std::wstring(&shortcut[1], shortcut.length() - 1); + m_shortcutCheckboxes.push_back(CreateWindowW(L"Button", (L"Create a shortcut for " + name + L" on the desktop").c_str(), WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, vertical, size.x, 15, m_hwnd, (HMENU)(LONGLONG)checkIndex, hInstance, NULL)); + CheckDlgButton(m_hwnd, checkIndex, BST_CHECKED); + vertical += 20; + checkIndex++; + } + for each (const auto & shortcut in m_shortcuts_s) { + const auto name = std::wstring(&shortcut[1], shortcut.length() - 1); + m_shortcutCheckboxes.push_back(CreateWindowW(L"Button", (L"Create a shortcut for " + name + L" in the start-menu").c_str(), WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, vertical, size.x, 15, m_hwnd, (HMENU)(LONGLONG)checkIndex, hInstance, NULL)); + CheckDlgButton(m_hwnd, checkIndex, BST_CHECKED); + vertical += 20; + checkIndex++; + } + + // Create Buttons + constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; + m_btnClose = CreateWindow("BUTTON", "Close", BUTTON_STYLES, size.x - 95, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); +} + +void Finish::enact() +{ + // Does nothing +} + +void Finish::paint() +{ + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(m_hwnd, &ps)); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, m_size.y), + Color(50, 25, 255, 125), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, m_size.x, m_size.y); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"Installation Complete", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + + EndPaint(m_hwnd, &ps); +} + +void Finish::goClose() +{ + m_showDirectory = IsDlgButtonChecked(m_hwnd, 1); + const auto instDir = m_installer->getDirectory() + "\\" + m_installer->getPackageName(); + // Open the installation directory + if user wants it to + if (m_showDirectory) + ShellExecute(NULL, "open", instDir.c_str(), NULL, NULL, SW_SHOWDEFAULT); + + // Create Shortcuts + int x = 2; + for each (const auto & shortcut in m_shortcuts_d) { + if (IsDlgButtonChecked(m_hwnd, x)) { + std::error_code ec; + const auto nonwideShortcut = from_wideString(shortcut); + auto srcPath = instDir; + if (srcPath.back() == '\\') + srcPath = std::string(&srcPath[0], srcPath.size() - 1ull); + srcPath += nonwideShortcut; + const auto dstPath = get_users_desktop() + "\\" + std::filesystem::path(srcPath).filename().string(); + create_shortcut(srcPath, instDir, dstPath); + } + x++; + } + for each (const auto & shortcut in m_shortcuts_s) { + if (IsDlgButtonChecked(m_hwnd, x)) { + std::error_code ec; + const auto nonwideShortcut = from_wideString(shortcut); + auto srcPath = instDir; + if (srcPath.back() == '\\') + srcPath = std::string(&srcPath[0], srcPath.size() - 1ull); + srcPath += nonwideShortcut; + const auto dstPath = get_users_startmenu() + "\\" + std::filesystem::path(srcPath).filename().string(); + create_shortcut(srcPath, instDir, dstPath); + } + x++; + } + PostQuitMessage(0); +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + const auto ptr = (Finish*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + const auto controlHandle = HWND(lParam); + if (message == WM_PAINT) + ptr->paint(); + else if (message == WM_CTLCOLORSTATIC) { + // Make checkbox text background color transparent + bool isCheckbox = controlHandle == ptr->m_checkbox; + for each (auto chkHandle in ptr->m_shortcutCheckboxes) + if (controlHandle == chkHandle) { + isCheckbox = true; + break; + } + if (isCheckbox) { + SetBkMode(HDC(wParam), TRANSPARENT); + return (LRESULT)GetStockObject(NULL_BRUSH); + } + } + else if (message == WM_COMMAND) { + const auto notification = HIWORD(wParam); + if (notification == BN_CLICKED) { + if (controlHandle == ptr->m_btnClose) + ptr->goClose(); + } + } + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/Installer/Screens/Finish.h b/src/Installer/Screens/Finish.h new file mode 100644 index 0000000..2c407ca --- /dev/null +++ b/src/Installer/Screens/Finish.h @@ -0,0 +1,34 @@ +#pragma once +#ifndef FINISH_H +#define FINISH_H + +#include "Screen.h" +#include + + +/** This state encapuslates the "Finished - Screen" state. */ +class Finish: public Screen { +public: + // Public (de)Constructors + ~Finish(); + Finish(Installer * installer, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size); + + + // Public Interface Implementations + virtual void enact() override; + virtual void paint() override; + + + // Public Methods + /** Switch to the next state. */ + void goClose(); + + + // Public Attributes + HWND m_checkbox = nullptr, m_btnClose = nullptr; + bool m_showDirectory = true; + std::vector m_shortcutCheckboxes; + std::vector m_shortcuts_d, m_shortcuts_s; +}; + +#endif // FINISH_H \ No newline at end of file diff --git a/src/Installer/Screens/Install.cpp b/src/Installer/Screens/Install.cpp new file mode 100644 index 0000000..571daed --- /dev/null +++ b/src/Installer/Screens/Install.cpp @@ -0,0 +1,120 @@ +#include "Install.h" +#include "Common.h" +#include "Resource.h" +#include "../Installer.h" + + +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +Install::~Install() +{ + UnregisterClass("INSTALL_SCREEN", m_hinstance); + DestroyWindow(m_hwnd); + DestroyWindow(m_hwndLog); + DestroyWindow(m_hwndPrgsBar); + DestroyWindow(m_btnFinish); + TaskLogger::RemoveCallback_TextAdded(m_logIndex); + TaskLogger::RemoveCallback_ProgressUpdated(m_taskIndex); +} + +Install::Install(Installer * installer, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size) + : Screen(installer, pos, size) +{ + // Create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = "INSTALL_SCREEN"; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("INSTALL_SCREEN", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); + + // Create log box and progress bar + m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 75, size.x - 20, size.y - 125, m_hwnd, NULL, hInstance, NULL); + m_hwndPrgsBar = CreateWindowEx(WS_EX_CLIENTEDGE, PROGRESS_CLASS, 0, WS_CHILD | WS_VISIBLE | WS_OVERLAPPED | WS_DLGFRAME | WS_CLIPCHILDREN | PBS_SMOOTH, 10, size.y - 40, size.x - 115, 30, m_hwnd, NULL, hInstance, NULL); + m_logIndex = TaskLogger::AddCallback_TextAdded([&](const std::string & message) { + SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)message.c_str()); + }); + m_taskIndex = TaskLogger::AddCallback_ProgressUpdated([&](const size_t & position, const size_t & range) { + SendMessage(m_hwndPrgsBar, PBM_SETRANGE32, 0, LPARAM(int_fast32_t(range))); + SendMessage(m_hwndPrgsBar, PBM_SETPOS, WPARAM(int_fast32_t(position)), 0); + RECT rc = { 580, 410, 800, 450 }; + RedrawWindow(m_hwnd, &rc, NULL, RDW_INVALIDATE); + + std::string progress = std::to_string(position == range ? 100 : int(std::floorf((float(position) / float(range)) * 100.0f))) + "%"; + EnableWindow(m_btnFinish, position == range); + SetWindowTextA(m_btnFinish, position == range ? "Finish >" : progress.c_str()); + }); + + // Create Buttons + constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; + m_btnFinish = CreateWindow("BUTTON", "Finish", BUTTON_STYLES, size.x - 95, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); + EnableWindow(m_btnFinish, false); +} + +void Install::enact() +{ + m_installer->beginInstallation(); +} + +void Install::paint() +{ + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(m_hwnd, &ps)); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, m_size.y), + Color(50, 25, 125, 225), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, m_size.x, m_size.y); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"Installing", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + + EndPaint(m_hwnd, &ps); +} + +void Install::goFinish() +{ + m_installer->setScreen(Installer::ScreenEnums::FINISH_SCREEN); +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + const auto ptr = (Install*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + const auto controlHandle = HWND(lParam); + if (message == WM_PAINT) + ptr->paint(); + else if (message == WM_CTLCOLORSTATIC) { + // Make log color white + SetBkColor(HDC(wParam), RGB(255, 255, 255)); + return (LRESULT)GetStockObject(WHITE_BRUSH); + } + else if (message == WM_COMMAND) { + const auto notification = HIWORD(wParam); + if (notification == BN_CLICKED) { + if (controlHandle == ptr->m_btnFinish) + ptr->goFinish(); + } + } + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/Installer/Screens/Install.h b/src/Installer/Screens/Install.h new file mode 100644 index 0000000..c7ea624 --- /dev/null +++ b/src/Installer/Screens/Install.h @@ -0,0 +1,32 @@ +#pragma once +#ifndef INSTALL_H +#define INSTALL_H + +#include "Screen.h" +#include + + +/** This state encapuslates the "Installing - Screen" state. */ +class Install : public Screen { +public: + // Public (de)Constructors + ~Install(); + Install(Installer * installer, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size); + + + // Public Interface Implementations + virtual void enact() override; + virtual void paint() override; + + + // Public Methods + /** Switch to the next state. */ + void goFinish(); + + + // Public Attributes + HWND m_hwndLog = nullptr, m_hwndPrgsBar = nullptr, m_btnFinish = nullptr; + size_t m_logIndex = 0ull, m_taskIndex = 0ull; +}; + +#endif // INSTALL_H \ No newline at end of file diff --git a/src/Installer/Screens/Screen.h b/src/Installer/Screens/Screen.h new file mode 100644 index 0000000..3a68a1d --- /dev/null +++ b/src/Installer/Screens/Screen.h @@ -0,0 +1,51 @@ +#pragma once +#ifndef SCREEN_H +#define SCREEN_H + +#include +#pragma warning(push) +#pragma warning(disable:4458) +#include +#pragma warning(pop) + + +using namespace Gdiplus; +class Installer; +struct vec2 { int x = 0, y = 0; }; + +/**Encapsulation of a windows GDI 'window' object, for a particular screen of the application. */ +class Screen { +public: + // Public (de)Constructors + Screen(Installer * installer, const vec2 & pos, const vec2 & size) : m_installer(installer), m_pos(pos), m_size(size) {} + + + // Public Methods + /** Sets the visibility & enable state of this window. + @param state whether or not to show and activate this window. */ + void setVisible(const bool & state) { + ShowWindow(m_hwnd, state); + EnableWindow(m_hwnd, state); + } + + + // Public Interface Declarations + /** Trigger this state to perform its screen action. */ + virtual void enact() = 0; + /** Render this window. */ + virtual void paint() = 0; + + + // Public Attributes + Installer * m_installer = nullptr; + HWND m_hwnd = nullptr; + + +protected: + // Private Attributes + WNDCLASSEX m_wcex; + HINSTANCE m_hinstance; + vec2 m_pos, m_size; +}; + +#endif // SCREEN_H \ No newline at end of file diff --git a/src/Installer/Screens/Welcome.cpp b/src/Installer/Screens/Welcome.cpp new file mode 100644 index 0000000..7a991d4 --- /dev/null +++ b/src/Installer/Screens/Welcome.cpp @@ -0,0 +1,114 @@ +#include "Welcome.h" +#include "../Installer.h" + + +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +Welcome::~Welcome() +{ + UnregisterClass("WELCOME_SCREEN", m_hinstance); + DestroyWindow(m_hwnd); + DestroyWindow(m_btnNext); + DestroyWindow(m_btnCancel); +} + +Welcome::Welcome(Installer * installer, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size) + : Screen(installer, pos, size) +{ + // Create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOWFRAME); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = "WELCOME_SCREEN"; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("WELCOME_SCREEN", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); + + // Create Buttons + constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; + m_btnNext = CreateWindow("BUTTON", "Next >", BUTTON_STYLES | BS_DEFPUSHBUTTON, size.x - 200, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); + m_btnCancel = CreateWindow("BUTTON", "Cancel", BUTTON_STYLES, size.x - 95, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); +} + +void Welcome::enact() +{ + // Does nothing +} + +void Welcome::paint() +{ + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(m_hwnd, &ps)); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, m_size.y), + Color(50, 25, 125, 225), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, m_size.x, m_size.y); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); + Font regUnderFont(&fontFamily, 14, FontStyleUnderline, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + SolidBrush blackBrush(Color(255, 0, 0, 0)); + SolidBrush greyBrush(Color(255, 127, 127, 127)); + SolidBrush blueishBrush(Color(255, 100, 125, 175)); + StringFormat format = StringFormat::GenericTypographic(); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"Welcome to the Installation Wizard", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + auto nameVer = m_installer->m_mfStrings[L"name"] + L" " + m_installer->m_mfStrings[L"version"]; + if (m_installer->m_mfStrings[L"name"].empty()) nameVer = L"it's contents"; + graphics.DrawString((L"The Wizard will install " + nameVer + L" on to your computer.").c_str(), -1, ®Font, PointF{ 10, 100 }, &format, &blackBrush); + graphics.DrawString(m_installer->m_mfStrings[L"description"].c_str(), -1, ®Font, RectF(10, 150, REAL(m_size.x - 20), REAL(m_size.y - 200)), &format, &blackBrush); + + // Draw -watermark- + graphics.DrawString(L"This software was generated using nSuite", -1, ®Font, PointF(10, REAL(m_size.y - 45)), &greyBrush); + graphics.DrawString(L"https://github.com/Yattabyte/nSuite", -1, ®UnderFont, PointF(10, REAL(m_size.y - 25)), &blueishBrush); + + EndPaint(m_hwnd, &ps); +} + +void Welcome::goNext() +{ + m_installer->setScreen(Installer::ScreenEnums::AGREEMENT_SCREEN); +} + +void Welcome::goCancel() +{ + PostQuitMessage(0); +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + const auto ptr = (Welcome*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + if (message == WM_PAINT) + ptr->paint(); + else if (message == WM_COMMAND) { + const auto notification = HIWORD(wParam); + if (notification == BN_CLICKED) { + auto controlHandle = HWND(lParam); + if (controlHandle == ptr->m_btnNext) + ptr->goNext(); + else if (controlHandle == ptr->m_btnCancel) + ptr->goCancel(); + } + } + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/Installer/Screens/Welcome.h b/src/Installer/Screens/Welcome.h new file mode 100644 index 0000000..afc28d3 --- /dev/null +++ b/src/Installer/Screens/Welcome.h @@ -0,0 +1,32 @@ +#pragma once +#ifndef WELCOME_H +#define WELCOME_H + +#include "Screen.h" + + +/** This state encapuslates the "Welcome - Screen" state. */ +class Welcome : public Screen { +public: + // Public (de)Constructors + ~Welcome(); + Welcome(Installer * installer, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size); + + + // Public Interface Implementations + virtual void enact() override; + virtual void paint() override; + + + // Public Methods + /** Switch to the next state. */ + void goNext(); + /** Switch to the cancel state. */ + void goCancel(); + + + // Public Attributes + HWND m_btnNext = nullptr, m_btnCancel = nullptr; +}; + +#endif // WELCOME_H \ No newline at end of file diff --git a/src/nStaller/archive.dat b/src/Installer/archive.npack similarity index 100% rename from src/nStaller/archive.dat rename to src/Installer/archive.npack diff --git a/src/Installer/icon.ico b/src/Installer/icon.ico new file mode 100644 index 0000000..cf8fcec Binary files /dev/null and b/src/Installer/icon.ico differ diff --git a/src/Installer/manifest.nman b/src/Installer/manifest.nman new file mode 100644 index 0000000..e69de29 diff --git a/src/Instructions.cpp b/src/Instructions.cpp index c3666b6..b92da36 100644 --- a/src/Instructions.cpp +++ b/src/Instructions.cpp @@ -44,7 +44,7 @@ size_t Insert_Instruction::SIZE() const { return sizeof(char) + (sizeof(size_t) * 2) + (sizeof(char) * newData.size()); } -void Insert_Instruction::DO(char * bufferNew, const size_t & newSize, const char * const bufferOld, const size_t & oldSize) const { +void Insert_Instruction::DO(char * bufferNew, const size_t & newSize, const char * const, const size_t &) const { for (auto i = index, x = size_t(0ull), length = newData.size(); i < newSize && x < length; ++i, ++x) bufferNew[i] = newData[x]; } @@ -89,7 +89,7 @@ size_t Repeat_Instruction::SIZE() const { return sizeof(char) + (sizeof(size_t) * 2ull) + sizeof(char); } -void Repeat_Instruction::DO(char * bufferNew, const size_t & newSize, const char * const bufferOld, const size_t & oldSize) const { +void Repeat_Instruction::DO(char * bufferNew, const size_t & newSize, const char * const, const size_t &) const { for (auto i = index, x = size_t(0ull); i < newSize && x < amount; ++i, ++x) bufferNew[i] = value; } diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..2bd56ad --- /dev/null +++ b/src/README.md @@ -0,0 +1,332 @@ +# Library +The core of this library can be found between 2 namespaces: +- BFT (BufferTools) +- DFT (DirectoryTools) + +#### The BufferTools namespace provides the following 5 functions: +- [CompressBuffer](#CompressBuffer) +- [DecompressBuffer](#DecompressBuffer) +- [DiffBuffers](#DiffBuffers) +- [PatchBuffer](#PatchBuffer) +- [HashBuffer](#HashBuffer) + +#### The DirectoryTools namespace exposes 4 similar helper functions: +- [CompressDirectory](#CompressDirectory) +- [DecompressDirectory](#DecompressDirectory) +- [DiffDirectories](#DiffDirectories) +- [PatchDirectory](#PatchDirectory) + +# Functions +### CompressBuffer +Compresses a source buffer into an equal or smaller-sized destination buffer. +After compression, it applies a small header describing how large the uncompressed buffer is. +```c++ +bool BFT::CompressBuffer( + char * sourceBuffer, + const size_t & sourceSize, + char ** destinationBuffer, + size_t & destinationSize +); +``` +Parameter: `sourceBuffer` the original buffer to read from. +Parameter: `sourceSize` the size of the source buffer in bytes. +Parameter: `destinationBuffer` pointer to the destination buffer, which will hold compressed contents. +Parameter: `destinationSize` reference updated with the size in bytes of the compressed destinationBuffer. +Return: true if compression success, false otherwise. + +Example Usage: +```c++ +// Create a buffer, fill it with data +char * buffer = new char[100]; + +// Compress +char * compressedBuffer(nullptr); +size_t compressedSize(0); +bool result = BFT::CompressBuffer(buffer, 100, &compressedBuffer, compressedSize); +delete[] buffer; +if (result) { + // Do something with compressedBuffer (size of compressedSize) + delete[] compressedBuffer; +} +``` + +### DecompressBuffer +Decompressess a source buffer into an equal or larger-sized destination buffer. +Prior to decompression, it reads from a small header describing how large of a buffer it needs to allocate. +```c++ +bool BFT::DecompressBuffer( + char * sourceBuffer, + const size_t & sourceSize, + char ** destinationBuffer, + size_t & destinationSize +); +``` +Parameter: `sourceBuffer` the original buffer to read from. +Parameter: `sourceSize` the size of the source buffer in bytes. +Parameter: `destinationBuffer` pointer to the destination buffer, which will hold decompressed contents. +Parameter: `destinationSize` reference updated with the size in bytes of the decompressed destinationBuffer. +Return: true if decompression success, false otherwise. + +Example Usage: +```c++ +// Create a buffer, fill it with data +char * compressedBuffer = new char[size_of_compressed_buffer]; + +// Decompress +char * buffer(nullptr); +size_t bufferSize(0); +bool result = BFT::DecompressBuffer(compressedBuffer, size_of_compressed_buffer, &buffer, bufferSize); +delete[] compressedBuffer; +if (result) { + // Do something with decompressedBuffer (size of bufferSize) + delete[] buffer; +} +``` + +### DiffBuffers +Processes two input buffers, diffing them. +Generates a compressed instruction set dictating how to get from the old buffer to the new buffer. +```c++ +bool BFT::DiffBuffers( + char * buffer_old, + const size_t & size_old, + char * buffer_new, + const size_t & size_new, + char ** buffer_diff, + size_t & size_diff, + size_t * instructionCount = nullptr +); +``` +Parameter: `buffer_old` the older of the 2 buffers. +Parameter: `size_old` the size of the old buffer. +Parameter: `buffer_new` the newer of the 2 buffers. +Parameter: `size_new` the size of the new buffer. +Parameter: `buffer_diff` pointer to store the diff buffer at. +Parameter: `size_diff` reference updated with the size of the compressed diff buffer. +Parameter: `instructionCount` (optional) pointer to update with the number of instructions processed. +Return: true if diff success, false otherwise. + +Example Usage: +```c++ +// Buffers for two input files +char * fileA = new char[100]; +char * fileB = new char[255]; + +// ...Fill the buffers with some content... // + +// Diff Files (generate instruction set from fileA - fileB) +char * diffBuffer(nullptr); +size_t diffSize(0), instructionCount(0); +bool result = BFT::DiffBuffers(fileA, 100, fileB, 255, &diffBuffer, diffSize, &instructionCount); +delete[] fileA; +delete[] fileB; +if (result) { + // Do something with diffBuffer (size of diffSize) + delete[] diffBuffer; +} +``` + +### PatchBuffer +Reads from a compressed instruction set, uses it to patch the 'older' buffer into the 'newer' buffer +```c++ +bool BFT::PatchBuffer( + char * buffer_old, + const size_t & size_old, + char ** buffer_new, + size_t & size_new, + char * buffer_diff, + const size_t & size_diff, + size_t * instructionCount = nullptr +); +``` +Parameter: `buffer_old` the older of the 2 buffers. +Parameter: `size_old` the size of the old buffer. +Parameter: `buffer_new` pointer to store the newer of the 2 buffers. +Parameter: `size_new` reference updated with the size of the new buffer. +Parameter: `buffer_diff` the compressed diff buffer (instruction set). +Parameter: `size_diff` the size of the compressed diff buffer. +Parameter: `instructionCount` (optional) pointer to update with the number of instructions processed. +Return: true if patch success, false otherwise. + +Example Usage: +```c++ +// Buffers for two input files +char * fileA = new char[100]; +char * diffBuffer = new char[size_of_diff]; + +// ...Fill the buffers with some content... // + +// Patch File (generate fileB from fileA + diffBuffer instructions) +char * fileB(nullptr); +size_t fileBSize(0), instructionCount(0); +bool result = BFT::PatchBuffer(fileA, 100, &fileB, fileBSize, diffBuffer, size_of_diff, &instructionCount); +delete[] fileA; +delete[] diffBuffer; +if (result) { + // Do something with fileB (size of fileBSize) + delete[] fileB; +} +``` + +### HashBuffer +Generates a hash value for the buffer provided, using the buffers' contents. +```c++ +size_t BFT::HashBuffer( + char * buffer, + const size_t & size +); +``` +Parameter: `buffer` the older of the 2 buffers. +Parameter: `size` the size of the old buffer. +Return: hash value for the buffer. + +Example Usage: +```c++ +// Buffers for two input files +char * fileA = new char[100]; +char * fileB = new char[255]; + +// ...Fill the buffers with some content... // + +size_t hashA = BFT::HashBuffer(fileA, 100); +size_t hashB = BFT::HashBuffer(fileB, 255); + +if (hashA != hashB) { + // Diff the buffers or do something with the knowledge that they differ +} +``` + +### CompressDirectory +Compresses all disk contents found within a source directory into an .npack - package formatted buffer. +After compression, it applies a small header dictating packaged folders' name. +```c++ +bool DFT::CompressDirectory( + const std::string & srcDirectory, + char ** packBuffer, + size_t & packSize, + size_t * byteCount = nullptr, + size_t * fileCount = nullptr, + const std::vector & exclusions = std::vector() +); +``` +Parameter: `srcDirectory` the absolute path to the directory to compress. +Parameter: `packBuffer` pointer to the destination buffer, which will hold compressed contents. +Parameter: `packSize` reference updated with the size in bytes of the compressed packBuffer. +Parameter: `byteCount` (optional) pointer updated with the number of bytes written into the package. +Parameter: `fileCount` (optional) pointer updated with the number of files written into the package. +Parameter: `exclusions` (optional) list of filenames/types to skip "\\string" match relative path, ".ext" match extension. +Return: true if compression success, false otherwise. + +Example Usage: +```c++ +std::string directory_to_compress = "C:\\some directory"; + +// Compress +char * packageBuffer(nullptr); +size_t packageSize(0ull), maxSize(0ull), fileCount(0ull); +bool result = DRT::CompressDirectory(directory_to_compress, &packageBuffer, packageSize, &maxSize, &fileCount, {"\\cache.txt", ".cache"}); +if (result) { + // Do something with packageBuffer (size of packageSize) + delete[] packageBuffer; +} +``` + +### DecompressDirectory +Decompresses an .npack - package formatted buffer into its component files in the destination directory. +```c++ +bool DFT::DecompressDirectory( + const std::string & dstDirectory, + char * packBuffer, + const size_t & packSize, + size_t * byteCount = nullptr, + size_t * fileCount = nullptr +); +``` +Parameter: `dstDirectory` the absolute path to the directory to compress. +Parameter: `packBuffer` the buffer containing the compressed package contents. +Parameter: `packSize` the size of the buffer in bytes. +Parameter: `byteCount` (optional) pointer updated with the number of bytes written to disk. +Parameter: `fileCount` (optional) pointer updated with the number of files written to disk. +Return: true if decompression success, false otherwise. + +Example Usage: +```c++ +std::string directory_to_write = "C:\\some directory"; +char * packageBuffer = get_package_buffer(); +size_t packageSize = get_package_size(); + +// Compress +size_t byteCount(0ull), fileCount(0ull); +bool result = DRT::DecompressDirectory(directory_to_write, packageBuffer, packageSize, &byteCount, &fileCount); +delete[] packageBuffer; +if (result) { + // Do something knowing that the package has extracted +} +``` + +### DiffDirectories +Processes two input directories and generates a compressed instruction set for transforming the old directory into the new directory. +```c++ +bool DFT::DiffDirectories( + const std::string & oldDirectory, + const std::string & newDirectory, + char ** diffBuffer, + size_t & diffSize, + size_t & instructionCount +); +``` +Parameter: `oldDirectory` the older directory, or path to an .npack file. +Parameter: `newDirectory` the newer directory, or path to an .npack file. +Parameter: `diffBuffer` pointer to the diff buffer, which will hold compressed diff instructions. +Parameter: `diffSize` reference updated with the size in bytes of the diff buffer. +Parameter: `instructionCount` (optional) pointer updated with the number of instructions compressed into the diff buffer. +Return: true if diff success, false otherwise. + +Example Usage: +```c++ +std::string oldDirectory = "C:\\old software"; +std::string newDirectory = "C:\\new software"; + +// Diff the 2 directories +char * diffBuffer(nullptr); +size_t diffSize(0ull), instructionCount(0ull); +bool result = DRT::DiffDirectories(oldDirectory, newDirectory, &diffBuffer, diffSize, &instructionCount); +if (result) { + // Do something with the diffBuffer (size of diffSize) + delete[] diffBuffer; +} +``` + +### PatchDirectory +Decompresses and executes the instructions contained within a previously-generated diff buffer. +Transforms the contents of an 'old' directory into that of the 'new' directory. +```c++ +bool DFT::PatchDirectory( + const std::string & dstDirectory, + char * diffBuffer, + const size_t & diffSizeComp, + size_t * bytesWritten, + size_t * instructionsUsed = nullptr +); +``` +Parameter: `dstDirectory` the destination directory to transform. +Parameter: `diffBuffer` the buffer containing the compressed diff instructions. +Parameter: `diffSize` the size in bytes of the compressed diff buffer. +Parameter: `bytesWritten` (optional) pointer updated with the number of bytes written to disk. +Parameter: `instructionCount` (optional) pointer updated with the number of instructions executed. +Return: true if patch success, false otherwise. + +Example Usage: +```c++ +std::string dstDirectory = "C:\\old software"; +char * diffBuffer = get_diff_buffer(); +size_t diffSize = get_diff_size(); + +// Diff the 2 directories +size_t bytesWritten(0ull), instructionsExecuted(0ull); +bool result = DRT::PatchDirectory(dstDirectory, diffBuffer, diffSize, &bytesWritten, &instructionsExecuted); +delete[] diffBuffer; +if (result) { + // Do something with the knowledge that the destination directory just updated to a new version +} +``` diff --git a/src/TaskLogger.h b/src/TaskLogger.h new file mode 100644 index 0000000..a198483 --- /dev/null +++ b/src/TaskLogger.h @@ -0,0 +1,112 @@ +#pragma once +#ifndef TASK_LOGGER_H +#define TASK_LOGGER_H + +#include +#include +#include + + +/*********** SINGLETON ***********/ +/* */ +/* Performs logging operations */ +/* */ +/*********** SINGLETON ***********/ +class TaskLogger { +public: + // Public Methods + /** Add a callback method which will be triggered when the log receives new text. + @param func the callback function. (only arg is a string) + @param pullOld if set to true, will immediately call the function, dumping all current log data into it. (optional, defaults true) + @return index to callback in logger, used to remove callback. */ + inline static size_t AddCallback_TextAdded(std::function && func, const bool & pullOld = true) { + auto & instance = GetInstance(); + + // Dump old text if this is true + if (pullOld) + func(instance.PullText()); + + auto index = instance.m_textCallbacks.size(); + instance.m_textCallbacks.emplace_back(std::move(func)); + return index; + } + /** Remove a callback method used for when text is added to the log. + @param index the index for the callback within the logger. */ + inline static void RemoveCallback_TextAdded(const size_t & index) { + auto & instance = GetInstance(); + instance.m_textCallbacks.erase(instance.m_textCallbacks.begin() + index); + } + /** Add a callback method which will be triggered when the task progress changes. + @param func the callback function. (args: progress, range)aults true) + @return index to callback in logger, used to remove callback. */ + inline static size_t AddCallback_ProgressUpdated(std::function && func) { + auto & instance = GetInstance(); + auto index = instance.m_progressCallbacks.size(); + instance.m_progressCallbacks.emplace_back(std::move(func)); + return index; + } + /** Remove a callback method used for when the log progress changes. + @param index the index for the callback within the logger. */ + inline static void RemoveCallback_ProgressUpdated(const size_t & index) { + auto & instance = GetInstance(); + instance.m_progressCallbacks.erase(instance.m_progressCallbacks.begin() + index); + } + /** Push new text into the logger. + @param text the new text to add. */ + inline static void PushText(const std::string & text) { + auto & instance = GetInstance(); + + // Add the message to the log + instance.m_log += text; + + // Notify all observers that the log has been updated + for each (const auto & callback in instance.m_textCallbacks) + callback(text); + } + /** Retrieve all text from the logger. + @return the entire message log. */ + inline static std::string PullText() { + return GetInstance().m_log; + } + /** Set the progress range for the logger. + @param value the upper range value. */ + inline static void SetRange(const size_t & value) { + GetInstance().m_range = value; + } + /** Get the progress range for the logger. + @return the upper range value. */ + inline static size_t GetRange() { + return GetInstance().m_range; + } + /** Set the current progress value for the logger, <= the upper range. + @param amount the progress value to use. */ + inline static void SetProgress(const size_t & amount) { + auto & instance = GetInstance(); + instance.m_pos = amount > instance.m_range ? instance.m_range : amount; + + // Notify all observers that the task has updated + for each (const auto & callback in instance.m_progressCallbacks) + callback(instance.m_pos, instance.m_range); + } + + +private: + // Private (de)constructors + inline ~TaskLogger() = default; + inline TaskLogger() = default; + inline TaskLogger(TaskLogger const&) = delete; + inline void operator=(TaskLogger const&) = delete; + inline static TaskLogger & GetInstance() { + static TaskLogger instance; + return instance; + } + + + // Private Attributes + std::string m_log; + size_t m_range = 0ull, m_pos = 0ull; + std::vector> m_textCallbacks; + std::vector> m_progressCallbacks; +}; + +#endif // TASK_LOGGER_H \ No newline at end of file diff --git a/src/Threader.h b/src/Threader.h index 850191d..d6e2ae7 100644 --- a/src/Threader.h +++ b/src/Threader.h @@ -19,8 +19,9 @@ class Threader { shutdown(); } /** Creates a threader object and generates as many worker threads as the system allows. */ - inline Threader() { - for (size_t x = 0; x < std::thread::hardware_concurrency(); ++x) { + inline Threader(const size_t & maxThreads = std::thread::hardware_concurrency()) { + m_maxThreads = maxThreads; + for (size_t x = 0; x < m_maxThreads; ++x) { std::thread thread([&]() { while (m_alive) { // Check if there is a job to do @@ -56,15 +57,20 @@ class Threader { m_jobs.emplace_back(func); m_jobsStarted++; } + /** Check if the threader has completed all its jobs. + @return true if finished, false otherwise. */ inline bool isFinished() const { return m_jobsStarted == m_jobsFinished; } + /** Prepare the threader for shutdown, notifying threads to complete early. */ inline void prepareForShutdown() { m_keepOpen = false; } + /** Shutsdown the threader, forcing threads to close. */ inline void shutdown() { m_alive = false; - for (size_t x = 0; x < std::thread::hardware_concurrency() && x < m_threads.size(); ++x) { + m_keepOpen = false; + for (size_t x = 0; x < m_maxThreads && x < m_threads.size(); ++x) { if (m_threads[x].joinable()) m_threads[x].join(); } @@ -81,6 +87,7 @@ class Threader { std::vector m_threads; std::deque> m_jobs; std::atomic_size_t m_threadsActive = 0ull, m_jobsStarted = 0ull, m_jobsFinished = 0ull; + size_t m_maxThreads = 0ull; }; #endif // THREADER_H \ No newline at end of file diff --git a/src/Uninstaller/CMakeLists.txt b/src/Uninstaller/CMakeLists.txt new file mode 100644 index 0000000..5c28e26 --- /dev/null +++ b/src/Uninstaller/CMakeLists.txt @@ -0,0 +1,58 @@ +################### +### Uninstaller ### +################### +set (Module Uninstaller) + +# Get source files +file (GLOB_RECURSE ROOT RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "*.cpp" "*.c" "*.h" "*.rc") +# Mimic the file-folder structure +foreach(source IN LISTS ROOT) + get_filename_component(source_path "${source}" PATH) + string(REPLACE "/" "\\" source_path_msvc "${source_path}") + source_group("${source_path_msvc}" FILES "${source}") +endforeach() + +# Add source files +add_executable(${Module} + ${ROOT} + ${CORE_DIR}/Common.h + ${CORE_DIR}/Resource.h + ${CORE_DIR}/Threader.h + ${CORE_DIR}/Instructions.h + ${CORE_DIR}/Instructions.cpp + ${CORE_DIR}/BufferTools.h + ${CORE_DIR}/BufferTools.cpp + ${CORE_DIR}/DirectoryTools.h + ${CORE_DIR}/DirectoryTools.cpp + ${CORE_DIR}/TaskLogger.h +) + +# Add libraries +target_link_libraries(${Module} + "Comctl32.lib" + "propsys.lib" + "Shlwapi.lib" +) + +# Set windows/visual studio settings +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /SUBSYSTEM:WINDOWS") +target_compile_Definitions(${Module} PRIVATE $<$:DEBUG>) +set_target_properties(${Module} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + PDB_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + VS_DEBUGGER_WORKING_DIRECTORY "$(SolutionDir)app" + LINK_FLAGS "/MANIFESTUAC:\"level='requireAdministrator' uiAccess='false'\"" +) + +# Force highest c++ version supported +if (MSVC_VERSION GREATER_EQUAL "1900") + include(CheckCXXCompilerFlag) + CHECK_CXX_COMPILER_FLAG("/std:c++latest" _cpp_latest_flag_supported) + if (_cpp_latest_flag_supported) + add_compile_options("/std:c++latest") + set_target_properties(${Module} PROPERTIES CXX_STANDARD 17) + set_target_properties(${Module} PROPERTIES CXX_STANDARD_REQUIRED ON) + endif() +endif() \ No newline at end of file diff --git a/src/Uninstaller/README.md b/src/Uninstaller/README.md new file mode 100644 index 0000000..ab582e1 --- /dev/null +++ b/src/Uninstaller/README.md @@ -0,0 +1,18 @@ +# Uninstaller +This program is a fully-fledged uninstallation application, made to run on Windows. It is generated by the installer application, and removes itself from the user's registry on completion. + +It uses the Windows GDI library for rendering. + +This application has 2 (two) resources embedded within it: + - IDI_ICON1 the application icon + - IDR_MANIFEST the installer manifest (attributes, strings, instructions) + +The raw version of this application is useless on its own, and is intended to be fullfilled by the Installer application. +The following is how the Installer uses this application: + - writes out a copy of this application to disk + - embedds its own manifest into **this application's** IDR_MANIFEST resource (to facilitate uninstallation) + +This uninstaller has several screens it displays to the user. +If at any point an error occurs, the program enters a failure state and dumps its entire operation log to disk (next to the program, error_log.txt). + +The uninstaller may reuse several of the manifest strings provided by its proceeding installer, such as name, version, and where shortcuts may be located (to remove them). \ No newline at end of file diff --git a/src/Uninstaller/Screens/Fail.cpp b/src/Uninstaller/Screens/Fail.cpp new file mode 100644 index 0000000..caa8315 --- /dev/null +++ b/src/Uninstaller/Screens/Fail.cpp @@ -0,0 +1,112 @@ +#include "Fail.h" +#include "Common.h" +#include "TaskLogger.h" +#include "../Uninstaller.h" +#include +#include +#include +#include + + +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +Fail::~Fail() +{ + UnregisterClass("FAIL_SCREEN", m_hinstance); + DestroyWindow(m_hwnd); + DestroyWindow(m_hwndLog); + DestroyWindow(m_btnClose); + TaskLogger::RemoveCallback_TextAdded(m_logIndex); +} + +Fail::Fail(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size) + : Screen(uninstaller, pos, size) +{ + // Create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = "FAIL_SCREEN"; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("FAIL_SCREEN", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); + + // Create error log + m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 75, size.x - 20, size.y - 125, m_hwnd, NULL, hInstance, NULL); + SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)"Error Log:\r\n"); + m_logIndex = TaskLogger::AddCallback_TextAdded([&](const std::string & message) { + SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)message.c_str()); + }); + + // Create Buttons + constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; + m_btnClose = CreateWindow("BUTTON", "Close", BUTTON_STYLES, size.x - 95, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); +} + +void Fail::enact() +{ + Uninstaller::dumpErrorLog(); +} + +void Fail::paint() +{ + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(m_hwnd, &ps)); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, m_size.y), + Color(50, 225, 25, 75), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, m_size.x, m_size.y); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"Uninstallation Incomplete", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + + EndPaint(m_hwnd, &ps); +} + +void Fail::goClose() +{ + PostQuitMessage(0); +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + const auto ptr = (Fail*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + const auto controlHandle = HWND(lParam); + if (message == WM_PAINT) + ptr->paint(); + else if (message == WM_CTLCOLORSTATIC) { + // Make log color white + SetBkColor(HDC(wParam), RGB(255, 255, 255)); + return (LRESULT)GetStockObject(WHITE_BRUSH); + } + else if (message == WM_COMMAND) { + const auto notification = HIWORD(wParam); + if (notification == BN_CLICKED) { + if (controlHandle == ptr->m_btnClose) + ptr->goClose(); + } + } + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/Uninstaller/Screens/Fail.h b/src/Uninstaller/Screens/Fail.h new file mode 100644 index 0000000..1901a37 --- /dev/null +++ b/src/Uninstaller/Screens/Fail.h @@ -0,0 +1,31 @@ +#pragma once +#ifndef FAILSTATE_H +#define FAILSTATE_H + +#include "Screen.h" + + +/** This state encapuslates the "Failure - Screen" state. */ +class Fail: public Screen { +public: + // Public (de)Constructors + ~Fail(); + Fail(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size); + + + // Public Interface Implementations + virtual void enact() override; + virtual void paint() override; + + + // Public Methods + /** Switch to the next state. */ + void goClose(); + + + // Public Attributes + HWND m_hwndLog = nullptr, m_btnClose = nullptr; + size_t m_logIndex = 0ull; +}; + +#endif // FAILSTATE_H \ No newline at end of file diff --git a/src/Uninstaller/Screens/Finish.cpp b/src/Uninstaller/Screens/Finish.cpp new file mode 100644 index 0000000..6e71ce7 --- /dev/null +++ b/src/Uninstaller/Screens/Finish.cpp @@ -0,0 +1,112 @@ +#include "Finish.h" +#include "Common.h" +#include "../Uninstaller.h" +#include +#include +#include +#include + + +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +Finish::~Finish() +{ + UnregisterClass("FINISH_SCREEN", m_hinstance); + DestroyWindow(m_hwnd); + DestroyWindow(m_btnClose); +} + +Finish::Finish(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size) + : Screen(uninstaller, pos, size) +{ + // Create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = "FINISH_SCREEN"; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("FINISH_SCREEN", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); + + // Create Buttons + constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; + m_btnClose = CreateWindow("BUTTON", "Close", BUTTON_STYLES, size.x - 95, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); +} + +void Finish::enact() +{ + // Does nothing +} + +void Finish::paint() +{ + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(m_hwnd, &ps)); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, m_size.y), + Color(50, 25, 255, 125), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, m_size.x, m_size.y); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"Uninstallation Complete", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + + EndPaint(m_hwnd, &ps); +} + +void Finish::goClose() +{ +#ifndef DEBUG + // Delete scraps of the installation directory (nuke the remaining directory) + std::wstring cmd(L"cmd.exe /C ping 1.1.1.1 -n 1 -w 5000 > Nul & rmdir /q/s \"" + m_uninstaller->getDirectory()); + cmd.erase(std::find(cmd.begin(), cmd.end(), L'\0'), cmd.end()); + cmd += L"\""; + STARTUPINFOW si = { 0 }; + PROCESS_INFORMATION pi = { 0 }; + + CreateProcessW(NULL, (LPWSTR)cmd.c_str(), NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi); + + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + + std::error_code er; + std::filesystem::remove_all(m_uninstaller->getDirectory(), er); +#endif + PostQuitMessage(0); +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + const auto ptr = (Finish*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + const auto controlHandle = HWND(lParam); + if (message == WM_PAINT) + ptr->paint(); + else if (message == WM_COMMAND) { + const auto notification = HIWORD(wParam); + if (notification == BN_CLICKED) { + if (controlHandle == ptr->m_btnClose) + ptr->goClose(); + } + } + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/Uninstaller/Screens/Finish.h b/src/Uninstaller/Screens/Finish.h new file mode 100644 index 0000000..08c9b6c --- /dev/null +++ b/src/Uninstaller/Screens/Finish.h @@ -0,0 +1,30 @@ +#pragma once +#ifndef FINISHSTATE_H +#define FINISHSTATE_H + +#include "Screen.h" + + +/** This state encapuslates the "Finished - Screen" state. */ +class Finish: public Screen { +public: + // Public (de)Constructors + ~Finish(); + Finish(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size); + + + // Public Interface Implementations + virtual void enact() override; + virtual void paint() override; + + + // Public Methods + /** Switch to the next state. */ + void goClose(); + + + // Public Attributes + HWND m_btnClose = nullptr; +}; + +#endif // FINISHSTATE_H \ No newline at end of file diff --git a/src/Uninstaller/Screens/Screen.h b/src/Uninstaller/Screens/Screen.h new file mode 100644 index 0000000..abb93b2 --- /dev/null +++ b/src/Uninstaller/Screens/Screen.h @@ -0,0 +1,51 @@ +#pragma once +#ifndef SCREEN_H +#define SCREEN_H + +#include +#pragma warning(push) +#pragma warning(disable:4458) +#include +#pragma warning(pop) + + +using namespace Gdiplus; +class Uninstaller; +struct vec2 { int x = 0, y = 0; }; + +/**Encapsulation of a windows GDI 'window' object, for a particular state of the application. */ +class Screen { +public: + // Public (de)Constructors + Screen(Uninstaller * uninstaller, const vec2 & pos, const vec2 & size) : m_uninstaller(uninstaller), m_pos(pos), m_size(size) {} + + + // Public Methods + /** Sets the visibility & enable state of this window. + @param state whether or not to show and activate this window. */ + void setVisible(const bool & state) { + ShowWindow(m_hwnd, state); + EnableWindow(m_hwnd, state); + } + + + // Public Interface Declarations + /** Trigger this state to perform its screen action. */ + virtual void enact() = 0; + /** Render this window. */ + virtual void paint() = 0; + + + // Public Attributes + Uninstaller * m_uninstaller = nullptr; + HWND m_hwnd = nullptr; + + +protected: + // Private Attributes + WNDCLASSEX m_wcex; + HINSTANCE m_hinstance; + vec2 m_pos, m_size; +}; + +#endif // SCREEN_H \ No newline at end of file diff --git a/src/Uninstaller/Screens/Uninstall.cpp b/src/Uninstaller/Screens/Uninstall.cpp new file mode 100644 index 0000000..4aafc33 --- /dev/null +++ b/src/Uninstaller/Screens/Uninstall.cpp @@ -0,0 +1,119 @@ +#include "Uninstall.h" +#include "Common.h" +#include "../Uninstaller.h" + + +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +Uninstall::~Uninstall() +{ + UnregisterClass("UNINSTALL_SCREEN", m_hinstance); + DestroyWindow(m_hwnd); + DestroyWindow(m_hwndLog); + DestroyWindow(m_hwndPrgsBar); + DestroyWindow(m_btnFinish); + TaskLogger::RemoveCallback_TextAdded(m_logIndex); + TaskLogger::RemoveCallback_ProgressUpdated(m_taskIndex); +} + +Uninstall::Uninstall(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size) + : Screen(uninstaller, pos, size) +{ + // Create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = "UNINSTALL_SCREEN"; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("UNINSTALL_SCREEN", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); + + // Create log box and progress bar + m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 75, size.x - 20, size.y - 125, m_hwnd, NULL, hInstance, NULL); + m_hwndPrgsBar = CreateWindowEx(WS_EX_CLIENTEDGE, PROGRESS_CLASS, 0, WS_CHILD | WS_VISIBLE | WS_OVERLAPPED | WS_DLGFRAME | WS_CLIPCHILDREN | PBS_SMOOTH, 10, size.y - 40, size.x - 115, 30, m_hwnd, NULL, hInstance, NULL); + m_logIndex = TaskLogger::AddCallback_TextAdded([&](const std::string & message) { + SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)message.c_str()); + }); + m_taskIndex = TaskLogger::AddCallback_ProgressUpdated([&](const size_t & position, const size_t & range) { + SendMessage(m_hwndPrgsBar, PBM_SETRANGE32, 0, LPARAM(int_fast32_t(range))); + SendMessage(m_hwndPrgsBar, PBM_SETPOS, WPARAM(int_fast32_t(position)), 0); + RECT rc = { 580, 410, 800, 450 }; + RedrawWindow(m_hwnd, &rc, NULL, RDW_INVALIDATE); + + std::string progress = std::to_string(position == range ? 100 : int(std::floorf((float(position) / float(range)) * 100.0f))) + "%"; + EnableWindow(m_btnFinish, position == range); + SetWindowTextA(m_btnFinish, position == range ? "Finish >" : progress.c_str()); + }); + + // Create Buttons + constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; + m_btnFinish = CreateWindow("BUTTON", "Finish", BUTTON_STYLES, size.x - 95, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); + EnableWindow(m_btnFinish, false); +} + +void Uninstall::enact() +{ + m_uninstaller->beginUninstallation(); +} + +void Uninstall::paint() +{ + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(m_hwnd, &ps)); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, m_size.y), + Color(50, 25, 125, 225), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, m_size.x, m_size.y); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"Uninstalling", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + + EndPaint(m_hwnd, &ps); +} + +void Uninstall::goFinish() +{ + m_uninstaller->setScreen(Uninstaller::ScreenEnums::FINISH_SCREEN); +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + const auto ptr = (Uninstall*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + const auto controlHandle = HWND(lParam); + if (message == WM_PAINT) + ptr->paint(); + else if (message == WM_CTLCOLORSTATIC) { + // Make log color white + SetBkColor(HDC(wParam), RGB(255, 255, 255)); + return (LRESULT)GetStockObject(WHITE_BRUSH); + } + else if (message == WM_COMMAND) { + const auto notification = HIWORD(wParam); + if (notification == BN_CLICKED) { + if (controlHandle == ptr->m_btnFinish) + ptr->goFinish(); + } + } + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/Uninstaller/Screens/Uninstall.h b/src/Uninstaller/Screens/Uninstall.h new file mode 100644 index 0000000..28b4d42 --- /dev/null +++ b/src/Uninstaller/Screens/Uninstall.h @@ -0,0 +1,33 @@ +#pragma once +#ifndef UNINSTALL_H +#define UNINSTALL_H + +#include "Screen.h" +#include +#include + + +/** This state encapuslates the "Uninstalling - Screen" state. */ +class Uninstall : public Screen { +public: + // Public (de)Constructors + ~Uninstall(); + Uninstall(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size); + + + // Public Interface Implementations + virtual void enact() override; + virtual void paint() override; + + + // Public Methods + /** Switch to the next state. */ + void goFinish(); + + + // Public Attributes + HWND m_hwndLog = nullptr, m_hwndPrgsBar = nullptr, m_btnFinish = nullptr; + size_t m_logIndex = 0ull, m_taskIndex = 0ull; +}; + +#endif // UNINSTALL_H \ No newline at end of file diff --git a/src/Uninstaller/Screens/Welcome.cpp b/src/Uninstaller/Screens/Welcome.cpp new file mode 100644 index 0000000..9e2a7c9 --- /dev/null +++ b/src/Uninstaller/Screens/Welcome.cpp @@ -0,0 +1,114 @@ +#include "Welcome.h" +#include "../Uninstaller.h" + + +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +Welcome::~Welcome() +{ + UnregisterClass("WELCOME_SCREEN", m_hinstance); + DestroyWindow(m_hwnd); + DestroyWindow(m_btnNext); + DestroyWindow(m_btnCancel); +} + +Welcome::Welcome(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size) + : Screen(uninstaller, pos, size) +{ + // Create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOWFRAME); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = "WELCOME_SCREEN"; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("WELCOME_SCREEN", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); + + // Create Buttons + constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; + m_btnNext = CreateWindow("BUTTON", "Uninstall >", BUTTON_STYLES | BS_DEFPUSHBUTTON, size.x - 200, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); + m_btnCancel = CreateWindow("BUTTON", "Cancel", BUTTON_STYLES, size.x - 95, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); +} + +void Welcome::enact() +{ + // Does nothing +} + +void Welcome::paint() +{ + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(m_hwnd, &ps)); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, m_size.y), + Color(50, 25, 125, 225), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, m_size.x, m_size.y); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); + Font regUnderFont(&fontFamily, 14, FontStyleUnderline, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + SolidBrush blackBrush(Color(255, 0, 0, 0)); + SolidBrush greyBrush(Color(255, 127, 127, 127)); + SolidBrush blueishBrush(Color(255, 100, 125, 175)); + StringFormat format = StringFormat::GenericTypographic(); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"Welcome to the Uninstallation Wizard", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + auto nameVer = m_uninstaller->m_mfStrings[L"name"] + L" " + m_uninstaller->m_mfStrings[L"version"]; + if (m_uninstaller->m_mfStrings[L"name"].empty()) nameVer = L"it's contents"; + graphics.DrawString((L"The Wizard will remove " + nameVer + L" from your computer.").c_str(), -1, ®Font, PointF{ 10, 75 }, &format, &blackBrush); + graphics.DrawString(L"Note: the installation directory for this software will be deleted.\r\nIf there are any files that you wish to preserve, move them before continuing.", -1, ®Font, RectF(10, 400, 620, 300), &format, &blackBrush); + + // Draw -watermark- + graphics.DrawString(L"This software was generated using nSuite", -1, ®Font, PointF(10, REAL(m_size.y - 45)), &greyBrush); + graphics.DrawString(L"https://github.com/Yattabyte/nSuite", -1, ®UnderFont, PointF(10, REAL(m_size.y - 25)), &blueishBrush); + + EndPaint(m_hwnd, &ps); +} + +void Welcome::goNext() +{ + m_uninstaller->setScreen(Uninstaller::ScreenEnums::UNINSTALL_SCREEN); +} + +void Welcome::goCancel() +{ + PostQuitMessage(0); +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + const auto ptr = (Welcome*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + if (message == WM_PAINT) + ptr->paint(); + else if (message == WM_COMMAND) { + const auto notification = HIWORD(wParam); + if (notification == BN_CLICKED) { + auto controlHandle = HWND(lParam); + if (controlHandle == ptr->m_btnNext) + ptr->goNext(); + else if (controlHandle == ptr->m_btnCancel) + ptr->goCancel(); + } + } + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/Uninstaller/Screens/Welcome.h b/src/Uninstaller/Screens/Welcome.h new file mode 100644 index 0000000..2a477ca --- /dev/null +++ b/src/Uninstaller/Screens/Welcome.h @@ -0,0 +1,32 @@ +#pragma once +#ifndef WELCOME_H +#define WELCOME_H + +#include "Screen.h" + + +/** This state encapuslates the "Welcome - Screen" state. */ +class Welcome : public Screen { +public: + // Public (de)Constructors + ~Welcome(); + Welcome(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size); + + + // Public Interface Implementations + virtual void enact() override; + virtual void paint() override; + + + // Public Methods + /** Switch to the next state. */ + void goNext(); + /** Switch to the cancel state. */ + void goCancel(); + + + // Public Attributes + HWND m_btnNext = nullptr, m_btnCancel = nullptr; +}; + +#endif // WELCOME_H \ No newline at end of file diff --git a/src/Uninstaller/Uninstaller.cpp b/src/Uninstaller/Uninstaller.cpp new file mode 100644 index 0000000..8111f3e --- /dev/null +++ b/src/Uninstaller/Uninstaller.cpp @@ -0,0 +1,315 @@ +#include "Uninstaller.h" +#include "Common.h" +#include "DirectoryTools.h" +#include "TaskLogger.h" +#include +#include +#include +#include +#include +#pragma warning(push) +#pragma warning(disable:4458) +#include +#pragma warning(pop) + +// Screens used in this GUI application +#include "Screens/Welcome.h" +#include "Screens/Uninstall.h" +#include "Screens/Finish.h" +#include "Screens/Fail.h" + + +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_ HINSTANCE, _In_ LPSTR, _In_ int) +{ + CoInitialize(NULL); + Gdiplus::GdiplusStartupInput gdiplusStartupInput; + ULONG_PTR gdiplusToken; + Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL); + Uninstaller uninstaller(hInstance); + + // Main message loop: + MSG msg; + while (GetMessage(&msg, NULL, 0, 0)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Close + CoUninitialize(); + return (int)msg.wParam; +} + +Uninstaller::Uninstaller() + : m_manifest(IDR_MANIFEST, "MANIFEST"), m_threader(1ull) +{ + // Process manifest + if (m_manifest.exists()) { + // Create a string stream of the manifest file + std::wstringstream ss; + ss << reinterpret_cast(m_manifest.getPtr()); + + // Cycle through every line, inserting attributes into the manifest map + std::wstring attrib, value; + while (ss >> attrib && ss >> std::quoted(value)) { + wchar_t * k = new wchar_t[attrib.length() + 1]; + wcscpy_s(k, attrib.length() + 1, attrib.data()); + m_mfStrings[k] = value; + } + } +} + +Uninstaller::Uninstaller(const HINSTANCE hInstance) : Uninstaller() +{ + // Ensure that a manifest exists + bool success = true; + if (!m_manifest.exists()) { + TaskLogger::PushText("Critical failure: uninstaller manifest doesn't exist!\r\n"); + success = false; + } + + // Acquire the installation directory + m_directory = m_mfStrings[L"directory"]; + if (m_directory.empty()) + m_directory = to_wideString(get_current_directory()); + + // Create window class + WNDCLASSEX wcex; + wcex.cbSize = sizeof(WNDCLASSEX); + wcex.style = CS_HREDRAW | CS_VREDRAW; + wcex.lpfnWndProc = WndProc; + wcex.cbClsExtra = 0; + wcex.cbWndExtra = 0; + wcex.hInstance = hInstance; + wcex.hIcon = LoadIcon(hInstance, (LPCSTR)IDI_ICON1); + wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + wcex.lpszMenuName = NULL; + wcex.lpszClassName = "Uninstaller"; + wcex.hIconSm = LoadIcon(wcex.hInstance, IDI_APPLICATION); + if (!RegisterClassEx(&wcex)) { + TaskLogger::PushText("Critical failure: could not create main window.\r\n"); + success = false; + } + else { + m_hwnd = CreateWindowW( + L"Uninstaller",(m_mfStrings[L"name"] + L" Uninstaller").c_str(), + WS_OVERLAPPED | WS_VISIBLE | WS_BORDER | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, + CW_USEDEFAULT, CW_USEDEFAULT, + 800, 500, + NULL, NULL, hInstance, NULL + ); + + // Create + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this);auto dwStyle = (DWORD)GetWindowLongPtr(m_hwnd, GWL_STYLE); + auto dwExStyle = (DWORD)GetWindowLongPtr(m_hwnd, GWL_EXSTYLE); + RECT rc = { 0, 0, 800, 500 }; + ShowWindow(m_hwnd, true); + UpdateWindow(m_hwnd); + AdjustWindowRectEx(&rc, dwStyle, false, dwExStyle); + SetWindowPos(m_hwnd, NULL, 0, 0, rc.right - rc.left, rc.bottom - rc.top, SWP_NOZORDER | SWP_NOMOVE); + + // The portions of the screen that change based on input + m_screens[WELCOME_SCREEN] = new Welcome(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); + m_screens[UNINSTALL_SCREEN] = new Uninstall(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); + m_screens[FINISH_SCREEN] = new Finish(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); + m_screens[FAIL_SCREEN] = new Fail(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); + setScreen(WELCOME_SCREEN); + } + +#ifndef DEBUG + if (!success) + invalidate(); +#endif +} + +void Uninstaller::invalidate() +{ + setScreen(FAIL_SCREEN); + m_valid = false; +} + +void Uninstaller::setScreen(const ScreenEnums & screenIndex) +{ + if (m_valid) { + m_screens[m_currentIndex]->setVisible(false); + m_screens[screenIndex]->enact(); + m_screens[screenIndex]->setVisible(true); + m_currentIndex = screenIndex; + RECT rc = { 0, 0, 160, 500 }; + RedrawWindow(m_hwnd, &rc, NULL, RDW_INVALIDATE); + } +} + +std::wstring Uninstaller::getDirectory() const +{ + return m_directory; +} + +void Uninstaller::beginUninstallation() +{ + m_threader.addJob([&]() { + // Find all installed files + const auto directory = sanitize_path(from_wideString(m_directory)); + const auto entries = get_file_paths(directory); + + // Find all shortcuts + const auto desktopStrings = m_mfStrings[L"shortcut"], startmenuStrings = m_mfStrings[L"startmenu"]; + size_t numD = std::count(desktopStrings.begin(), desktopStrings.end(), L',') + 1ull, numS = std::count(startmenuStrings.begin(), startmenuStrings.end(), L',') + 1ull; + std::vector shortcuts_d, shortcuts_s; + shortcuts_d.reserve(numD + numS); + shortcuts_s.reserve(numD + numS); + size_t last = 0; + if (!desktopStrings.empty()) + for (size_t x = 0; x < numD; ++x) { + // Find end of shortcut + auto nextComma = desktopStrings.find(L',', last); + if (nextComma == std::wstring::npos) + nextComma = desktopStrings.size(); + + // Find demarkation point where left half is the shortcut path, right half is the shortcut name + shortcuts_d.push_back(desktopStrings.substr(last, nextComma - last)); + + // Skip whitespace, find next element + last = nextComma + 1ull; + while (last < desktopStrings.size() && (desktopStrings[last] == L' ' || desktopStrings[last] == L'\r' || desktopStrings[last] == L'\t' || desktopStrings[last] == L'\n')) + last++; + } + last = 0; + if (!startmenuStrings.empty()) + for (size_t x = 0; x < numS; ++x) { + // Find end of shortcut + auto nextComma = startmenuStrings.find(L',', last); + if (nextComma == std::wstring::npos) + nextComma = startmenuStrings.size(); + + // Find demarkation point where left half is the shortcut path, right half is the shortcut name + shortcuts_s.push_back(startmenuStrings.substr(last, nextComma - last)); + + // Skip whitespace, find next element + last = nextComma + 1ull; + while (last < startmenuStrings.size() && (startmenuStrings[last] == L' ' || startmenuStrings[last] == L'\r' || startmenuStrings[last] == L'\t' || startmenuStrings[last] == L'\n')) + last++; + } + + // Set progress bar range to include all files + shortcuts + 1 (cleanup step) + TaskLogger::SetRange(entries.size() + shortcuts_d.size() + shortcuts_s.size() + 2); + size_t progress = 0ull; + + // Remove all files in the installation folder, list them + std::error_code er; + if (!entries.size()) + TaskLogger::PushText("Already uninstalled / no files found.\r\n"); + else { + for each (const auto & entry in entries) { + TaskLogger::PushText("Deleting file: \"" + entry.path().string() + "\"\r\n"); + std::filesystem::remove(entry, er); + TaskLogger::SetProgress(++progress); + } + } + + // Remove all shortcuts + for each (const auto & shortcut in shortcuts_d) { + const auto path = get_users_desktop() + "\\" + std::filesystem::path(shortcut).filename().string() + ".lnk"; + TaskLogger::PushText("Deleting desktop shortcut: \"" + path + "\"\r\n"); + std::filesystem::remove(path, er); + progress++; + } + for each (const auto & shortcut in shortcuts_s) { + const auto path = get_users_startmenu() + "\\" + std::filesystem::path(shortcut).filename().string() + ".lnk"; + TaskLogger::PushText("Deleting start-menu shortcut: \"" + path + "\"\r\n"); + std::filesystem::remove(path, er); + progress++; + } + + // Clean up whatever's left (empty folders) + std::filesystem::remove_all(directory, er); + TaskLogger::SetProgress(++progress); + + // Remove registry entry for this uninstaller + RegDeleteKeyExW(HKEY_LOCAL_MACHINE, (L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" + m_mfStrings[L"name"]).c_str(), KEY_ALL_ACCESS, NULL); + TaskLogger::SetProgress(++progress); + }); +} + +void Uninstaller::dumpErrorLog() +{ + // Dump error log to disk + const auto dir = get_current_directory() + "\\error_log.txt"; + const auto t = std::time(0); + char dateData[127]; + ctime_s(dateData, 127, &t); + std::string logData(""); + + // If the log doesn't exist, add header text + if (!std::filesystem::exists(dir)) + logData += "Uninstaller error log:\r\n"; + + // Add remaining log data + logData += std::string(dateData) + TaskLogger::PullText() + "\r\n"; + + // Try to create the file + std::filesystem::create_directories(std::filesystem::path(dir).parent_path()); + std::ofstream file(dir, std::ios::binary | std::ios::out | std::ios::app); + if (!file.is_open()) + TaskLogger::PushText("Cannot dump error log to disk...\r\n"); + else + file.write(logData.c_str(), (std::streamsize)logData.size()); + file.close(); +} + +void Uninstaller::paint() +{ + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(m_hwnd, &ps)); + + // Draw Background + const LinearGradientBrush backgroundGradient1( + Point(0, 0), + Point(0, 500), + Color(255, 25, 25, 25), + Color(255, 75, 75, 75) + ); + graphics.FillRectangle(&backgroundGradient1, 0, 0, 170, 500); + + // Draw Steps + const SolidBrush lineBrush(Color(255, 100, 100, 100)); + graphics.FillRectangle(&lineBrush, 28, 0, 5, 500); + constexpr static wchar_t* step_labels[] = { L"Welcome", L"Uninstall", L"Finish" }; + FontFamily fontFamily(L"Segoe UI"); + Font font(&fontFamily, 15, FontStyleBold, UnitPixel); + REAL vertical_offset = 15; + const auto frameIndex = (int)m_currentIndex; + for (int x = 0; x < 3; ++x) { + // Draw Circle + auto color = x < frameIndex ? Color(255, 100, 100, 100) : x == frameIndex ? Color(255, 25, 225, 125) : Color(255, 255, 255, 255); + if (x == 2 && frameIndex == 3) + color = Color(255, 225, 25, 75); + const SolidBrush brush(color); + Pen pen(color); + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawEllipse(&pen, 20, (int)vertical_offset, 20, 20); + graphics.FillEllipse(&brush, 20, (int)vertical_offset, 20, 20); + + // Draw Text + graphics.DrawString(step_labels[x], -1, &font, PointF{ 50, vertical_offset }, &brush); + + if (x == 1) + vertical_offset = 460; + else + vertical_offset += 50; + } + + EndPaint(m_hwnd, &ps); +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + const auto ptr = (Uninstaller*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + if (message == WM_PAINT) + ptr->paint(); + else if (message == WM_DESTROY) + PostQuitMessage(0); + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/Uninstaller/Uninstaller.h b/src/Uninstaller/Uninstaller.h new file mode 100644 index 0000000..95c3a84 --- /dev/null +++ b/src/Uninstaller/Uninstaller.h @@ -0,0 +1,71 @@ +#pragma once +#ifndef UNINSTALLER_H +#define UNINSTALLER_H + +#include "Resource.h" +#include "Threader.h" +#include +#include +#include + + +class Screen; + +/** Encapsulates the logical features of the uninstaller. */ +class Uninstaller { +public: + // Public (de)Constructors + ~Uninstaller() = default; + Uninstaller(const HINSTANCE hInstance); + + + // Public Enumerations + const enum ScreenEnums { + WELCOME_SCREEN, UNINSTALL_SCREEN, FINISH_SCREEN, FAIL_SCREEN, + SCREEN_COUNT + }; + + + // Public Methods + /** When called, invalidates the uninstaller, halting it from progressing. */ + void invalidate(); + /** Make the screen identified by the supplied enum as active, deactivating the previous screen. + @param screenIndex the new screen to use. */ + void setScreen(const ScreenEnums & screenIndex); + /** Retrieves the installation directory. + @return active installation directory. */ + std::wstring getDirectory() const; + /** Uninstall the application. */ + void beginUninstallation(); + /** Dumps error log to disk. */ + static void dumpErrorLog(); + /** Render this window. */ + void paint(); + + + // Public manifest strings + struct compare_string { + bool operator()(const wchar_t * a, const wchar_t * b) const { + return wcscmp(a, b) < 0; + } + }; + std::map m_mfStrings; + + +private: + // Private Constructors + Uninstaller(); + + + // Private Attributes + Threader m_threader; + Resource m_manifest; + std::wstring m_directory = L""; + bool m_valid = true; + ScreenEnums m_currentIndex = WELCOME_SCREEN; + Screen * m_screens[SCREEN_COUNT]; + HWND m_hwnd = nullptr; +}; + + +#endif // UNINSTALLER_H \ No newline at end of file diff --git a/src/Uninstaller/Uninstaller.rc b/src/Uninstaller/Uninstaller.rc new file mode 100644 index 0000000..c97483c Binary files /dev/null and b/src/Uninstaller/Uninstaller.rc differ diff --git a/src/Uninstaller/icon.ico b/src/Uninstaller/icon.ico new file mode 100644 index 0000000..ddd6009 Binary files /dev/null and b/src/Uninstaller/icon.ico differ diff --git a/src/Uninstaller/manifest.nman b/src/Uninstaller/manifest.nman new file mode 100644 index 0000000..e69de29 diff --git a/src/Unpacker/CMakeLists.txt b/src/Unpacker/CMakeLists.txt new file mode 100644 index 0000000..7e63da3 --- /dev/null +++ b/src/Unpacker/CMakeLists.txt @@ -0,0 +1,49 @@ +################ +### Unpacker ### +################ +set (Module Unpacker) + +# Get source files +file (GLOB_RECURSE ROOT RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "*.cpp" "*.c" "*.h" "*.rc") +# Mimic the file-folder structure +foreach(source IN LISTS ROOT) + get_filename_component(source_path "${source}" PATH) + string(REPLACE "/" "\\" source_path_msvc "${source_path}") + source_group("${source_path_msvc}" FILES "${source}") +endforeach() + +# Add source files +add_executable(${Module} + ${ROOT} + ${CORE_DIR}/Common.h + ${CORE_DIR}/Resource.h + ${CORE_DIR}/Threader.h + ${CORE_DIR}/Instructions.h + ${CORE_DIR}/Instructions.cpp + ${CORE_DIR}/BufferTools.h + ${CORE_DIR}/BufferTools.cpp + ${CORE_DIR}/DirectoryTools.h + ${CORE_DIR}/DirectoryTools.cpp + ${CORE_DIR}/TaskLogger.h +) + +# Set windows/visual studio settings +target_compile_Definitions(${Module} PRIVATE $<$:DEBUG>) +set_target_properties(${Module} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + PDB_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + VS_DEBUGGER_WORKING_DIRECTORY "$(SolutionDir)app" +) + +# Force highest c++ version supported +if (MSVC_VERSION GREATER_EQUAL "1900") + include(CheckCXXCompilerFlag) + CHECK_CXX_COMPILER_FLAG("/std:c++latest" _cpp_latest_flag_supported) + if (_cpp_latest_flag_supported) + add_compile_options("/std:c++latest") + set_target_properties(${Module} PROPERTIES CXX_STANDARD 17) + set_target_properties(${Module} PROPERTIES CXX_STANDARD_REQUIRED ON) + endif() +endif() \ No newline at end of file diff --git a/src/Unpacker/README.md b/src/Unpacker/README.md new file mode 100644 index 0000000..d6f9284 --- /dev/null +++ b/src/Unpacker/README.md @@ -0,0 +1,14 @@ +# Unpacker +This program is a mini portable installation application. It doesn't modify the registry, nor generates any further applications. +Its goal is to be a quick means of dumping some package into the directory it runs from. + +It doesn't use any fancy rendering library, it instead runs in a terminal and requires no user input. + +This application has 2 (two) resources embedded within it: + - IDI_ICON1 the application icon + - IDR_ARCHIVE the package to be installed + +The raw version of this application is useless on its own, and is intended to be fullfilled by nSuite using the `-packager` command. +The following is how nSuite uses this application: + - writes out a copy of this application to disk + - packages a directory, embedding the package resource into **this application's** IDR_ARCHIVE resource \ No newline at end of file diff --git a/src/Unpacker/Unpacker.cpp b/src/Unpacker/Unpacker.cpp new file mode 100644 index 0000000..b86aa02 --- /dev/null +++ b/src/Unpacker/Unpacker.cpp @@ -0,0 +1,60 @@ +#include "Common.h" +#include "DirectoryTools.h" +#include "Resource.h" +#include + + +/** Entry point. */ +int main() +{ + // Tap-in to the log, have it redirect to the console + TaskLogger::AddCallback_TextAdded([&](const std::string & message) { + std::cout << message; + }); + + TaskLogger::PushText( + " ~\r\n" + " Unpackager /\r\n" + " ~------------------~\r\n" + " /\r\n" + "~\r\n\r\n" + ); + + // Acquire archive resource + const auto start = std::chrono::system_clock::now(); + const auto dstDirectory = sanitize_path(get_current_directory()); + size_t fileCount(0ull), byteCount(0ull); + Resource archive(IDR_ARCHIVE, "ARCHIVE"); + if (!archive.exists()) + exit_program("Cannot access archive resource (may be absent, corrupt, or have different identifiers), aborting...\r\n"); + + // Read the package header + char * packBufferOffset = reinterpret_cast(archive.getPtr()); + const auto folderSize = *reinterpret_cast(packBufferOffset); + packBufferOffset = reinterpret_cast(PTR_ADD(packBufferOffset, size_t(sizeof(size_t)))); + const auto folderName = std::string(reinterpret_cast(packBufferOffset), folderSize); + + // Report where we're unpacking to + TaskLogger::PushText( + "Unpacking to the following directory:\r\n" + "\t> " + dstDirectory + "\\" + folderName + + "\r\n" + ); + + // Unpackage using the resource file + if (!DRT::DecompressDirectory(dstDirectory, reinterpret_cast(archive.getPtr()), archive.getSize(), &byteCount, &fileCount)) + exit_program("Cannot decompress embedded package resource, aborting...\r\n"); + + // Success, report results + const auto end = std::chrono::system_clock::now(); + const std::chrono::duration elapsed_seconds = end - start; + TaskLogger::PushText( + "Files written: " + std::to_string(fileCount) + "\r\n" + + "Bytes written: " + std::to_string(byteCount) + "\r\n" + + "Total duration: " + std::to_string(elapsed_seconds.count()) + " seconds\r\n\r\n" + ); + + // Pause and exit + system("pause"); + exit(EXIT_SUCCESS); +} \ No newline at end of file diff --git a/src/Unpacker/Unpacker.rc b/src/Unpacker/Unpacker.rc new file mode 100644 index 0000000..29c05a7 Binary files /dev/null and b/src/Unpacker/Unpacker.rc differ diff --git a/src/Unpacker/archive.npack b/src/Unpacker/archive.npack new file mode 100644 index 0000000..e69de29 diff --git a/src/nStaller/icon.ico b/src/Unpacker/icon.ico similarity index 100% rename from src/nStaller/icon.ico rename to src/Unpacker/icon.ico diff --git a/src/Updater/CMakeLists.txt b/src/Updater/CMakeLists.txt new file mode 100644 index 0000000..cdc70e9 --- /dev/null +++ b/src/Updater/CMakeLists.txt @@ -0,0 +1,49 @@ +############### +### Updater ### +################ +set (Module Updater) + +# Get source files +file (GLOB_RECURSE ROOT RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "*.cpp" "*.c" "*.h" "*.rc") +# Mimic the file-folder structure +foreach(source IN LISTS ROOT) + get_filename_component(source_path "${source}" PATH) + string(REPLACE "/" "\\" source_path_msvc "${source_path}") + source_group("${source_path_msvc}" FILES "${source}") +endforeach() + +# Add source files +add_executable(${Module} + ${ROOT} + ${CORE_DIR}/Common.h + ${CORE_DIR}/Resource.h + ${CORE_DIR}/Threader.h + ${CORE_DIR}/Instructions.h + ${CORE_DIR}/Instructions.cpp + ${CORE_DIR}/BufferTools.h + ${CORE_DIR}/BufferTools.cpp + ${CORE_DIR}/DirectoryTools.h + ${CORE_DIR}/DirectoryTools.cpp + ${CORE_DIR}/TaskLogger.h +) + +# Set visual studio settings +target_compile_Definitions(${Module} PRIVATE $<$:DEBUG>) +set_target_properties(${Module} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + PDB_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + VS_DEBUGGER_WORKING_DIRECTORY "$(SolutionDir)app" +) + +# Force highest c++ version supported +if (MSVC_VERSION GREATER_EQUAL "1900") + include(CheckCXXCompilerFlag) + CHECK_CXX_COMPILER_FLAG("/std:c++latest" _cpp_latest_flag_supported) + if (_cpp_latest_flag_supported) + add_compile_options("/std:c++latest") + set_target_properties(${Module} PROPERTIES CXX_STANDARD 17) + set_target_properties(${Module} PROPERTIES CXX_STANDARD_REQUIRED ON) + endif() +endif() \ No newline at end of file diff --git a/src/Updater/README.md b/src/Updater/README.md new file mode 100644 index 0000000..6eb6083 --- /dev/null +++ b/src/Updater/README.md @@ -0,0 +1,17 @@ +# Updater +This program is a naive upadater application. It consumes .ndiff files found next to it, and applies them automatically if it can. + +Its goal is to be a quick means of updating a directory for a user. + +It attempts to provide reasonable security against corruption by comparing hashes before/after patching, and aborts before any actual changes are made to disk. + + +## Notes: +.ndiff files are generated by nSuite using the `-diff` command. + +This program essentially encapsulates nSuite's `-patch` command. + +It doesn't use any fancy rendering library, it instead runs in a terminal and requires minimal user input. + +This application has 1 (one) resource embedded within it: + - IDI_ICON1 the application icon diff --git a/src/nUpdater/nUpdater.cpp b/src/Updater/Updater.cpp similarity index 61% rename from src/nUpdater/nUpdater.cpp rename to src/Updater/Updater.cpp index 8a60cb7..7a92e32 100644 --- a/src/nUpdater/nUpdater.cpp +++ b/src/Updater/Updater.cpp @@ -1,5 +1,6 @@ #include "Common.h" #include "DirectoryTools.h" +#include "TaskLogger.h" #include #include @@ -19,31 +20,37 @@ static auto get_patches(const std::string & srcDirectory) /** Entry point. */ int main() { + // Tap-in to the log, have it redirect to the console + TaskLogger::AddCallback_TextAdded([&](const std::string & message) { + std::cout << message; + }); + // Find all patch files? - const auto dstDirectory(get_current_directory()); + const auto dstDirectory = sanitize_path(get_current_directory()); const auto patches = get_patches(dstDirectory); // Report an overview of supplied procedure - std::cout << - " ~\n" - " Updater /\n" - " ~-----------------~\n" - " /\n" - "~\n\n" - "There are " << patches.size() << " patches(s) found.\n" - "\n"; + TaskLogger::PushText( + " ~\r\n" + " Updater /\r\n" + " ~------------------~\r\n" + " /\r\n" + "~\r\n\r\n" + "There are " + std::to_string(patches.size()) + " patches(s) found.\r\n" + "\r\n" + ); if (patches.size()) { pause_program("Ready to update?"); // Begin updating const auto start = std::chrono::system_clock::now(); - size_t bytesWritten(0ull), instructionsUsed(0ull), patchesApplied(0ull); + size_t bytesWritten(0ull), patchesApplied(0ull); for each (const auto & patch in patches) { // Open diff file std::ifstream diffFile(patch, std::ios::binary | std::ios::beg); const size_t diffSize = std::filesystem::file_size(patch); if (!diffFile.is_open()) { - std::cout << "Cannot read diff file, skipping...\n"; + TaskLogger::PushText("Cannot read diff file, skipping...\r\n"); continue; } else { @@ -51,15 +58,15 @@ int main() char * diffBuffer = new char[diffSize]; diffFile.read(diffBuffer, std::streamsize(diffSize)); diffFile.close(); - if (!DRT::PatchDirectory(dstDirectory, diffBuffer, diffSize, bytesWritten, instructionsUsed)) { - std::cout << "skipping patch...\n"; + if (!DRT::PatchDirectory(dstDirectory, diffBuffer, diffSize, &bytesWritten)) { + TaskLogger::PushText("skipping patch...\r\n"); delete[] diffBuffer; continue; } // Delete patch file at very end if (!std::filesystem::remove(patch)) - std::cout << "Cannot delete diff file \"" << patch << "\" from disk, make sure to delete it manually.\n"; + TaskLogger::PushText("Cannot delete diff file \"" + patch.path().string() + "\" from disk, make sure to delete it manually.\r\n"); patchesApplied++; delete[] diffBuffer; } @@ -68,12 +75,14 @@ int main() // Success, report results const auto end = std::chrono::system_clock::now(); const std::chrono::duration elapsed_seconds = end - start; - std::cout - << "Patches used: " << patchesApplied << " out of " << patches.size() << "\n" - << "Bytes written: " << bytesWritten << "\n" - << "Total duration: " << elapsed_seconds.count() << " seconds\n\n"; + TaskLogger::PushText( + "Patches used: " + std::to_string(patchesApplied) + " out of " + std::to_string(patches.size()) + "\r\n" + + "Bytes written: " + std::to_string(bytesWritten) + "\r\n" + + "Total duration: " + std::to_string(elapsed_seconds.count()) + " seconds\r\n\r\n" + ); } + // Pause and exit system("pause"); exit(EXIT_SUCCESS); } \ No newline at end of file diff --git a/src/Updater/Updater.rc b/src/Updater/Updater.rc new file mode 100644 index 0000000..a497b89 Binary files /dev/null and b/src/Updater/Updater.rc differ diff --git a/src/nUpdater/icon.ico b/src/Updater/icon.ico similarity index 100% rename from src/nUpdater/icon.ico rename to src/Updater/icon.ico diff --git a/src/nStaller/CMakeLists.txt b/src/nStaller/CMakeLists.txt deleted file mode 100644 index 2689c36..0000000 --- a/src/nStaller/CMakeLists.txt +++ /dev/null @@ -1,42 +0,0 @@ -# Get source files for this project -file (GLOB_RECURSE ROOT RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "*.cpp" "*.c" "*.h" "*.rc") -# Generate source groups mimicking the folder structure -foreach(source IN LISTS ROOT) - get_filename_component(source_path "${source}" PATH) - string(REPLACE "/" "\\" source_path_msvc "${source_path}") - source_group("${source_path_msvc}" FILES "${source}") -endforeach() - - -############ -# EXEC # -############ -set (Module nStaller) -# Create a library using those source files -add_executable(${Module} ${ROOT} - ${CORE_DIR}/Common.h - ${CORE_DIR}/Resource.h - ${CORE_DIR}/Threader.h - ${CORE_DIR}/Instructions.h - ${CORE_DIR}/Instructions.cpp - ${CORE_DIR}/BufferTools.h - ${CORE_DIR}/BufferTools.cpp - ${CORE_DIR}/DirectoryTools.h - ${CORE_DIR}/DirectoryTools.cpp ) -# Set working directory to the project directory -set_target_properties (${Module} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} - LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} - ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} - PDB_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}) -target_compile_Definitions (${Module} PRIVATE $<$:DEBUG>) -set_target_properties(${Module} PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY "$(SolutionDir)app") - -if (MSVC_VERSION GREATER_EQUAL "1900") - include(CheckCXXCompilerFlag) - CHECK_CXX_COMPILER_FLAG("/std:c++latest" _cpp_latest_flag_supported) - if (_cpp_latest_flag_supported) - add_compile_options("/std:c++latest") - set_target_properties(${Module} PROPERTIES CXX_STANDARD 17) - set_target_properties(${Module} PROPERTIES CXX_STANDARD_REQUIRED ON) - endif() -endif() \ No newline at end of file diff --git a/src/nStaller/nStaller.cpp b/src/nStaller/nStaller.cpp deleted file mode 100644 index 2f3862e..0000000 --- a/src/nStaller/nStaller.cpp +++ /dev/null @@ -1,45 +0,0 @@ -#include "Common.h" -#include "DirectoryTools.h" -#include "Resource.h" -#include - - -/** Entry point. */ -int main() -{ - // Check command line arguments - std::string dstDirectory(get_current_directory()); - - // Report an overview of supplied procedure - std::cout << - " ~\n" - " Installer /\n" - " ~-----------------~\n" - " /\n" - "~\n\n" - "Installing to the following directory:\n" - "\t> " + dstDirectory + "\\\n" - "\n"; - pause_program("Ready to install?"); - - // Acquire archive resource - const auto start = std::chrono::system_clock::now(); - size_t fileCount(0ull), byteCount(0ull); - Resource archive(IDR_ARCHIVE, "ARCHIVE"); - if (!archive.exists()) - exit_program("Cannot access archive resource (may be absent, corrupt, or have different identifiers), aborting...\n"); - - // Unpackage using the resource file - if (!DRT::DecompressDirectory(dstDirectory, reinterpret_cast(archive.getPtr()), archive.getSize(), byteCount, fileCount)) - exit_program("Cannot decompress embedded package resource, aborting...\n"); - - // Success, report results - const auto end = std::chrono::system_clock::now(); - const std::chrono::duration elapsed_seconds = end - start; - std::cout - << "Files written: " << fileCount << "\n" - << "Bytes written: " << byteCount << "\n" - << "Total duration: " << elapsed_seconds.count() << " seconds\n\n"; - system("pause"); - exit(EXIT_SUCCESS); -} \ No newline at end of file diff --git a/src/nStaller/nStaller.rc b/src/nStaller/nStaller.rc deleted file mode 100644 index fb7149a..0000000 Binary files a/src/nStaller/nStaller.rc and /dev/null differ diff --git a/src/nSuite/CMakeLists.txt b/src/nSuite/CMakeLists.txt index 0620ca8..d91f0b6 100644 --- a/src/nSuite/CMakeLists.txt +++ b/src/nSuite/CMakeLists.txt @@ -1,38 +1,46 @@ -# Get source files for this project +############## +### nSuite ### +############## +set (Module nSuite) + +# Get source files file (GLOB_RECURSE ROOT RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "*.cpp" "*.c" "*.h" "*.rc") -# Generate source groups mimicking the folder structure +# Mimic the file-folder structure foreach(source IN LISTS ROOT) get_filename_component(source_path "${source}" PATH) string(REPLACE "/" "\\" source_path_msvc "${source_path}") source_group("${source_path_msvc}" FILES "${source}") endforeach() +# Add source files +add_executable(${Module} + ${ROOT} + ${CORE_DIR}/Common.h + ${CORE_DIR}/Resource.h + ${CORE_DIR}/Threader.h + ${CORE_DIR}/Instructions.h + ${CORE_DIR}/Instructions.cpp + ${CORE_DIR}/BufferTools.h + ${CORE_DIR}/BufferTools.cpp + ${CORE_DIR}/DirectoryTools.h + ${CORE_DIR}/DirectoryTools.cpp + ${CORE_DIR}/TaskLogger.h +) -############ -# EXEC # -############ -set (Module nSuite) -# Create a library using those source files -add_executable(${Module} ${ROOT} - ${CORE_DIR}/Common.h - ${CORE_DIR}/Resource.h - ${CORE_DIR}/Threader.h - ${CORE_DIR}/Instructions.h - ${CORE_DIR}/Instructions.cpp - ${CORE_DIR}/BufferTools.h - ${CORE_DIR}/BufferTools.cpp - ${CORE_DIR}/DirectoryTools.h - ${CORE_DIR}/DirectoryTools.cpp) -# This module requires the installer to be built first -add_dependencies(nSuite nStaller) -# Set working directory to the project directory -set_target_properties(${Module} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} - LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} - ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} - PDB_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} ) -target_compile_Definitions (${Module} PRIVATE $<$:DEBUG>) -set_target_properties(${Module} PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY "$(SolutionDir)app") +# This module requires the installer and updater to be built first +add_dependencies(nSuite Installer Updater Unpacker) + +# Set visual studio settings +target_compile_Definitions(${Module} PRIVATE $<$:DEBUG>) +set_target_properties(${Module} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + PDB_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + VS_DEBUGGER_WORKING_DIRECTORY "$(SolutionDir)app" +) +# Force highest c++ version supported if (MSVC_VERSION GREATER_EQUAL "1900") include(CheckCXXCompilerFlag) CHECK_CXX_COMPILER_FLAG("/std:c++latest" _cpp_latest_flag_supported) diff --git a/src/nSuite/Commands/DiffCommand.cpp b/src/nSuite/Commands/DiffCommand.cpp index 361b3fd..0b585fa 100644 --- a/src/nSuite/Commands/DiffCommand.cpp +++ b/src/nSuite/Commands/DiffCommand.cpp @@ -1,38 +1,39 @@ #include "DiffCommand.h" #include "BufferTools.h" -#include "DirectoryTools.h" #include "Common.h" +#include "DirectoryTools.h" +#include "TaskLogger.h" #include void DiffCommand::execute(const int & argc, char * argv[]) const { // Supply command header to console - std::cout << - " ~\n" - " Patch Maker /\n" - " ~-----------------~\n" - " /\n" - "~\n\n"; + TaskLogger::PushText( + " ~\r\n" + " Patch Maker /\r\n" + " ~-----------------~\r\n" + " /\r\n" + "~\r\n\r\n" + ); // Check command line arguments std::string oldDirectory(""), newDirectory(""), dstDirectory(""); for (int x = 2; x < argc; ++x) { - std::string command(argv[x], 5); - std::transform(command.begin(), command.end(), command.begin(), ::tolower); + std::string command = string_to_lower(std::string(argv[x], 5)); if (command == "-old=") - oldDirectory = std::string(&argv[x][5]); + oldDirectory = sanitize_path(std::string(&argv[x][5])); else if (command == "-new=") - newDirectory = std::string(&argv[x][5]); + newDirectory = sanitize_path(std::string(&argv[x][5])); else if (command == "-dst=") - dstDirectory = std::string(&argv[x][5]); + dstDirectory = sanitize_path(std::string(&argv[x][5])); else exit_program( - " Arguments Expected:\n" - " -old=[path to the older directory]\n" - " -new=[path to the newer directory]\n" - " -dst=[path to write the diff file] (can omit filename)\n" - "\n" + " Arguments Expected:\r\n" + " -old=[path to the older directory]\r\n" + " -new=[path to the newer directory]\r\n" + " -dst=[path to write the diff file] (can omit filename)\r\n" + "\r\n" ); } @@ -41,10 +42,7 @@ void DiffCommand::execute(const int & argc, char * argv[]) const const auto time = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); std::tm bt; localtime_s(&bt, &time); - dstDirectory = - dstDirectory - + "\\" - + std::to_string(bt.tm_year) + std::to_string(bt.tm_mon) + std::to_string(bt.tm_mday) + std::to_string(bt.tm_hour) + std::to_string(bt.tm_min) + std::to_string(bt.tm_sec); + dstDirectory = sanitize_path(dstDirectory) + "\\" + std::to_string(bt.tm_year) + std::to_string(bt.tm_mon) + std::to_string(bt.tm_mday) + std::to_string(bt.tm_hour) + std::to_string(bt.tm_min) + std::to_string(bt.tm_sec); } // Ensure a file-extension is chosen @@ -54,14 +52,14 @@ void DiffCommand::execute(const int & argc, char * argv[]) const // Diff the 2 directories specified char * diffBuffer(nullptr); size_t diffSize(0ull), instructionCount(0ull); - if (!DRT::DiffDirectory(oldDirectory, newDirectory, &diffBuffer, diffSize, instructionCount)) - exit_program("aborting...\n"); + if (!DRT::DiffDirectories(oldDirectory, newDirectory, &diffBuffer, diffSize, &instructionCount)) + exit_program("Cannot diff the two paths chosen, aborting...\r\n"); // Create diff file std::filesystem::create_directories(std::filesystem::path(dstDirectory).parent_path()); std::ofstream file(dstDirectory, std::ios::binary | std::ios::out); if (!file.is_open()) - exit_program("Cannot write diff file to disk, aborting...\n"); + exit_program("Cannot write diff file to disk, aborting...\r\n"); // Write the diff file to disk file.write(diffBuffer, std::streamsize(diffSize)); @@ -69,7 +67,8 @@ void DiffCommand::execute(const int & argc, char * argv[]) const delete[] diffBuffer; // Output results - std::cout - << "Instruction(s): " << instructionCount << "\n" - << "Bytes written: " << diffSize << "\n"; + TaskLogger::PushText( + "Instruction(s): " + std::to_string(instructionCount) + "\r\n" + + "Bytes written: " + std::to_string(diffSize) + "\r\n" + ); } \ No newline at end of file diff --git a/src/nSuite/Commands/InstallerCommand.cpp b/src/nSuite/Commands/InstallerCommand.cpp index b430842..9fa9ab3 100644 --- a/src/nSuite/Commands/InstallerCommand.cpp +++ b/src/nSuite/Commands/InstallerCommand.cpp @@ -1,42 +1,43 @@ #include "InstallerCommand.h" #include "BufferTools.h" +#include "Common.h" #include "DirectoryTools.h" +#include "TaskLogger.h" #include "Resource.h" -#include "Common.h" #include void InstallerCommand::execute(const int & argc, char * argv[]) const { // Supply command header to console - std::cout << - " ~\n" - " Installer Maker /\n" - " ~-----------------~\n" - " /\n" - "~\n\n"; + TaskLogger::PushText( + " ~\r\n" + " Installer Maker /\r\n" + " ~-----------------~\r\n" + " /\r\n" + "~\r\n\r\n" + ); // Check command line arguments std::string srcDirectory(""), dstDirectory(""); for (int x = 2; x < argc; ++x) { - std::string command(argv[x], 5); - std::transform(command.begin(), command.end(), command.begin(), ::tolower); + std::string command = string_to_lower(std::string(argv[x], 5)); if (command == "-src=") - srcDirectory = std::string(&argv[x][5]); + srcDirectory = sanitize_path(std::string(&argv[x][5])); else if (command == "-dst=") - dstDirectory = std::string(&argv[x][5]); + dstDirectory = sanitize_path(std::string(&argv[x][5])); else exit_program( - " Arguments Expected:\n" - " -src=[path to the directory to compress]\n" - " -dst=[path to write the installer] (can omit filename)\n" - "\n" + " Arguments Expected:\r\n" + " -src=[path to the directory to package]\r\n" + " -dst=[path to write the installer] (can omit filename)\r\n" + "\r\n" ); } // If user provides a directory only, append a filename - if (std::filesystem::is_directory(dstDirectory)) - dstDirectory += "\\installer.exe"; + if (std::filesystem::is_directory(dstDirectory)) + dstDirectory = sanitize_path(dstDirectory) + "\\installer.exe"; // Ensure a file-extension is chosen if (!std::filesystem::path(dstDirectory).has_extension()) @@ -44,32 +45,51 @@ void InstallerCommand::execute(const int & argc, char * argv[]) const // Compress the directory specified char * packBuffer(nullptr); - size_t packSize(0ull), fileCount(0ull); - if (!DRT::CompressDirectory(srcDirectory, &packBuffer, packSize, fileCount)) - exit_program("Cannot create installer from the directory specified, aborting...\n"); + size_t packSize(0ull), maxSize(0ull), fileCount(0ull); + if (!DRT::CompressDirectory(srcDirectory, &packBuffer, packSize, &maxSize, &fileCount, {"\\manifest.nman"})) + exit_program("Cannot create installer from the directory specified, aborting...\r\n"); // Acquire installer resource Resource installer(IDR_INSTALLER, "INSTALLER"); if (!installer.exists()) - exit_program("Cannot access installer resource, aborting...\n"); + exit_program("Cannot access installer resource, aborting...\r\n"); // Write installer to disk std::filesystem::create_directories(std::filesystem::path(dstDirectory).parent_path()); std::ofstream file(dstDirectory, std::ios::binary | std::ios::out); if (!file.is_open()) - exit_program("Cannot write installer to disk, aborting...\n"); + exit_program("Cannot write installer to disk, aborting...\r\n"); file.write(reinterpret_cast(installer.getPtr()), (std::streamsize)installer.getSize()); file.close(); // Update installer's resource auto handle = BeginUpdateResource(dstDirectory.c_str(), false); if (!(bool)UpdateResource(handle, "ARCHIVE", MAKEINTRESOURCE(IDR_ARCHIVE), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), packBuffer, (DWORD)packSize)) - exit_program("Cannot write archive contents to the installer, aborting...\n"); + exit_program("Cannot write archive contents to the installer, aborting...\r\n"); + // Try to find manifest file + if (std::filesystem::exists(srcDirectory + "\\manifest.nman")) { + const auto manifestSize = std::filesystem::file_size(srcDirectory + "\\manifest.nman"); + std::ifstream maniFile(srcDirectory + "\\manifest.nman", std::ios::binary | std::ios::beg); + if (!maniFile.is_open()) + exit_program("Cannot open manifest file from disk, aborting...\r\n"); + + // Read manifest file + char * maniBuffer = new char[manifestSize]; + maniFile.read(maniBuffer, (std::streamsize)manifestSize); + maniFile.close(); + + // Update installers' manifest resource + if (!(bool)UpdateResource(handle, "MANIFEST", MAKEINTRESOURCE(IDR_MANIFEST), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), maniBuffer, (DWORD)manifestSize)) + exit_program("Cannot write manifest contents to the installer, aborting...\r\n"); + delete[] maniBuffer; + } EndUpdateResource(handle, FALSE); delete[] packBuffer; // Output results - std::cout - << "Files packaged: " << fileCount << "\n" - << "Bytes packaged: " << packSize << "\n"; + TaskLogger::PushText( + "Files packaged: " + std::to_string(fileCount) + "\r\n" + + "Bytes packaged: " + std::to_string(maxSize) + "\r\n" + + "Compressed Size: " + std::to_string(packSize) + "\r\n" + ); } \ No newline at end of file diff --git a/src/nSuite/Commands/PackCommand.cpp b/src/nSuite/Commands/PackCommand.cpp index e5e1138..b6b246d 100644 --- a/src/nSuite/Commands/PackCommand.cpp +++ b/src/nSuite/Commands/PackCommand.cpp @@ -1,41 +1,42 @@ #include "PackCommand.h" #include "BufferTools.h" -#include "DirectoryTools.h" #include "Common.h" +#include "DirectoryTools.h" +#include "TaskLogger.h" #include void PackCommand::execute(const int & argc, char * argv[]) const { // Supply command header to console - std::cout << - " ~\n" - " Packager /\n" - " ~-----------------~\n" - " /\n" - "~\n\n"; + TaskLogger::PushText( + " ~\r\n" + " Packager /\r\n" + " ~----------------~\r\n" + " /\r\n" + "~\r\n\r\n" + ); // Check command line arguments std::string srcDirectory(""), dstDirectory(""); for (int x = 2; x < argc; ++x) { - std::string command(argv[x], 5); - std::transform(command.begin(), command.end(), command.begin(), ::tolower); + std::string command = string_to_lower(std::string(argv[x], 5)); if (command == "-src=") - srcDirectory = std::string(&argv[x][5]); + srcDirectory = sanitize_path(std::string(&argv[x][5])); else if (command == "-dst=") - dstDirectory = std::string(&argv[x][5]); + dstDirectory = sanitize_path(std::string(&argv[x][5])); else exit_program( - " Arguments Expected:\n" - " -src=[path to the directory to compress]\n" - " -dst=[path to write the package] (can omit filename)\n" - "\n" + " Arguments Expected:\r\n" + " -src=[path to the directory to package]\r\n" + " -dst=[path to write the package] (can omit filename)\r\n" + "\r\n" ); } // If user provides a directory only, append a filename if (std::filesystem::is_directory(dstDirectory)) - dstDirectory += "\\" + std::filesystem::path(srcDirectory).stem().generic_string() + ".npack"; + dstDirectory = sanitize_path(dstDirectory) + "\\" + std::filesystem::path(srcDirectory).stem().string() + ".npack"; // Ensure a file-extension is chosen if (!std::filesystem::path(dstDirectory).has_extension()) @@ -43,21 +44,23 @@ void PackCommand::execute(const int & argc, char * argv[]) const // Compress the directory specified char * packBuffer(nullptr); - size_t packSize(0ull), fileCount(0ull); - if (!DRT::CompressDirectory(srcDirectory, &packBuffer, packSize, fileCount)) - exit_program("Cannot create package from the directory specified, aborting...\n"); + size_t packSize(0ull), maxSize(0ull), fileCount(0ull); + if (!DRT::CompressDirectory(srcDirectory, &packBuffer, packSize, &maxSize, &fileCount)) + exit_program("Cannot create package from the directory specified, aborting...\r\n"); // Write package to disk std::filesystem::create_directories(std::filesystem::path(dstDirectory).parent_path()); std::ofstream file(dstDirectory, std::ios::binary | std::ios::out); if (!file.is_open()) - exit_program("Cannot write package to disk, aborting...\n"); + exit_program("Cannot write package to disk, aborting...\r\n"); file.write(packBuffer, (std::streamsize)packSize); file.close(); delete[] packBuffer; // Output results - std::cout - << "Files packaged: " << fileCount << "\n" - << "Bytes packaged: " << packSize << "\n"; + TaskLogger::PushText( + "Files packaged: " + std::to_string(fileCount) + "\r\n" + + "Bytes packaged: " + std::to_string(maxSize) + "\r\n" + + "Compressed Size: " + std::to_string(packSize) + "\r\n" + ); } \ No newline at end of file diff --git a/src/nSuite/Commands/PackagerCommand.cpp b/src/nSuite/Commands/PackagerCommand.cpp new file mode 100644 index 0000000..11762c1 --- /dev/null +++ b/src/nSuite/Commands/PackagerCommand.cpp @@ -0,0 +1,78 @@ +#include "PackagerCommand.h" +#include "BufferTools.h" +#include "Common.h" +#include "DirectoryTools.h" +#include "TaskLogger.h" +#include "Resource.h" +#include + + +void PackagerCommand::execute(const int & argc, char * argv[]) const +{ + // Supply command header to console + TaskLogger::PushText( + " ~\r\n" + " Portable Package Maker /\r\n" + " ~------------------------~\r\n" + " /\r\n" + "~\r\n\r\n" + ); + + // Check command line arguments + std::string srcDirectory(""), dstDirectory(""); + for (int x = 2; x < argc; ++x) { + std::string command = string_to_lower(std::string(argv[x], 5)); + if (command == "-src=") + srcDirectory = sanitize_path(std::string(&argv[x][5])); + else if (command == "-dst=") + dstDirectory = sanitize_path(std::string(&argv[x][5])); + else + exit_program( + " Arguments Expected:\r\n" + " -src=[path to the directory to package]\r\n" + " -dst=[path to write the portable package] (can omit filename)\r\n" + "\r\n" + ); + } + + // If user provides a directory only, append a filename + if (std::filesystem::is_directory(dstDirectory)) + dstDirectory = sanitize_path(dstDirectory) + "\\package.exe"; + + // Ensure a file-extension is chosen + if (!std::filesystem::path(dstDirectory).has_extension()) + dstDirectory += ".exe"; + + // Compress the directory specified + char * packBuffer(nullptr); + size_t packSize(0ull), maxSize(0ull), fileCount(0ull); + if (!DRT::CompressDirectory(srcDirectory, &packBuffer, packSize, &maxSize, &fileCount)) + exit_program("Cannot create package from the directory specified, aborting...\r\n"); + + // Acquire installer resource + Resource unpacker(IDR_UNPACKER, "UNPACKER"); + if (!unpacker.exists()) + exit_program("Cannot access unpacker resource, aborting...\r\n"); + + // Write installer to disk + std::filesystem::create_directories(std::filesystem::path(dstDirectory).parent_path()); + std::ofstream file(dstDirectory, std::ios::binary | std::ios::out); + if (!file.is_open()) + exit_program("Cannot write package to disk, aborting...\r\n"); + file.write(reinterpret_cast(unpacker.getPtr()), (std::streamsize)unpacker.getSize()); + file.close(); + + // Update installer's resource + auto handle = BeginUpdateResource(dstDirectory.c_str(), false); + if (!(bool)UpdateResource(handle, "ARCHIVE", MAKEINTRESOURCE(IDR_ARCHIVE), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), packBuffer, (DWORD)packSize)) + exit_program("Cannot write archive contents to the package, aborting...\r\n"); + EndUpdateResource(handle, FALSE); + delete[] packBuffer; + + // Output results + TaskLogger::PushText( + "Files packaged: " + std::to_string(fileCount) + "\r\n" + + "Bytes packaged: " + std::to_string(maxSize) + "\r\n" + + "Compressed Size: " + std::to_string(packSize) + "\r\n" + ); +} \ No newline at end of file diff --git a/src/nSuite/Commands/PackagerCommand.h b/src/nSuite/Commands/PackagerCommand.h new file mode 100644 index 0000000..a393ef7 --- /dev/null +++ b/src/nSuite/Commands/PackagerCommand.h @@ -0,0 +1,15 @@ +#pragma once +#ifndef PACKAGERCOMMAND_H +#define PACKAGERCOMMAND_H + +#include "Command.h" + + +/** Command to compress an entire directory into a portable installer. */ +class PackagerCommand : public Command { +public: + // Public interface implementation + virtual void execute(const int & argc, char * argv[]) const override; +}; + +#endif // PACKAGERCOMMAND_H \ No newline at end of file diff --git a/src/nSuite/Commands/PatchCommand.cpp b/src/nSuite/Commands/PatchCommand.cpp index 128fa23..41d27d9 100644 --- a/src/nSuite/Commands/PatchCommand.cpp +++ b/src/nSuite/Commands/PatchCommand.cpp @@ -1,55 +1,60 @@ #include "PatchCommand.h" #include "BufferTools.h" -#include "DirectoryTools.h" #include "Common.h" +#include "DirectoryTools.h" +#include "TaskLogger.h" #include void PatchCommand::execute(const int & argc, char * argv[]) const { // Supply command header to console - std::cout << - " ~\n" - " Patcher /\n" - " ~-----------------~\n" - " /\n" - "~\n\n"; + TaskLogger::PushText( + " ~\r\n" + " Patcher /\r\n" + " ~-----------------~\r\n" + " /\r\n" + "~\r\n\r\n" + ); // Check command line arguments std::string srcDirectory(""), dstDirectory(""); for (int x = 2; x < argc; ++x) { - std::string command(argv[x], 5); - std::transform(command.begin(), command.end(), command.begin(), ::tolower); + std::string command = string_to_lower(std::string(argv[x], 5)); if (command == "-src=") - srcDirectory = std::string(&argv[x][5]); + srcDirectory = sanitize_path(std::string(&argv[x][5])); else if (command == "-dst=") - dstDirectory = std::string(&argv[x][5]); + dstDirectory = sanitize_path(std::string(&argv[x][5])); else exit_program( - " Arguments Expected:\n" - " -src=[path to the .ndiff file]\n" - " -dst=[path to the directory to patch]\n" - "\n" + " Arguments Expected:\r\n" + " -src=[path to the .ndiff file]\r\n" + " -dst=[path to the directory to patch]\r\n" + "\r\n" ); } + // Ensure a file-extension is chosen + if (!std::filesystem::path(dstDirectory).has_extension()) + dstDirectory += ".ndiff"; // Open diff file std::ifstream diffFile(srcDirectory, std::ios::binary | std::ios::beg); const size_t diffSize = std::filesystem::file_size(srcDirectory); if (!diffFile.is_open()) - exit_program("Cannot read diff file, aborting...\n"); + exit_program("Cannot read diff file, aborting...\r\n"); // Patch the directory specified char * diffBuffer = new char[diffSize]; diffFile.read(diffBuffer, std::streamsize(diffSize)); diffFile.close(); size_t bytesWritten(0ull), instructionsUsed(0ull); - if (!DRT::PatchDirectory(dstDirectory, diffBuffer, diffSize, bytesWritten, instructionsUsed)) - exit_program("aborting...\n"); + if (!DRT::PatchDirectory(dstDirectory, diffBuffer, diffSize, &bytesWritten, &instructionsUsed)) + exit_program("aborting...\r\n"); delete[] diffBuffer; // Output results - std::cout - << "Instruction(s): " << instructionsUsed << "\n" - << "Bytes written: " << bytesWritten << "\n"; + TaskLogger::PushText( + "Instruction(s): " + std::to_string(instructionsUsed) + "\r\n" + + "Bytes written: " + std::to_string(bytesWritten) + "\r\n" + ); } \ No newline at end of file diff --git a/src/nSuite/Commands/UnpackCommand.cpp b/src/nSuite/Commands/UnpackCommand.cpp index d5bcc36..ab6d92e 100644 --- a/src/nSuite/Commands/UnpackCommand.cpp +++ b/src/nSuite/Commands/UnpackCommand.cpp @@ -1,43 +1,48 @@ #include "UnpackCommand.h" #include "BufferTools.h" -#include "DirectoryTools.h" #include "Common.h" +#include "DirectoryTools.h" +#include "TaskLogger.h" #include void UnpackCommand::execute(const int & argc, char * argv[]) const { // Supply command header to console - std::cout << - " ~\n" - " Unpacker /\n" - " ~-----------------~\n" - " /\n" - "~\n\n"; + TaskLogger::PushText( + " ~\r\n" + " Unpacker /\r\n" + " ~-----------------~\r\n" + " /\r\n" + "~\r\n\r\n" + ); // Check command line arguments std::string srcDirectory(""), dstDirectory(""); for (int x = 2; x < argc; ++x) { - std::string command(argv[x], 5); - std::transform(command.begin(), command.end(), command.begin(), ::tolower); + std::string command = string_to_lower(std::string(argv[x], 5)); if (command == "-src=") - srcDirectory = std::string(&argv[x][5]); + srcDirectory = sanitize_path(std::string(&argv[x][5])); else if (command == "-dst=") - dstDirectory = std::string(&argv[x][5]); + dstDirectory = sanitize_path(std::string(&argv[x][5])); else exit_program( - " Arguments Expected:\n" - " -src=[path to the package file]\n" - " -dst=[directory to write package contents]\n" - "\n" + " Arguments Expected:\r\n" + " -src=[path to the package file]\r\n" + " -dst=[directory to write package contents]\r\n" + "\r\n" ); } + // Ensure a file-extension is chosen + if (!std::filesystem::path(srcDirectory).has_extension()) + srcDirectory += ".npack"; + // Open pack file std::ifstream packFile(srcDirectory, std::ios::binary | std::ios::beg); const size_t packSize = std::filesystem::file_size(srcDirectory); if (!packFile.is_open()) - exit_program("Cannot read diff file, aborting...\n"); + exit_program("Cannot read diff file, aborting...\r\n"); // Copy contents into a buffer char * packBuffer = new char[packSize]; @@ -45,12 +50,14 @@ void UnpackCommand::execute(const int & argc, char * argv[]) const packFile.close(); // Unpackage using the resource file - size_t fileCount(0ull), byteCount(0ull); - if (!DRT::DecompressDirectory(dstDirectory, packBuffer, packSize, byteCount, fileCount)) - exit_program("Cannot decompress package file, aborting...\n"); + size_t byteCount(0ull), fileCount(0ull); + if (!DRT::DecompressDirectory(dstDirectory, packBuffer, packSize, &byteCount, &fileCount)) + exit_program("Cannot decompress package file, aborting...\r\n"); + delete[] packBuffer; // Output results - std::cout - << "Files written: " << fileCount << "\n" - << "Bytes written: " << byteCount << "\n"; + TaskLogger::PushText( + "Files written: " + std::to_string(fileCount) + "\r\n" + + "Bytes processed: " + std::to_string(byteCount) + "\r\n" + ); } \ No newline at end of file diff --git a/src/nSuite/README.md b/src/nSuite/README.md new file mode 100644 index 0000000..cd15ce3 --- /dev/null +++ b/src/nSuite/README.md @@ -0,0 +1,41 @@ +# nSuite +The nSuite program is intended to be used by developers or those who wish to package/diff/distribute one or many files. +It is a command-line application and is run by using one of the following arguments: + +- #### `-installer -src= -dst=` + - Creates a fully-fledged installer (Windows) with all the contents of the source directory + - Writes the installer to the destination path specified + - Generates an uninstaller, and links it up in the user's registry + - Can write a 'manifest.nman' file in the root source directory + - Specify string attributes for the installer, such as name, version, derscriptions, shortcuts + +- #### `-packager -src= -dst=` + - Creates a mini installer with no GUI (terminal only) with all the contents of the source directory + - No uninstaller, registry modifications, or manifest file + +- #### `-pack -src= -dst=` + - Packages and compresses all the contents of the source directory into an .npack file + - Writes package file to the destination path specified + - Requires nSuite to unpackage + - Note: + - these package files are what is embedded in the installers above + - can be used in diffing as a substitution for a source 'old/new' directory + +- #### `-unpack -src= -dst=` + - Decompresses and unpackages the contents held in an .npack file + - Writes package contents to the destination path specified + - Note: this command is executed what is executed in the installers above + +- #### `-diff -old= -new= -dst=` + - Finds all the common, added, and removed files between the old and new directories specified + - Generates patch instructions for all the differences found between all these files (including add/delete file instructions) + - Files are analyzed byte/8byte wise, and is accelerated by multiple threads + - **Instead of directories, can specify .npack files. Usefull if needing to maintain multiple versions on disk.** + - nSuite will virtualize the content within, treating it as a directory for you. + + - #### `-patch -src= -dst=` + - Uses a source .ndiff file, and executes all the patch instructions it contains on the destination directory specified + - Security: + - Patch file has before/after hashes + - File hashes must match, otherwise the application is aborted prior to writing-out to disk + - Strict conditions to prevent against file corruption. \ No newline at end of file diff --git a/src/nSuite/nSuite.cpp b/src/nSuite/nSuite.cpp index eb15202..57bff6c 100644 --- a/src/nSuite/nSuite.cpp +++ b/src/nSuite/nSuite.cpp @@ -1,9 +1,11 @@ #include "Common.h" +#include "TaskLogger.h" #include // Command inclusions #include "Commands/Command.h" #include "Commands/InstallerCommand.h" +#include "Commands/PackagerCommand.h" #include "Commands/DiffCommand.h" #include "Commands/PatchCommand.h" #include "Commands/PackCommand.h" @@ -18,36 +20,43 @@ int main(int argc, char *argv[]) struct compare_string { bool operator()(const char * a, const char * b) const { return strcmp(a, b) < 0; } }; const std::map commandMap{ { "-installer" , new InstallerCommand() }, + { "-packager" , new PackagerCommand() }, { "-pack" , new PackCommand() }, { "-unpack" , new UnpackCommand() }, { "-diff" , new DiffCommand() }, { "-patch" , new PatchCommand() } }; + TaskLogger::AddCallback_TextAdded([&](const std::string & message) { + std::cout << message; + }); // Check for valid arguments if (argc <= 1 || commandMap.find(argv[1]) == commandMap.end()) exit_program( - " ~\n" - " nStallr Help: /\n" - " ~-----------------~\n" - " /\n" - "~\n\n" - " Operations Supported:\n" - " -installer (To package and compress an entire directory into an executable file)\n" - " -pack (To compress an entire directory into a single .npack file)\n" - " -unpack (To decompress an entire directory from a .npack file)\n" - " -diff (To diff an entire directory into a single .ndiff file)\n" - " -patch (To patch an entire directory from a .ndiff file)\n" - "\n\n" + " ~\r\n" + " nSuite Help: /\r\n" + " ~-----------------~\r\n" + " /\r\n" + "~\r\n\r\n" + " Operations Supported:\r\n" + " -installer (Packages a directory into a GUI Installer (for Windows))\r\n" + " -packager (Packages a directory into a portable package executable (like a mini installer))\r\n" + " -pack (Packages a directory into an .npack file)\r\n" + " -unpack (Unpackages into a directory from an .npack file)\r\n" + " -diff (Diff. 2 directories into an .ndiff file)\r\n" + " -patch (Patches a directory from an .ndiff file)\r\n" + "\r\n\r\n" ); // Command exists in command map, execute it commandMap.at(argv[1])->execute(argc, argv); - // Output results and finish + // Success, report results const auto end = std::chrono::system_clock::now(); const std::chrono::duration elapsed_seconds = end - start; - std::cout << "Total duration: " << elapsed_seconds.count() << " seconds\n\n"; + TaskLogger::PushText("Total duration: " + std::to_string(elapsed_seconds.count()) + " seconds\r\n\r\n"); + + // Pause and exit system("pause"); exit(EXIT_SUCCESS); } \ No newline at end of file diff --git a/src/nSuite/nSuite.rc b/src/nSuite/nSuite.rc index 0e81f79..3db002a 100644 Binary files a/src/nSuite/nSuite.rc and b/src/nSuite/nSuite.rc differ diff --git a/src/nUpdater/CMakeLists.txt b/src/nUpdater/CMakeLists.txt deleted file mode 100644 index fc1600e..0000000 --- a/src/nUpdater/CMakeLists.txt +++ /dev/null @@ -1,42 +0,0 @@ -# Get source files for this project -file (GLOB_RECURSE ROOT RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "*.cpp" "*.c" "*.h" "*.rc") -# Generate source groups mimicking the folder structure -foreach(source IN LISTS ROOT) - get_filename_component(source_path "${source}" PATH) - string(REPLACE "/" "\\" source_path_msvc "${source_path}") - source_group("${source_path_msvc}" FILES "${source}") -endforeach() - - -############ -# EXEC # -############ -set (Module nUpdater) -# Create a library using those source files -add_executable(${Module} ${ROOT} - ${CORE_DIR}/Common.h - ${CORE_DIR}/Resource.h - ${CORE_DIR}/Threader.h - ${CORE_DIR}/Instructions.h - ${CORE_DIR}/Instructions.cpp - ${CORE_DIR}/BufferTools.h - ${CORE_DIR}/BufferTools.cpp - ${CORE_DIR}/DirectoryTools.h - ${CORE_DIR}/DirectoryTools.cpp ) -# Set working directory to the project directory -set_target_properties (${Module} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} - LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} - ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} - PDB_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}) -target_compile_Definitions (${Module} PRIVATE $<$:DEBUG>) -set_target_properties(${Module} PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY "$(SolutionDir)app") - -if (MSVC_VERSION GREATER_EQUAL "1900") - include(CheckCXXCompilerFlag) - CHECK_CXX_COMPILER_FLAG("/std:c++latest" _cpp_latest_flag_supported) - if (_cpp_latest_flag_supported) - add_compile_options("/std:c++latest") - set_target_properties(${Module} PROPERTIES CXX_STANDARD 17) - set_target_properties(${Module} PROPERTIES CXX_STANDARD_REQUIRED ON) - endif() -endif() \ No newline at end of file diff --git a/src/nUpdater/nUpdater.rc b/src/nUpdater/nUpdater.rc deleted file mode 100644 index 7330268..0000000 Binary files a/src/nUpdater/nUpdater.rc and /dev/null differ diff --git a/src/resource.h b/src/resource.h index ff6fd86..466d698 100644 --- a/src/resource.h +++ b/src/resource.h @@ -3,14 +3,17 @@ #define RESOURCE_H // Used for icons -#define IDI_ICON1 101 -// Used by installer.rc -#define IDR_ARCHIVE 102 +#define IDI_ICON1 101 // Used by installerMaker.rc -#define IDR_INSTALLER 103 +#define IDR_INSTALLER 102 +#define IDR_UNPACKER 103 +// Used by uninstaller.rc +#define IDR_ARCHIVE 104 +#define IDR_MANIFEST 105 +#define IDR_UNINSTALLER 106 #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NEXT_RESOURCE_VALUE 104 +#define _APS_NEXT_RESOURCE_VALUE 107 #define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_CONTROL_VALUE 1001 #define _APS_NEXT_SYMED_VALUE 101