From 7c74a3e7869a1795c34c461729c5f7acd69b143d Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Thu, 20 Oct 2022 17:41:23 +0200 Subject: [PATCH 1/4] feat: Re-implement argument parsing --- oodle-cli.cpp | 75 ++++++++++++++++++++++++++++----------------------- 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/oodle-cli.cpp b/oodle-cli.cpp index d74b1b0..caf2b44 100644 --- a/oodle-cli.cpp +++ b/oodle-cli.cpp @@ -24,37 +24,43 @@ const char* LIB_FILE = "oo2core_8_win64.dll"; void usage() { std::cerr << - "Usage: oodle-cli \n" + "Usage: oodle-cli [OPTIONS] [--] \n" "Decompress a to using the Oodle algorithm.\n" "The size of the uncompressed data must be known.\n" "\n" + "Options:\n" + " -v Increase Oodle's verbosity. May be specified up to three times.\n" + "\n" "The following environmental variables are recognized:\n" - " VERBOSITY: An integer between 0 and 3, with 0 being no logging and 3 being maximum logging.\n" " DLL_SEARCH_PATH: A color (':') separated list of directories to search for the Oodle library. May be relative to the current working directory.\n"; } int main(int argc, char* argv[]) { - if (argc < 4) { - std::cerr << "Arguments missing!\n\n"; - usage(); - return 1; + int verbosity = 0; + int i = 1; + + for (; i < argc; i++) { + char* arg = argv[i]; + + if (strcmp(arg, "--") == 0 || arg[0] != '-') { + break; + } + + if (strcmp(arg, "--help") == 0) { + usage(); + return 0; + } + else if (strcmp(arg, "-v") == 0) { + verbosity++; + } + else { + std::cerr << "WARN: Unknown option '" << arg << "'!\n\n"; + } } char* var; size_t len; - int verbosity = 0; - if (_dupenv_s(&var, &len, "VERBOSITY") == 0) { - if (var) { - verbosity = std::stoi(std::string(var, len)); - std::cout << "Setting Oodle verbosity to " << verbosity << ".\n"; - free(var); - } - } - else { - std::cerr << "Failed to read environment variable 'VERBOSITY'. Falling back to default.\n"; - } - DWORD load_flags = 0; if (_dupenv_s(&var, &len, "DLL_SEARCH_PATH") == 0) { if (var) { @@ -69,10 +75,10 @@ int main(int argc, char* argv[]) } if (!path.is_absolute() || !AddDllDirectory(path.c_str())) { - std::cerr << "Failed to add DLL search path: '" << token << "'!\n"; + std::cerr << "WARN: Failed to add DLL search path: '" << token << "'!\n"; } else { - std::cout << "Added DLL search path: '" << path << "'.\n"; + std::cout << "INFO: Added DLL search path: '" << path << "'.\n"; } } @@ -81,7 +87,7 @@ int main(int argc, char* argv[]) } } else { - std::cerr << "Failed to read environment variable 'DLL_SEARCH_PATH'. Skipping.\n"; + std::cerr << "ERROR: Failed to read environment variable 'DLL_SEARCH_PATH'. Skipping.\n"; } HINSTANCE hDLL = LoadLibraryEx(LIB_NAME, NULL, load_flags); @@ -96,21 +102,22 @@ int main(int argc, char* argv[]) return 1; } - std::string in_name = argv[1]; - std::string out_name = argv[2]; - size_t raw_buffer_size = std::stoi(argv[3]); - std::cout << "Attempting to decompress from '" << in_name << "' to '" << out_name << "' (" << raw_buffer_size << " bytes).\n"; - - std::ifstream in_file(in_name, std::ios::binary | std::ios::ate); - if (!in_file) { - std::cerr << "Failed to open compressed file!\n"; + if (argc - i < 3) { + std::cerr << "ERROR: Arguments missing!\n\n"; + usage(); return 1; } - std::ofstream out_file(out_name, std::ios::binary); - if (!out_file) { - std::cerr << "Failed to open output file!\n"; + std::string in_name = argv[i]; + std::string out_name = argv[i + 1]; + size_t raw_buffer_size = std::stoi(argv[i + 2]); + + std::cout << "INFO: Attempting to decompress from '" << in_name << "' to '" << out_name << "' (" << raw_buffer_size << " bytes).\n"; + + std::ifstream in_file(in_name, std::ios::binary | std::ios::ate); + if (!in_file) { + std::cerr << "ERROR: Failed to open compressed file!\n"; return 1; } @@ -121,7 +128,7 @@ int main(int argc, char* argv[]) in_file.read(compressed_buffer.data(), compressed_buffer_size); if (in_file.fail()) { - std::cerr << "Failed to read compressed file!\n"; + std::cerr << "ERROR: Failed to read compressed file!\n"; return 1; } @@ -156,5 +163,5 @@ int main(int argc, char* argv[]) return 1; } - std::cout << "Done!\n"; + std::cout << "INFO: Done!\n"; } From a615ae0354f35d026b59e80d33fb49bb41079ef8 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Thu, 20 Oct 2022 17:43:14 +0200 Subject: [PATCH 2/4] feat: Add argument to check library This mode allows checking if the DLL can be found, without having to create any kind of dummy data or parse stderr. --- oodle-cli.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/oodle-cli.cpp b/oodle-cli.cpp index caf2b44..554b0fe 100644 --- a/oodle-cli.cpp +++ b/oodle-cli.cpp @@ -30,6 +30,7 @@ void usage() { "\n" "Options:\n" " -v Increase Oodle's verbosity. May be specified up to three times.\n" + " -c Only check if the library can be found and used.\n" "\n" "The following environmental variables are recognized:\n" " DLL_SEARCH_PATH: A color (':') separated list of directories to search for the Oodle library. May be relative to the current working directory.\n"; @@ -38,6 +39,7 @@ void usage() { int main(int argc, char* argv[]) { int verbosity = 0; + bool check_lib = FALSE; int i = 1; for (; i < argc; i++) { @@ -51,6 +53,9 @@ int main(int argc, char* argv[]) usage(); return 0; } + else if (strcmp(arg, "-c") == 0) { + check_lib = TRUE; + } else if (strcmp(arg, "-v") == 0) { verbosity++; } @@ -96,12 +101,16 @@ int main(int argc, char* argv[]) return 1; } - auto decompress = (decltype(OodleLZ_Decompress)*)GetProcAddress((HMODULE)hDLL, "OodleLZ_Decompress"); + auto decompress = reinterpret_cast(GetProcAddress((HMODULE)hDLL, "OodleLZ_Decompress")); if (decompress == NULL) { std::cerr << "ERROR: The library is incompatible!\n"; return 1; } + if (check_lib) { + std::cout << "INFO: '" << LIB_FILE << "' found and loaded.\n"; + return 0; + } if (argc - i < 3) { std::cerr << "ERROR: Arguments missing!\n\n"; From 36edf93538973ee3a32c3b3f081a9cd00b9483b5 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Thu, 20 Oct 2022 17:45:45 +0200 Subject: [PATCH 3/4] feat: Implement Oodle logging The `verbosity` parameter hadn't worked before because there is a callback that needs to be set. Since the tool runs under Wine, the Windows default behaviour applied, which logged to the Event Viewer. To get output to stdout/stderr, a custom logging handler needs to be provided. --- oodle-cli.cpp | 33 ++++++++++++++++++++++++++++++++ oodle2.h | 53 ++++++++++++++++++++++++++++++++++----------------- 2 files changed, 69 insertions(+), 17 deletions(-) diff --git a/oodle-cli.cpp b/oodle-cli.cpp index 554b0fe..09406d5 100644 --- a/oodle-cli.cpp +++ b/oodle-cli.cpp @@ -22,6 +22,9 @@ const LPCWSTR LIB_NAME = L"oo2core_8_win64"; // It's not like this is going to change often. const char* LIB_FILE = "oo2core_8_win64.dll"; +typedef decltype(OodleLZ_Decompress)* decompress_func; +typedef decltype(OodleCore_Plugins_SetPrintf)* setprintf_func; + void usage() { std::cerr << "Usage: oodle-cli [OPTIONS] [--] \n" @@ -36,6 +39,27 @@ void usage() { " DLL_SEARCH_PATH: A color (':') separated list of directories to search for the Oodle library. May be relative to the current working directory.\n"; } +void printf_callback(int verboseLevel, const char* file, int line, const char* fmt, ...) { + std::cout << "[OODLE] "; + + // For some reason, this doesn't actually map to the config value. + switch (verboseLevel) { + case 0: + std::cout << "EXTRAORDINARILY IMPORTANT: "; + break; + case 1: + std::cout << "EXTERMELY IMPORTANT: "; + break; + case 2: + std::cout << "VERY IMPORTANT: "; + break; + } + + va_list args; + va_start(args, fmt); + vfprintf(stdout, fmt, args); +} + int main(int argc, char* argv[]) { int verbosity = 0; @@ -124,6 +148,15 @@ int main(int argc, char* argv[]) std::cout << "INFO: Attempting to decompress from '" << in_name << "' to '" << out_name << "' (" << raw_buffer_size << " bytes).\n"; + if (verbosity > 0) { + auto fn = reinterpret_cast(GetProcAddress((HMODULE)hDLL, "OodleCore_Plugins_SetPrintf")); + if (fn == NULL) { + std::cerr << "ERROR: The library is incompatible!\n"; + return 1; + } + reinterpret_cast(fn(printf_callback)); + } + std::ifstream in_file(in_name, std::ios::binary | std::ios::ate); if (!in_file) { std::cerr << "ERROR: Failed to open compressed file!\n"; diff --git a/oodle2.h b/oodle2.h index 1e77d92..3133263 100644 --- a/oodle2.h +++ b/oodle2.h @@ -7,12 +7,10 @@ // the DLL is incompatible when MAJOR is bumped // MINOR is for internal revs and bug fixes that don't affect API compatibility // TODO: Check if the DLL gives a minor version -#define OODLE2_VERSION_MAJOR 8 -#define OODLE2_VERSION_MINOR 14 +#define OODLE2_VERSION_MAJOR 8 +#define OODLE2_VERSION_MINOR 14 // OodleVersion string is 1 . MAJOR . MINOR -// don't make it from macros cuz the doc tool has to parse the string literal - #define OodleVersion "2.8.14" @@ -49,7 +47,6 @@ typedef enum OodleLZ_Compressor { } OodleLZ_Compressor; - typedef enum OodleLZ_CheckCRC { OodleLZ_CheckCRC_No = 0, OodleLZ_CheckCRC_Yes = 1, @@ -163,16 +160,16 @@ typedef enum OodleLZ_FuzzSafe { seek chunk boundaries are relative to _dictionaryBase_, not to _rawBuf_. */ -int OodleLZ_Compress( +extern "C" intptr_t __stdcall OodleLZ_Compress( OodleLZ_Compressor compressor, - const void* raw_buffer, + const void* raw_buffer, size_t raw_len, - void* compressed_buffer, + void* compressed_buffer, OodleLZ_CompressionLevel level, - void* options, // Default: null + void* options, // Default: null size_t dictionary_base, // Default: null - const void* lrm, // Default: null - void* scratch_memory, // Default: null + const void* lrm, // Default: null + void* scratch_memory, // Default: null size_t scratch_size // Default: null ); @@ -250,19 +247,41 @@ int OodleLZ_Compress( Use of OodleLZ_FuzzSafe_No is deprecated. */ -int OodleLZ_Decompress( +extern "C" intptr_t __stdcall OodleLZ_Decompress( const void* compressed_buffer, size_t compressed_length, - void* raw_buffer, + void* raw_buffer, size_t raw_length, OodleLZ_FuzzSafe fuzz_safe, // Default: OodleLZ_FuzzSafe_Yes OodleLZ_CheckCRC check_crc, // Default: OodleLZ_CheckCRC_No OodleLZ_Verbosity verbosity, // Default: OodleLZ_Verbosity_None - void* decBufBase, // Default: null + void* decBufBase, // Default: null size_t decBufSize, // Default: 0 - void* callback, // Default: null - void* callback_user_data, // Default: null - void* decoder_memory, // Default: null + void* callback, // Default: null + void* callback_user_data, // Default: null + void* decoder_memory, // Default: null size_t decoder_memory_size, // Default: 0 OodleLZ_Decode_ThreadPhase thread_phase // Default: OodleLZ_Decode_Unthreaded ); + +/* Function pointer to Oodle Core printf + $:verboseLevel verbosity of the message; 0-2 ; lower = more important + $:file C file that sent the message + $:line C line that sent the message + $:fmt vararg printf format string + The logging function installed here must parse varargs like printf. + _verboseLevel_ may be used to omit verbose messages. +*/ +extern "C" typedef void(__stdcall t_fp_OodleCore_Plugin_Printf)(int verboseLevel, const char* file, int line, const char* fmt, ...); + +/* Install the callback used by Oodle Core for logging + $:fp_rrRawPrintf function pointer to your log function; may be NULL to disable all logging + $:return returns the previous function pointer + Use this function to install your own printf for Oodle Core. + The default implementation in debug builds, if you install nothing, uses the C stdio printf for logging. + On Microsoft platforms, it uses OutputDebugString and not stdio. + To disable all logging, call OodleCore_Plugins_SetPrintf(NULL) + WARNING : this function is NOT thread safe! It should be done only once and done in a place where the caller can guarantee thread safety. + In the debug build of Oodle, you can install OodleCore_Plugin_Printf_Verbose to get more verbose logging +*/ +t_fp_OodleCore_Plugin_Printf* __stdcall OodleCore_Plugins_SetPrintf(t_fp_OodleCore_Plugin_Printf* fp_rrRawPrintf); \ No newline at end of file From 2bc7751d3b0e2e6c6eb917fa12244400bcbb3bed Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Thu, 20 Oct 2022 17:47:42 +0200 Subject: [PATCH 4/4] feat: Implement brute-forcing the raw size Oodle must know the size of the uncompressed data when decompressing data. If you don't know that size, the only option is to guess. And since it needs to be the exact number, you simply have to try every number. --- oodle-cli.cpp | 110 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 83 insertions(+), 27 deletions(-) diff --git a/oodle-cli.cpp b/oodle-cli.cpp index 09406d5..5dc4b09 100644 --- a/oodle-cli.cpp +++ b/oodle-cli.cpp @@ -25,15 +25,21 @@ const char* LIB_FILE = "oo2core_8_win64.dll"; typedef decltype(OodleLZ_Decompress)* decompress_func; typedef decltype(OodleCore_Plugins_SetPrintf)* setprintf_func; +#define OK 0 +#define GENERIC_ERROR -1 +#define DECOMPRESS_ERROR -2 + void usage() { std::cerr << "Usage: oodle-cli [OPTIONS] [--] \n" "Decompress a to using the Oodle algorithm.\n" - "The size of the uncompressed data must be known.\n" + "The size of the uncompressed data must be known. If 0 is given, the tool will\n" + "attempt to brute-force the correct number by incrementing from the compressed size.\n" "\n" "Options:\n" " -v Increase Oodle's verbosity. May be specified up to three times.\n" " -c Only check if the library can be found and used.\n" + " --guess When guessing the uncompressed size, a maximum number as multiple of the compressed size prevents an infinite loop. This changes the factor. Defaults to 10." "\n" "The following environmental variables are recognized:\n" " DLL_SEARCH_PATH: A color (':') separated list of directories to search for the Oodle library. May be relative to the current working directory.\n"; @@ -60,10 +66,51 @@ void printf_callback(int verboseLevel, const char* file, int line, const char* f vfprintf(stdout, fmt, args); } +int do_decompress(decompress_func decompress, std::vector compressed_buffer, std::string out_name, size_t raw_buffer_size, OodleLZ_Verbosity verbosity) { + std::vector raw_buffer(raw_buffer_size); + + intptr_t res = decompress( + compressed_buffer.data(), + compressed_buffer.size(), + raw_buffer.data(), + raw_buffer_size, + OodleLZ_FuzzSafe_Yes, + OodleLZ_CheckCRC_No, + verbosity, + nullptr, + 0, + nullptr, + nullptr, + nullptr, + 0, + OodleLZ_Decode_Unthreaded + ); + + if (res != raw_buffer_size) { + return DECOMPRESS_ERROR; + } + + std::ofstream out_file(out_name, std::ios::binary); + if (!out_file) { + std::cerr << "ERROR: Failed to open output file!\n"; + return GENERIC_ERROR; + } + + out_file.write(raw_buffer.data(), res); + + if (out_file.fail()) { + std::cerr << "ERROR: Failed to write output file!\n"; + return GENERIC_ERROR; + } + + return OK; +} + int main(int argc, char* argv[]) { int verbosity = 0; bool check_lib = FALSE; + int guess_factor = 10; int i = 1; for (; i < argc; i++) { @@ -83,6 +130,10 @@ int main(int argc, char* argv[]) else if (strcmp(arg, "-v") == 0) { verbosity++; } + else if (strcmp(arg, "--guess") == 0) { + i++; + guess_factor = std::stoi(argv[i]); + } else { std::cerr << "WARN: Unknown option '" << arg << "'!\n\n"; } @@ -174,35 +225,40 @@ int main(int argc, char* argv[]) return 1; } - std::vector raw_buffer(raw_buffer_size); + if (raw_buffer_size > 0) { + int res = do_decompress(decompress, compressed_buffer, out_name, raw_buffer_size, (OodleLZ_Verbosity)verbosity); - int res = decompress( - compressed_buffer.data(), - compressed_buffer_size, - raw_buffer.data(), - raw_buffer_size, - OodleLZ_FuzzSafe_Yes, - OodleLZ_CheckCRC_No, - (OodleLZ_Verbosity)verbosity, - nullptr, - 0, - nullptr, - nullptr, - nullptr, - 0, - OodleLZ_Decode_Unthreaded - ); - - out_file.write(raw_buffer.data(), res); - - if (out_file.fail()) { - std::cerr << "Failed to write output file!\n"; - return 1; + if (res == DECOMPRESS_ERROR) { + std::cerr << "ERROR: Failed to decompress!\n"; + return 1; + } + else if (res == GENERIC_ERROR) { + return 1; + } } + else { + size_t guessed_size = compressed_buffer.size(); + // I need some maximum value to avoid infinite loops. The factor is configurable. + size_t max = guessed_size * guess_factor; - if (res != raw_buffer_size) { - std::cerr << "Failed to decompress. Expected " << raw_buffer_size << " bytes, got " << res << "!\n"; - return 1; + while (true) { + int res = do_decompress(decompress, compressed_buffer, out_name, guessed_size, (OodleLZ_Verbosity)verbosity); + + if (res == GENERIC_ERROR) { + return 1; + } + else if (res == OK) { + std::cout << "INFO: Found decompressed size as " << guessed_size << "\n"; + break; + } + + guessed_size++; + + if (guessed_size > max) { + std::cerr << "ERROR: Exceeded maximum value for guessing uncompressed size!\n"; + return 1; + } + } } std::cout << "INFO: Done!\n";