diff --git a/meson.build b/meson.build index a87d6fa..621ea6d 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project('layeredfs', 'c', 'cpp', version: '4.1', default_options: [ - 'cpp_std=c++23', + 'cpp_std=c++26', 'buildtype=release', 'strip=true', 'werror=true', diff --git a/src/arc.cpp b/src/arc.cpp index 831f302..c07f1a7 100644 --- a/src/arc.cpp +++ b/src/arc.cpp @@ -34,11 +34,11 @@ std::optional ArcArchive::from_stream(std::istream &stream) { return std::nullopt; } if (hdr.magic != ARC_MAGIC) { - log_warning("arc: bad magic %08x", hdr.magic); + log_warning("arc: bad magic {:08x}", hdr.magic); return std::nullopt; } if (hdr.compression != ARC_COMPRESSION_NONE && hdr.compression != ARC_COMPRESSION_AVSLZ) { - log_warning("arc: unknown compression %u", hdr.compression); + log_warning("arc: unknown compression {}", hdr.compression); return std::nullopt; } @@ -57,7 +57,7 @@ std::optional ArcArchive::from_stream(std::istream &stream) { stream.seekg(e.file_offset); std::vector packed(e.packed_size); if (!stream.read(reinterpret_cast(packed.data()), e.packed_size)) { - log_warning("arc: couldn't read data for '%s'", name.c_str()); + log_warning("arc: couldn't read data for '{}'", name); return std::nullopt; } @@ -65,7 +65,7 @@ std::optional ArcArchive::from_stream(std::istream &stream) { size_t out_size = e.unpacked_size; auto raw = lz_decompress(packed.data(), packed.size(), &out_size); if (!raw || out_size != e.unpacked_size) { - log_warning("arc: decompression failed for '%s'", name.c_str()); + log_warning("arc: decompression failed for '{}'", name); if (raw) free(raw); return std::nullopt; } @@ -92,7 +92,7 @@ static uint32_t align_up(uint32_t v, uint32_t align) { bool ArcArchive::save(const char* path) { std::ofstream f(path, std::ios::binary); if (!f) { - log_warning("arc: couldn't open '%s' for writing", path); + log_warning("arc: couldn't open '{}' for writing", path); return false; } diff --git a/src/avs.cpp b/src/avs.cpp index a88563c..3356a9c 100644 --- a/src/avs.cpp +++ b/src/avs.cpp @@ -209,12 +209,6 @@ const avs_exports_t avs_exports[] = { FOREACH_AVS_FUNC(AVS_FUNC_PTR) FOREACH_AVS_FUNC_OPTIONAL(AVS_FUNC_PTR) -/*void* (*avs_fs_mount)(char* mountpoint, char* fsroot, char* fstype, int a5); -void* hook_avs_fs_mount(char* mountpoint, char* fsroot, char* fstype, int a5) { - log_misc("Mounting %s at %s with type %s", fsroot, mountpoint, fstype); - return avs_fs_mount(mountpoint, fsroot, fstype, a5); -}*/ - #define TEST_HOOK_AND_APPLY(func) if (MH_CreateHookApi(dll_name, avs_exports[i].func, (LPVOID)hook_ ## func, (LPVOID*)&func) != MH_OK || func == NULL) continue #define LOAD_FUNC(func) if( (func = (decltype(func))GetProcAddress(mod_handle, avs_exports[i].func)) == NULL) continue #define CHECK_UNIQUE(func) if( avs_exports[i].func != NULL && GetProcAddress(mod_handle, avs_exports[i].func) == NULL) continue @@ -227,7 +221,7 @@ bool init_avs(void) { #ifdef _DEBUG for (int i = 0; i < lenof(avs_exports); i++) { -#define VERBOSE_EXPORT_CHECK(ret_type, name, ...) if(avs_exports[i]. ## name == NULL) log_warning("MISSING EXPORT %d: %s", i, #name); +#define VERBOSE_EXPORT_CHECK(ret_type, name, ...) if(avs_exports[i]. ## name == NULL) log_warning("MISSING EXPORT {}: {}", i, #name); FOREACH_AVS_FUNC(VERBOSE_EXPORT_CHECK) FOREACH_AVS_FUNC_OPTIONAL(VERBOSE_EXPORT_CHECK) } @@ -291,7 +285,7 @@ property_t prop_from_file_handle(AVS_FILE f) { avs_fs_lseek(f, 0, SEEK_SET); memsize = property_read_query_memsize(avs_fs_read, f, NULL, NULL); if (memsize < 0) { - log_warning("Couldn't get memsize %08X (%s)", memsize, get_prop_error_str(memsize)); + log_warning("Couldn't get memsize {:08X} ({})", memsize, get_prop_error_str(memsize)); goto FAIL; } } @@ -305,7 +299,7 @@ property_t prop_from_file_handle(AVS_FILE f) { prop = property_create(flags, prop_buffer, memsize); if (!prop) { // double cast to squash truncation warning - log_warning("Couldn't create prop (%s)", get_prop_error_str((int32_t)(size_t)prop)); + log_warning("Couldn't create prop ({})", get_prop_error_str((int32_t)(size_t)prop)); goto FAIL; } @@ -314,7 +308,7 @@ property_t prop_from_file_handle(AVS_FILE f) { avs_fs_close(f); if (ret < 0) { - log_warning("Couldn't read prop (%s)", get_prop_error_str(ret)); + log_warning("Couldn't read prop ({})", get_prop_error_str(ret)); goto FAIL; } @@ -349,7 +343,7 @@ char* prop_to_xml_string(property_t prop, rapidxml::xml_document<>& allocator) { } else { xml[0] = '\0'; - log_warning("property_mem_write failed (%s)", get_prop_error_str(written)); + log_warning("property_mem_write failed ({})", get_prop_error_str(written)); } return xml; diff --git a/src/avs.h b/src/avs.h index 1b0ee0f..0b1ec3c 100644 --- a/src/avs.h +++ b/src/avs.h @@ -272,7 +272,7 @@ bool rapidxml_from_avs_file( try { doc.parse(xml); } catch (const rapidxml::parse_error& e) { - log_warning("Couldn't parse xml (%s byte %d)", e.what(), (int)(e.where() - xml)); + log_warning("Couldn't parse xml ({} byte {})", e.what(), (int)(e.where() - xml)); auto f = fopen("debug.xml", "wb"); fwrite(xml, strlen(xml), 1, f); return false; diff --git a/src/avs_standalone.cpp b/src/avs_standalone.cpp index a9e122b..3b149c9 100644 --- a/src/avs_standalone.cpp +++ b/src/avs_standalone.cpp @@ -157,7 +157,7 @@ LONG WINAPI exc_handler(_EXCEPTION_POINTERS *ExceptionInfo) { case DBG_PRINTEXCEPTION_C: break; default: - log_warning("Unhandled exception code 0x%lX at %p\n", + log_warning("Unhandled exception code {:#x} at {:p}", ExceptionInfo->ExceptionRecord->ExceptionCode, ExceptionInfo->ExceptionRecord->ExceptionAddress ); diff --git a/src/config.cpp b/src/config.cpp index 2491d23..7cee754 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -120,13 +120,14 @@ void load_config(void) { } void print_config(void) { - log_info("Options: %s=%d %s=%d %s=%d %s=%s %s=%s %s=%s %s=%s", + log_info("Options: {}={} {}={} {}={} {}={} {}={} {}={} {}={}", VERBOSE_FLAG, config.verbose_logs, DEVMODE_FLAG, config.developer_mode, DISABLE_FLAG, config.disable, - LOGFILE_FLAG, config.logfile, - ALLOWLIST_FLAG, allowlist, - BLOCKLIST_FLAG, blocklist, + // TODO: std::optional + LOGFILE_FLAG, config.logfile ? config.logfile : "(null)", + ALLOWLIST_FLAG, allowlist ? allowlist : "(null)", + BLOCKLIST_FLAG, blocklist ? blocklist : "(null)", MOD_FOLDER_FLAG, config.get_mod_folder().c_str() ); } diff --git a/src/hook.cpp b/src/hook.cpp index d0aa74f..c927da6 100644 --- a/src/hook.cpp +++ b/src/hook.cpp @@ -63,7 +63,7 @@ class PkfsHookFile final : public HookFile { log_if_modfile(); auto ret = pkfs_fs_open(get_path_to_open().c_str()); if(ret == 0) { - log_verbose("pkfs_fs_open(%s) failed in call_real", get_path_to_open().c_str()); + log_verbose("pkfs_fs_open({}) failed in call_real", get_path_to_open()); } return ret; } @@ -86,7 +86,7 @@ class PkfsHookFile final : public HookFile { // This of course is racey, so if there is a *real* error in another // thread, this resets it. But it's a tight race, and I'll take that // chance. - log_verbose("pkfs_open(%s) failed in load_to_vec, clearing HDD error", get_path_to_open().c_str()); + log_verbose("pkfs_open({}) failed in load_to_vec, clearing HDD error", get_path_to_open()); pkfs_clear_hdd_error(); return nullopt; } @@ -233,7 +233,7 @@ void handle_arc(HookFile &file) { } arc = std::move(*_arc); } else { - log_info("arc: no original file, creating from scratch: \"%s\"", file.norm_path.c_str()); + log_info("arc: no original file, creating from scratch: \"{}\"", file.norm_path); } auto out_folder = out.substr(0, out.rfind("/")); @@ -296,13 +296,13 @@ void handle_arc(HookFile &file) { auto out_folder = xml_orig.substr(0, xml_orig.rfind("/")); if (!mkdir_p(out_folder)) { - log_warning("Couldn't create arc xml cache folder %s", out_folder.c_str()); + log_warning("Couldn't create arc xml cache folder {}", out_folder); continue; } FILE *f = fopen(xml_orig.c_str(), "wb"); if (!f) { - log_warning("Couldn't create arc xml base at %s", xml_orig.c_str()); + log_warning("Couldn't create arc xml base at {}", xml_orig); continue; } fwrite(arc_file.second.data(), 1, arc_file.second.size(), f); @@ -324,7 +324,7 @@ void handle_arc(HookFile &file) { auto data = f.load_to_vec(); if (!data) { - log_warning("arc: couldn't load mod file '%s'", path.first.c_str()); + log_warning("arc: couldn't load mod file '{}'", path.first); continue; } arc.add_or_replace(name, std::move(*data)); @@ -338,7 +338,7 @@ void handle_arc(HookFile &file) { cache_hasher.commit(); file.mod_path = out; - log_misc("arc generation took %d ms", time() - start); + log_misc("arc generation took {} ms", time() - start); } void handle_texbin(HookFile &file) { @@ -413,7 +413,7 @@ void handle_texbin(HookFile &file) { } texbin = *_texbin; } else { - log_info("Found texbin mods but no original file, creating from scratch: \"%s\"", file.norm_path.c_str()); + log_info("Found texbin mods but no original file, creating from scratch: \"{}\"", file.norm_path); } auto folder_terminator = out.rfind("/"); @@ -438,7 +438,7 @@ void handle_texbin(HookFile &file) { cache_hasher.commit(); file.mod_path = out; - log_misc("Texbin generation took %d ms", time() - start); + log_misc("Texbin generation took {} ms", time() - start); } uint32_t handle_file_open(HookFile &file) { @@ -474,7 +474,7 @@ uint32_t handle_file_open(HookFile &file) { if(file.ramfs_demangle()) { ramfs_demangler_on_fs_open(file.path, ret); } - // log_verbose("(returned %d)", ret); + // log_verbose("(returned {})", ret); return ret; } @@ -482,7 +482,7 @@ int hook_avs_fs_lstat(const char* name, struct avs_stat *st) { if (name == NULL) return avs_fs_lstat(name, st); - log_verbose("statting %s", name); + log_verbose("statting {}", name); string path = name; // can it be modded ie is it under /data ? @@ -499,7 +499,7 @@ int hook_avs_fs_convert_path(char dest_name[256], const char *name) { if (name == NULL) return avs_fs_convert_path(dest_name, name); - log_verbose("convert_path %s", name); + log_verbose("convert_path {}", name); string path = name; // can it be modded ie is it under /data ? @@ -513,7 +513,7 @@ int hook_avs_fs_convert_path(char dest_name[256], const char *name) { } int hook_avs_fs_mount(const char* mountpoint, const char* fsroot, const char* fstype, const char* args) { - log_verbose("mounting %s to %s with type %s and args %s", fsroot, mountpoint, fstype, args); + log_verbose("mounting {} to {} with type {} and args {}", fsroot, mountpoint, fstype, args ? args : "(null)"); ramfs_demangler_on_fs_mount(mountpoint, fsroot, fstype, args); // In new jubeat, a modded IFS file will be loaded as such: @@ -559,7 +559,7 @@ DWORD WINAPI hook_GetLongPathNameA(LPCSTR lpszShortPath, LPSTR lpszLongPath, DWO AVS_FILE hook_avs_fs_open(const char* name, uint16_t mode, int flags) { if(name == NULL || inside_pkfs_hook) return avs_fs_open(name, mode, flags); - log_verbose("opening %s mode %d flags %d", name, mode, flags); + log_verbose("opening {} mode {} flags {}", name, mode, flags); // only touch reads if (mode != avs_open_mode_read()) { return avs_fs_open(name, mode, flags); @@ -577,7 +577,7 @@ AVS_FILE hook_avs_fs_open(const char* name, uint16_t mode, int flags) { } unsigned int hook_pkfs_open(const char *name) { - log_verbose("pkfs_open %s", name); + log_verbose("pkfs_open {}", name); string path = name; @@ -630,7 +630,8 @@ static void dump_loaded_dll_info() { bool first = true; for(auto mod = head; mod && (first || mod != head); mod = mod->Flink) { auto ldr = reinterpret_cast(mod); - log_verbose(" %.*ls", ldr->FullDllName.Length, ldr->FullDllName.Buffer); + std::string narrow_dll_name(ldr->FullDllName.Buffer, ldr->FullDllName.Buffer + ldr->FullDllName.Length); + log_verbose(" {}", narrow_dll_name); first = false; } } @@ -667,7 +668,7 @@ extern "C" { #ifdef SPECIAL_VER log_info("Build config: " SPECIAL_VER); #endif - log_info("AVS DLL detected: %s", avs_loaded_dll_name); + log_info("AVS DLL detected: {}", avs_loaded_dll_name); print_config(); #ifdef UNPAK log_info(".pak dumper mode enabled"); @@ -714,7 +715,7 @@ extern "C" { log_info("Detected mod folders:"); for (auto &p : available_mods()) { - log_info("%s", p.c_str()); + log_info("{}", p); } return 0; diff --git a/src/hook.h b/src/hook.h index 4b9fa5c..c082199 100644 --- a/src/hook.h +++ b/src/hook.h @@ -59,7 +59,7 @@ class HookFile { void log_if_modfile() { if (mod_path) { - log_verbose("Using %s", mod_path->c_str()); + log_verbose("Using {}", *mod_path); } } diff --git a/src/imagefs.cpp b/src/imagefs.cpp index 06ec1c6..263d120 100644 --- a/src/imagefs.cpp +++ b/src/imagefs.cpp @@ -88,7 +88,7 @@ bool add_images_to_list(string_set &extra_pngs, rapidxml::xml_node<> *texturelis vector textures; for (auto it = extra_pngs.begin(); it != extra_pngs.end(); ++it) { - log_verbose("New image: %s", it->c_str()); + log_verbose("New image: {}", *it); string png_tex = *it + ".png"; auto png_loc = find_first_modfile(ifs_mod_path + "/" + png_tex); @@ -122,7 +122,7 @@ bool add_images_to_list(string_set &extra_pngs, rapidxml::xml_node<> *texturelis log_warning("Couldn't pack textures :("); return false; } - log_misc("Texture packing %d ms", time() - pack_start); + log_misc("Texture packing {} ms", time() - pack_start); // because the property API, being // a) written by Konami @@ -187,7 +187,7 @@ bool add_images_to_list(string_set &extra_pngs, rapidxml::xml_node<> *texturelis } } - log_misc("Texture extend total time %d ms", time() - start); + log_misc("Texture extend total time {} ms", time() - start); return true; } @@ -198,7 +198,7 @@ void parse_texturelist(HookFile &file) { auto ifs_path = file.norm_path; // truncate ifs_path.resize(ifs_path.size() - strlen("/tex/texturelist.xml")); - // log_misc("Reading ifs %s", ifs_path.c_str()); + // log_misc("Reading ifs {}", ifs_path); auto ifs_mod_path = ifs_path; string_replace(ifs_mod_path, ".ifs", "_ifs"); @@ -240,14 +240,14 @@ void parse_texturelist(HookFile &file) { auto format = texture->first_attribute("format"); if (!format) { - log_warning("Texture missing format %s", path_to_open.c_str()); + log_warning("Texture missing format {}", path_to_open); continue; } //128 128 auto size = texture->first_node("size"); if (!size) { - log_warning("Texture missing size %s", path_to_open.c_str()); + log_warning("Texture missing size {}", path_to_open); continue; } @@ -263,7 +263,7 @@ void parse_texturelist(HookFile &file) { image = image->next_sibling("image")) { auto name = image->first_attribute("name"); if (!name) { - log_warning("Texture missing name %s", path_to_open.c_str()); + log_warning("Texture missing name {}", path_to_open); continue; } @@ -271,14 +271,14 @@ void parse_texturelist(HookFile &file) { auto imgrect = image->first_node("imgrect"); auto uvrect = image->first_node("uvrect"); if (!imgrect || !uvrect) { - log_warning("Texture missing dimensions %s", path_to_open.c_str()); + log_warning("Texture missing dimensions {}", path_to_open); continue; } // it's a 4u16 sscanf(imgrect->value(), "%" SCNu16 " %" SCNu16 " %" SCNu16 " %" SCNu16, &dimensions[0], &dimensions[1], &dimensions[2], &dimensions[3]); - // log_misc("Image '%s' compress %d format %d", name->value(), compress, format_type); + // log_misc("Image '{}' compress {} format {}", name->value(), compress, format_type); image_t image_info; image_info.name = name->value(); MD5 md5; @@ -297,7 +297,7 @@ void parse_texturelist(HookFile &file) { } } - log_verbose("%d added PNGs", extra_pngs.size()); + log_verbose("{} added PNGs", extra_pngs.size()); if (extra_pngs.size() > 0) { if (add_images_to_list(extra_pngs, texturelist_node, ifs_path, ifs_mod_path, compress)) prop_was_rewritten = true; @@ -341,12 +341,12 @@ bool cache_texture(string const&png_path, image_t const&tex) { error = lodepng_decode32_file(&image, &width, &height, png_path.c_str()); if (error) { - log_warning("can't load png %u: %s\n", error, lodepng_error_text(error)); + log_warning("can't load png {}: {}\n", error, lodepng_error_text(error)); return false; } if ((int)width != tex.width || (int)height != tex.height) { - log_warning("Loaded png (%dx%d) doesn't match texturelist.xml (%dx%d), ignoring", width, height, tex.width, tex.height); + log_warning("Loaded png ({}x{}) doesn't match texturelist.xml ({}x{}), ignoring", width, height, tex.width, tex.height); return false; } @@ -420,7 +420,7 @@ void parse_afplist(HookFile &file) { auto ifs_path = file.norm_path; // truncate ifs_path.resize(ifs_path.size() - strlen("/tex/afplist.xml")); - // log_misc("Reading ifs %s", ifs_path.c_str()); + // log_misc("Reading ifs {}", ifs_path); auto ifs_mod_path = ifs_path; string_replace(ifs_mod_path, ".ifs", "_ifs"); @@ -452,14 +452,14 @@ void parse_afplist(HookFile &file) { auto name = afp->first_attribute("name"); if (!name) { - log_warning("AFP missing name %s", path_to_open.c_str()); + log_warning("AFP missing name {}", path_to_open); continue; } // 5 8 11 16 19 auto geo = afp->first_node("geo"); if (!geo) { - log_warning("AFP missing geo %s", path_to_open.c_str()); + log_warning("AFP missing geo {}", path_to_open); continue; } @@ -469,7 +469,7 @@ void parse_afplist(HookFile &file) { .mod_path = ifs_mod_path + folder + file, }); mapped++; - // log_info("AFP %s -> %s", md5_path.c_str(), (ifs_mod_path + folder + file).c_str()); + // log_info("AFP {} -> {}", md5_path, ifs_mod_path + folder + file); }; std::lock_guard lock(afp_md5_names_mtx); @@ -485,7 +485,7 @@ void parse_afplist(HookFile &file) { } } - log_verbose("Mapped %d AFP filenames", mapped); + log_verbose("Mapped {} AFP filenames", mapped); } std::optional>> lookup_png_from_md5(HookFile &file) { @@ -495,7 +495,7 @@ std::optional>> lookup_png_from return std::nullopt; } - //log_misc("Mapped file %s is found!", norm_path.c_str()); + //log_misc("Mapped file {} is found!", norm_path); auto tex = tex_search->second; lock.unlock(); // is it safe to unlock this early? Time will tell... @@ -519,15 +519,15 @@ void handle_texture(HookFile &file) { auto &[png_path, tex] = *lookup; if (tex->compression == UNSUPPORTED_COMPRESS) { - log_warning("Unsupported compression for %s", png_path.c_str()); + log_warning("Unsupported compression for {}", png_path); return; } if (tex->format == UNSUPPORTED_FORMAT) { - log_warning("Unsupported texture format for %s", png_path.c_str()); + log_warning("Unsupported texture format for {}", png_path); return; } - log_verbose("Mapped file %s found!", png_path.c_str()); + log_verbose("Mapped file {} found!", png_path); if (cache_texture(png_path, *tex)) { file.mod_path = tex->cache_file(); } @@ -541,7 +541,7 @@ std::optional lookup_afp_from_md5(HookFile &file) { return std::nullopt; } - //log_misc("Mapped file %s is found!", norm_path.c_str()); + //log_misc("Mapped file {} is found!", norm_path); auto afp = afp_search->second; lock.unlock(); // is it safe to unlock this early? Time will tell... @@ -553,7 +553,7 @@ void handle_afp(HookFile &file) { if(!lookup) return; - log_verbose("Mapped file %s found!", lookup->c_str()); + log_verbose("Mapped file {} found!", *lookup); file.mod_path = *lookup; return; } @@ -584,9 +584,9 @@ void merge_xmls(HookFile &file) { auto cache_hasher = CacheHasher(out_hashed); cache_hasher.add(starting); // don't forget to take the input into account - log_info("Merging into %s", starting.c_str()); + log_info("Merging into {}", starting); for (auto &path : to_merge) { - log_info(" %s", path.c_str()); + log_info(" {}", path); cache_hasher.add(path); } cache_hasher.finish(); @@ -600,7 +600,7 @@ void merge_xmls(HookFile &file) { auto first_result = rapidxml_from_avs_filepath(starting, merged_xml, merged_xml); if (!first_result) { - log_warning("Couldn't merge (can't load first xml %s)", starting.c_str()); + log_warning("Couldn't merge (can't load first xml {})", starting); return; } @@ -608,7 +608,7 @@ void merge_xmls(HookFile &file) { rapidxml::xml_document<> rapid_to_merge; auto merge_load_result = rapidxml_from_avs_filepath(path, rapid_to_merge, merged_xml); if (!merge_load_result) { - log_warning("Couldn't merge (can't load xml) %s", path.c_str()); + log_warning("Couldn't merge (can't load xml) {}", path); return; } @@ -630,5 +630,5 @@ void merge_xmls(HookFile &file) { cache_hasher.commit(); file.mod_path = out; - log_misc("Merge took %d ms", time() - start); + log_misc("Merge took {} ms", time() - start); } diff --git a/src/log.hpp b/src/log.hpp index c5c31ce..e6ba270 100644 --- a/src/log.hpp +++ b/src/log.hpp @@ -1,24 +1,35 @@ +#pragma once + #include "config.hpp" // since log_verbose uses it +#include #ifndef LOG_MODULE #define LOG_MODULE "layeredfs" #endif +inline constexpr char _log_module[] = LOG_MODULE; + +using log_formatter_t = void (*)(const char *module, const char *fmt, ...); + +template +void log_base(std::format_string fmt, Args&&... args) { + (*Logger)(Module, "%s", std::format(fmt, std::forward(args)...).c_str()); +} + + // functions that default to file output, but will be overriden to point to AVS // logging functions if the user doesn't specify their own log file -#define log_fatal(...) imp_log_body_fatal(LOG_MODULE, __VA_ARGS__) -#define log_warning(...) imp_log_body_warning(LOG_MODULE, __VA_ARGS__) -#define log_info(...) imp_log_body_info(LOG_MODULE, __VA_ARGS__) -#define log_misc(...) imp_log_body_misc(LOG_MODULE, __VA_ARGS__) +extern log_formatter_t imp_log_body_fatal; +extern log_formatter_t imp_log_body_warning; +extern log_formatter_t imp_log_body_info; +extern log_formatter_t imp_log_body_misc; + +#define log_fatal(...) log_base<&imp_log_body_fatal, _log_module>(__VA_ARGS__); +#define log_warning(...) log_base<&imp_log_body_warning, _log_module>(__VA_ARGS__); +#define log_info(...) log_base<&imp_log_body_info, _log_module>(__VA_ARGS__); +#define log_misc(...) log_base<&imp_log_body_misc, _log_module>(__VA_ARGS__); // layeredfs super-verbose (since most people have loglevel misc already) #define log_verbose(...) if(config.verbose_logs) {log_misc(__VA_ARGS__);} // for the playpen void log_to_stdout(void); - -typedef void (*log_formatter_t)(const char *module, const char *fmt, ...); - -extern log_formatter_t imp_log_body_fatal; -extern log_formatter_t imp_log_body_warning; -extern log_formatter_t imp_log_body_info; -extern log_formatter_t imp_log_body_misc; diff --git a/src/modpath_handler.cpp b/src/modpath_handler.cpp index e8a0ee9..6671fff 100644 --- a/src/modpath_handler.cpp +++ b/src/modpath_handler.cpp @@ -39,13 +39,13 @@ std::set walk_dir(const string &path, const stri log_warning("\"data\" folder detected in mod root. Move all files inside to the mod root, or it will not work"); } result_path = root + ffd.cFileName + "/"; - log_verbose(" %s", result_path.c_str()); + log_verbose(" {}", result_path); auto subdir_walk = walk_dir(path + "/" + ffd.cFileName, result_path); result.insert(subdir_walk.begin(), subdir_walk.end()); } else { result_path = root + ffd.cFileName; - log_verbose(" %s", result_path.c_str()); + log_verbose(" {}", result_path); } result.insert(result_path); } while (FindNextFileA(contents, &ffd) != 0); @@ -64,7 +64,7 @@ void cache_mods(void) { config.developer_mode = devmode; for (auto &dir : avail_mods) { - log_verbose("Walking %s", dir.c_str()); + log_verbose("Walking {}", dir); mod_contents_t mod; mod.name = dir; // even in developer mode we want to walk the mods directory for effective logging @@ -82,7 +82,7 @@ static vector game_folders; void init_modpath_handler(void) { log_verbose("Top level folders:"); for (auto folder : folders_in_folder(".")) { - log_verbose(" %s", folder.c_str()); + log_verbose(" {}", folder); // data is the normal case we transparently handle if (!strcasecmp(folder.c_str(), "data")) { @@ -149,7 +149,7 @@ vector available_mods() { // if there is an allowlist, is this mod on it? if (!config.allowlist.empty() && config.allowlist.find(folder) == config.allowlist.end()) { if (first_search) - log_info("Ignoring non-allowlisted mod %s", folder.c_str()); + log_info("Ignoring non-allowlisted mod {}", folder); continue; } @@ -157,7 +157,7 @@ vector available_mods() { // is this mod in the blocklist? if (config.blocklist.find(folder) != config.blocklist.end()) { if (first_search) - log_info("Ignoring blocklisted mod %s", folder.c_str()); + log_info("Ignoring blocklisted mod {}", folder); continue; } @@ -220,7 +220,7 @@ optional find_first_cached_item(const string &norm_path) { } optional find_first_modfile(const string &norm_path) { - //log_verbose("%s(%s)", __FUNCTION__, norm_path.c_str()); + //log_verbose("{}({})", __FUNCTION__, norm_path); if (config.developer_mode) { for (auto &dir : available_mods()) { auto mod_path = dir + "/" + norm_path; diff --git a/src/playpen.cpp b/src/playpen.cpp index c39eb65..ba82d5a 100644 --- a/src/playpen.cpp +++ b/src/playpen.cpp @@ -35,7 +35,7 @@ void lz_unfuck(uint8_t *buf, size_t len) { } } - log_info("unfucked %d bytes", repl); + log_info("unfucked {} bytes", repl); } optional> readFile(const char* filename) @@ -45,7 +45,7 @@ optional> readFile(const char* filename) std::ifstream file(filename, std::ios::binary); if(!file) { - log_warning("Couldn't open %s", filename); + log_warning("Couldn't open {}", filename); return nullopt; } @@ -96,16 +96,16 @@ void avs_playpen() { // return; // } // auto debug = *_debug; - // log_info("Loaded %d bytes", debug.size()); + // log_info("Loaded {} bytes", debug.size()); // // lz_unfuck(&debug[8], comp_sz); // // auto decomp = lz_decompress(&debug[8], comp_sz, &decomp_sz); // auto decomp = texbin_lz77_decompress(debug); - // log_info("Decomp to %u", decomp.size()); + // log_info("Decomp to {}", decomp.size()); // auto comp = texbin_lz77_compress(decomp); - // log_info("Comp again to %u", comp.size()); + // log_info("Comp again to {}", comp.size()); // auto decomp2 = texbin_lz77_decompress(comp); - // log_info("Final decomp to %u", decomp2.size()); + // log_info("Final decomp to {}", decomp2.size()); // auto f = fopen("debug.out.bin", "wb"); // fwrite(&decomp[0], 1, decomp.size(), f); @@ -142,7 +142,7 @@ void avs_playpen() { auto memsize = property_read_query_memsize(avs_fs_read, f, NULL, NULL); if (memsize < 0) { - log_warning("Couldn't get memsize %08X", memsize); + log_warning("Couldn't get memsize {:08X}", memsize); goto FAIL; } @@ -176,7 +176,7 @@ FAIL: return; } for (char* n = avs_fs_readdir(d); n; n = avs_fs_readdir(d)) - log_info("dir %s", n); + log_info("dir {}", n); avs_fs_closedir(d);*/ //char name[64]; //auto playpen = prop_from_file("playpen.xml"); @@ -188,15 +188,15 @@ FAIL: //print_node(end); /*for (int i = 0; i <= 8; i++) { if (i == 6 || i == 3) continue; - log_info("Traverse: %d", i); + log_info("Traverse: {}", i); auto node = property_search(playpen, NULL, "/root/t2"); auto nnn = property_node_traversal(node, 8); auto nna = property_node_traversal(nnn, TRAVERSE_FIRST_ATTR); property_node_name(nna, name, 64); - log_info("bloop %s", name); + log_info("bloop {}", name); for (;node;node = property_node_traversal(node, i)) { if (!property_node_name(node, name, 64)) { - log_info(" %s", name); + log_info(" {}", name); } } }*/ diff --git a/src/ramfs_demangler.cpp b/src/ramfs_demangler.cpp index adb10ce..f1a588e 100644 --- a/src/ramfs_demangler.cpp +++ b/src/ramfs_demangler.cpp @@ -110,11 +110,11 @@ void ramfs_demangler_register_arc_inner_ifs(const std::string& basename, const s std::lock_guard lock(mangling_mtx); auto existing = arc_inner_by_basename.find(basename); if (existing != arc_inner_by_basename.end() && existing->second != demangled_path) { - log_warning("arc demangle: basename collision for '%s' (%s vs %s), later one wins", - basename.c_str(), existing->second.c_str(), demangled_path.c_str()); + log_warning("arc demangle: basename collision for '{}' ({} vs {}), later one wins", + basename, existing->second, demangled_path); } arc_inner_by_basename[basename] = demangled_path; - log_verbose("arc inner basename '%s' -> %s", basename.c_str(), demangled_path.c_str()); + log_verbose("arc inner basename '{}' -> {}", basename, demangled_path); } void ramfs_demangler_on_fs_read(AVS_FILE context, void* dest) { @@ -124,7 +124,7 @@ void ramfs_demangler_on_fs_read(AVS_FILE context, void* dest) { if (find != open_file_map.end()) { auto path = find->second; // even this is too verbose - //log_verbose("Mapped %p to %s", dest, path.c_str()); + // log_verbose("Mapped {:p} to {}", dest, path); ram_load_map[dest] = path; auto cleanup = cleanup_map.find(path); @@ -160,7 +160,7 @@ void ramfs_demangler_on_fs_mount(const char* mountpoint, const char* fsroot, con auto find = ram_load_map.find(buffer); if (find != ram_load_map.end()) { auto orig_path = find->second; - log_verbose("ramfs mount mapped to %s", orig_path.c_str()); + log_verbose("ramfs mount mapped to {}", orig_path); ramfs_map[mount_path.c_str()] = orig_path; auto cleanup = cleanup_map.find(orig_path); @@ -184,7 +184,7 @@ void ramfs_demangler_on_fs_mount(const char* mountpoint, const char* fsroot, con : arc_inner_by_basename.find(bn); if (by_name != arc_inner_by_basename.end()) { auto orig_path = by_name->second; - log_verbose("ramfs mount basename '%s' mapped to %s", bn.c_str(), orig_path.c_str()); + log_verbose("ramfs mount basename '{}' mapped to {}", bn, orig_path); ramfs_map[mount_path.c_str()] = orig_path; // No cleanup_map entry: we never saw the open for this inner ifs. } @@ -194,7 +194,7 @@ void ramfs_demangler_on_fs_mount(const char* mountpoint, const char* fsroot, con auto find = ramfs_map.find(fsroot); if (find != ramfs_map.end()) { auto orig_path = *find; - log_verbose("link mount mapped to %s", orig_path.c_str()); + log_verbose("link mount mapped to {}", orig_path); ramfs_map[mountpoint] = orig_path; auto cleanup = cleanup_map.find(orig_path); @@ -207,7 +207,7 @@ void ramfs_demangler_on_fs_mount(const char* mountpoint, const char* fsroot, con auto find = ramfs_map.longest_prefix(fsroot); if (find != ramfs_map.end()) { auto orig_path = *find; - log_verbose("imagefs mount mapped to %s", orig_path.c_str()); + log_verbose("imagefs mount mapped to {}", orig_path); mangling_map[mountpoint] = orig_path; auto cleanup = cleanup_map.find(orig_path); @@ -228,7 +228,7 @@ void ramfs_demangler_on_fs_mount(const char* mountpoint, const char* fsroot, con string root = (string)fsroot; ramfs_demangler_demangle_if_possible_nolock(root); if (normalise_path(root, /* demangle */ false)) { - log_verbose("imagefs mount mapped to %s", root.c_str()); + log_verbose("imagefs mount mapped to {}", root); mangling_map[mountpoint] = root; } } @@ -240,7 +240,7 @@ void ramfs_demangler_demangle_if_possible(std::string& raw_path) { auto search = mangling_map.longest_prefix(raw_path); if (search != mangling_map.end()) { - // log_verbose("can demangle %s to %s", search.key().c_str(), search->c_str()); + // log_verbose("can demangle {} to {}", search.key(), *search); string_replace(raw_path, search.key().c_str(), search->c_str()); } } diff --git a/src/tests.cpp b/src/tests.cpp index fdeb5ee..620b752 100644 --- a/src/tests.cpp +++ b/src/tests.cpp @@ -28,7 +28,7 @@ FOREACH_EXTRA_FUNC(AVS_FUNC_PTR) class LogTestName : public testing::EmptyTestEventListener { void OnTestStart(const testing::TestInfo& info) override { - log_misc("--------- Running %s.%s", info.test_suite_name(), info.name()); + log_misc("--------- Running {}.{}", info.test_suite_name(), info.name()); } }; @@ -133,7 +133,7 @@ TEST(ImageFs, MD5DemanglingWorks) { if(!norm) return std::string(); TestHookFile file(path, *norm); - log_info("Lookup %s norm %s", path.c_str(), norm->c_str()); + log_info("Lookup {} norm {}", path, *norm); auto lookup = lookup_png_from_md5(file); EXPECT_NE(lookup, std::nullopt); if(!lookup) return std::string(); @@ -150,7 +150,7 @@ TEST(ImageFs, MD5DemanglingWorks) { if(!norm) return std::string(); TestHookFile file(path, *norm); - log_info("Lookup %s norm %s", path.c_str(), norm->c_str()); + log_info("Lookup {} norm {}", path, *norm); auto lookup = lookup_afp_from_md5(file); EXPECT_NE(lookup, std::nullopt); if(!lookup) return std::string(); @@ -230,9 +230,8 @@ TEST(RamFs, DemanglingWorks) { ramfs_demangler_on_fs_open("./data/test.ifs", fake_handle); ramfs_demangler_on_fs_read(fake_handle, fake_buffer); - char* flags = snprintf_auto("base=0x%llx", (unsigned long long)(uintptr_t)fake_buffer); - ramfs_demangler_on_fs_mount("/sd0", "test.ifs", "ramfs", flags); - free(flags); + std::string flags = std::format("base={:#x}", (uintptr_t)fake_buffer); + ramfs_demangler_on_fs_mount("/sd0", "test.ifs", "ramfs", flags.c_str()); ramfs_demangler_on_fs_mount("/game/test", "/sd0/test.ifs", "imagefs", nullptr); @@ -264,9 +263,8 @@ TEST(RamFs, DemanglingWorksNabla) { ramfs_demangler_on_fs_open("/data/graphics/ver07/logo.ifs", fake_handle); ramfs_demangler_on_fs_read(fake_handle, fake_buffer); - char* flags = snprintf_auto("base=0x%llx", (unsigned long long)(uintptr_t)fake_buffer); - ramfs_demangler_on_fs_mount("/mnt/bm2d/rmp_89_2714881", "image.bin", "ramfs", flags); - free(flags); + std::string flags = std::format("base={:#x}", (uintptr_t)fake_buffer); + ramfs_demangler_on_fs_mount("/mnt/bm2d/rmp_89_2714881", "image.bin", "ramfs", flags.c_str()); ramfs_demangler_on_fs_mount("/mnt/bm2d/rfs88r89/logo.ifs", "/mnt/bm2d/rmp_89_2714881/image.bin", "link", ""); ramfs_demangler_on_fs_mount("/mnt/bm2d/ngp88/logo.ifs", "/mnt/bm2d/rfs88r89/logo.ifs", "imagefs", nullptr); @@ -382,9 +380,8 @@ TEST(ArcArchive, MergedXmlInsideArcWithOriginalInsideArc) { static void exercise_inner_ifs_demangle(std::string const& arc_path) { // The buffer pointer doesn't matter — basename lookup is the fallback path uint8_t fake_buffer[1]; - char* flags = snprintf_auto("base=0x%llx", (unsigned long long)(uintptr_t)fake_buffer); - ramfs_demangler_on_fs_mount("/sd9", "inner.ifs", "ramfs", flags); - free(flags); + std::string flags = std::format("base={:#x}", (uintptr_t)fake_buffer); + ramfs_demangler_on_fs_mount("/sd9", "inner.ifs", "ramfs", flags.c_str()); ramfs_demangler_on_fs_mount("/game/inner_test", "/sd9/inner.ifs", "imagefs", nullptr); std::string p = "/game/inner_test/some_subfile"; diff --git a/src/texbin.cpp b/src/texbin.cpp index 509c7ad..565b434 100644 --- a/src/texbin.cpp +++ b/src/texbin.cpp @@ -50,12 +50,12 @@ class TexbinHdr { void debug() { log_misc("texbin hdr"); - log_misc(" archive size: %" PRId32, archive_size); - log_misc(" file count: %" PRId32, file_count); - log_misc(" data offset: %" PRId32, data_offset); - log_misc(" rect offset: %" PRId32, rect_offset); - log_misc(" name offset: %" PRId32, name_offset); - log_misc(" data entry offset: %" PRId32, data_entry_offset); + log_misc(" archive size: {}", archive_size); + log_misc(" file count: {}", file_count); + log_misc(" data offset: {}", data_offset); + log_misc(" rect offset: {}", rect_offset); + log_misc(" name offset: {}", name_offset); + log_misc(" data entry offset: {}", data_entry_offset); } }; @@ -73,8 +73,8 @@ class TexbinNamesHdr { void debug() { log_misc("texbin names hdr"); - log_misc(" section size: %" PRId32, sect_size); - log_misc(" names count: %" PRId32, names_count); + log_misc(" section size: {}", sect_size); + log_misc(" names count: {}", names_count); } }; @@ -90,10 +90,10 @@ class TexbinRectHdr { void debug() { log_misc("texbin rect hdr"); - log_misc(" section size: %" PRId32, sect_size); - log_misc(" image count: %" PRId32, image_count); - log_misc(" name offset: %" PRId32, name_offset); - log_misc(" entries offset: %" PRId32, rect_entry_offset); + log_misc(" section size: {}", sect_size); + log_misc(" image count: {}", image_count); + log_misc(" name offset: {}", name_offset); + log_misc(" entries offset: {}", rect_entry_offset); } }; @@ -227,7 +227,7 @@ static vector load_names(istream &f, uint32_t name_offset) { TexbinNameEntry entry; for(uint32_t i = 0; i < name_hdr.names_count; i++) { if(!f.read((char*)&entry, sizeof(entry))) { - log_warning("bad name entry at %" PRId32, i); + log_warning("bad name entry at {}", i); return ret; } auto pos = f.tellg(); @@ -239,7 +239,7 @@ static vector load_names(istream &f, uint32_t name_offset) { name += ch; } if(!f) { - log_warning("bad name entry at %" PRId32, i); + log_warning("bad name entry at {}", i); return ret; } ret[entry.id] = name; @@ -261,7 +261,7 @@ static vector> load_data(istream &f, const TexbinHdr& hdr) { TexbinDataEntry entry; for(uint32_t i = 0; i < hdr.file_count; i++) { if(!f.read((char*)&entry, sizeof(entry))) { - log_warning("bad data entry at %" PRId32, i); + log_warning("bad data entry at {}", i); return ret; } auto pos = f.tellg(); @@ -274,9 +274,7 @@ static vector> load_data(istream &f, const TexbinHdr& hdr) { // actual data len, so they're not broken. uint32_t sizes[2]; if(!f.read((char*)&sizes[0], sizeof(sizes))) { - log_warning("can't read data at i %" PRId32 " offset %" PRId32, - i, entry.offset - ); + log_warning("can't read data at i {} offset {}", i, entry.offset); return ret; } @@ -293,9 +291,7 @@ static vector> load_data(istream &f, const TexbinHdr& hdr) { vector data; data.resize(entry.size); if(!f.read((char*)&data[0], entry.size)) { - log_warning("can't read data at i %" PRId32 " offset %" PRId32 " len %" PRId32, - i, entry.offset, entry.size - ); + log_warning("can't read data at i {} offset {} len {}", i, entry.offset, entry.size); return ret; } ret.push_back(data); @@ -388,7 +384,7 @@ bool Texbin::add_or_replace_image(const char *image_name, const char *png_path) unsigned width, height; error = lodepng::decode(image, width, height, png_path); if (error) { - log_warning("Can't load png %u: %s\n", error, lodepng_error_text(error)); + log_warning("Can't load png {}: {}", error, lodepng_error_text(error)); return false; } @@ -397,25 +393,25 @@ bool Texbin::add_or_replace_image(const char *image_name, const char *png_path) // rect image names may shadow normal image names, so check them first if(existing_rect != rects.end()) { if(width != existing_rect->second.w || height != existing_rect->second.h) { - log_info("Replacement rect image %s has dimensions %dx%d but original is %dx%d, ignoring", + log_info("Replacement rect image {} has dimensions {}x{} but original is {}x{}, ignoring", image_name, width, height, existing_rect->second.w, existing_rect->second.h ); return false; } - log_info("Replacing rect image %s", image_name); + log_info("Replacing rect image {}", image_name); existing_rect->second.dirty_data = image; } else if(existing_image != images.end()) { auto [w, h] = existing_image->second.peek_dimensions(); if(width != w || height != h) { - log_info("Replacement image %s has dimensions %dx%d but original is %dx%d, repacking anyway", + log_info("Replacement image {} has dimensions {}x{} but original is {}x{}, repacking anyway", image_name, width, height, w, h ); } - log_info("Replacing %s", image_name); + log_info("Replacing {}", image_name); images[image_name] = ImageEntryParsed(argb8888_to_texture_data(&image[0], width, height)); } else{ - log_info("Adding new image %s", image_name); + log_info("Adding new image {}", image_name); images[image_name] = ImageEntryParsed(argb8888_to_texture_data(&image[0], width, height)); } @@ -459,7 +455,11 @@ optional Texbin::from_stream(istream &f) { } if(hdr.archive_size != file_len) { - log_warning("bad archive size (file said %d stream said %d)", hdr.archive_size, file_len); + log_warning("bad archive size (file said {} stream said {})", + hdr.archive_size, + // TODO: hope that P3374R1 is implemented for C++26? + static_cast(file_len) + ); return nullopt; } @@ -520,7 +520,7 @@ optional Texbin::from_stream(istream &f) { } if(entry.x1 >= entry.x2 || entry.y1 >= entry.y2) { - log_warning("rect entry has invalid dimensions (%d,%d,%d,%d)", + log_warning("rect entry has invalid dimensions ({},{},{},{})", entry.x1, entry.x2, entry.y1, entry.y2 ); return nullopt; @@ -546,7 +546,7 @@ optional Texbin::from_stream(istream &f) { optional Texbin::from_path(const char *path) { // there are a handful of .bin files we might try to parse that *aren't* // texbins, so gate all logs before header magic check behind log_verbose - log_verbose("Opening %s", path); + log_verbose("Opening {}", path); ifstream f (path, ios::binary); if(!f) { log_verbose("cannot open"); @@ -568,21 +568,21 @@ void Texbin::process_dirty_rects() { for(auto &[rect_name, rects] : updates) { auto _image = images.find(rect_name); if(_image == images.end()) { - log_warning("Can't update rect %s: no tex???", rect_name.c_str()); + log_warning("Can't update rect {}: no tex???", rect_name); continue; } auto image = &_image->second; auto _tex = image->tex_to_argb8888(); if(!_tex) { - log_warning("Can't update rect %s: cannot load tex", rect_name.c_str()); + log_warning("Can't update rect {}: cannot load tex", rect_name); continue; } auto [tex, width, height] = *_tex; for(auto &rect : rects) { if(rect->x2() > width || rect->y2() > height) { - log_warning("Can't update rect in %s: out of bounds (canvas is %dx%d, rect is x1,x2,y1,y2 %d,%d,%d,%d)", - rect_name.c_str(), + log_warning("Can't update rect in {}: out of bounds (canvas is {}x{}, rect is x1,x2,y1,y2 {},{},{},{})", + rect_name, width, height, rect->x, rect->x2(), rect->y, rect->y2() @@ -657,8 +657,8 @@ bool Texbin::save(const char *dest) { for(auto &[name, rect] : rects) { auto parent = images.find(rect.parent_name); if(parent == images.end()) { - log_warning("Rect entry \"%s\" has an invalid parent name \"%s\"", - name.c_str(), rect.parent_name.c_str() + log_warning("Rect entry \"{}\" has an invalid parent name \"{}\"", + name, rect.parent_name ); return false; } @@ -891,7 +891,7 @@ optional, uint16_t, uint16_t>> ImageEntryParsed::tex_to_ar break; default: - log_warning("Unsupported tex format type 0x%X", hdr->format1 & 0xFF); + log_warning("Unsupported tex format type {:#x}", hdr->format1 & 0xFF); return nullopt; } diff --git a/src/utils.cpp b/src/utils.cpp index b49e9b7..22d30a6 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -4,21 +4,9 @@ #include #include "utils.hpp" -#include "log.hpp" #include "avs.h" #include "hook.h" -char* snprintf_auto(const char* fmt, ...) { - va_list argList; - - va_start(argList, fmt); - size_t len = vsnprintf(NULL, 0, fmt, argList); - auto s = (char*)malloc(len + 1); - vsnprintf(s, len + 1, fmt, argList); - va_end(argList); - return s; -} - bool string_ends_with(const char * str, const char * suffix) { size_t str_len = strlen(str); size_t suffix_len = strlen(suffix); @@ -227,7 +215,7 @@ uint64_t file_time(const char* path) { ULARGE_INTEGER result; result.LowPart = mtime.dwLowDateTime; result.HighPart = mtime.dwHighDateTime; - // log_verbose("file time %lu for %s", result.QuadPart, path); + // log_verbose("file time {} for {}", result.QuadPart, path); return result.QuadPart; // NOTE: can't use this method because the DLL time is taken before AVS is @@ -236,7 +224,7 @@ uint64_t file_time(const char* path) { // struct avs_stat st; // auto res = avs_fs_lstat(path, &st); // if (res) { - // log_verbose("file time %ld for %s", st.st_mtime, path); + // log_verbose("file time {} for {}", st.st_mtime, path); // return st.st_mtime; // } else { // return 0; diff --git a/src/utils.hpp b/src/utils.hpp index 919beaf..bb0cb53 100644 --- a/src/utils.hpp +++ b/src/utils.hpp @@ -13,7 +13,6 @@ #define lenof(x) (sizeof(x) / sizeof(*x)) -char* snprintf_auto(const char* fmt, ...); bool string_ends_with(const char * str, const char * suffix); bool string_ends_with(const std::string &str, const char * suffix); // case insensitive