From 5875ed880a07a1377090e645595944d8c4352bce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Tue, 9 Jun 2026 19:51:15 +0000 Subject: [PATCH] Fix Malformed Preset Filenames (#6712) Sanitize preset names for filesystem safety, log errors without crashing Co-authored-by: Unreference <87878910+unreference@users.noreply.github.com> --- soh/soh/Enhancements/Presets/Presets.cpp | 59 +++++++++++++++++++----- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/soh/soh/Enhancements/Presets/Presets.cpp b/soh/soh/Enhancements/Presets/Presets.cpp index ea241d302d..8701a41764 100644 --- a/soh/soh/Enhancements/Presets/Presets.cpp +++ b/soh/soh/Enhancements/Presets/Presets.cpp @@ -14,11 +14,37 @@ #include "soh/Enhancements/randomizer/randomizer_check_tracker.h" #include "soh/Enhancements/randomizer/randomizer_entrance_tracker.h" #include "soh/Enhancements/randomizer/randomizer_item_tracker.h" -#include "soh/Enhancements/randomizer/SeedContext.h" #include "soh/Enhancements/randomizer/settings.h" namespace fs = std::filesystem; +/** + * Replace characters to prevent crashes from invalid paths (e.g, "test :)" creating an NTFS Alternate Data Stream + * instead of a regular file). + */ +static std::string SanitizeFilename(const std::string& name) { + std::string result; + result.reserve(name.size()); + for (const char c : name) { + if (c == '<' || c == '>' || c == ':' || c == '"' || c == '/' || c == '\\' || c == '|' || c == '?' || c == '*' || + c < 32) { + result += '_'; + } else { + result += c; + } + } + + while (!result.empty() && (result.back() == '.' || result.back() == ' ')) { + result.pop_back(); + } + + if (result.empty()) { + result = "Unnamed"; + } + + return result; +} + namespace SohGui { extern std::shared_ptr mSohMenu; } // namespace SohGui @@ -75,7 +101,7 @@ static BlockInfo blockInfo[PRESET_SECTION_MAX] = { }; std::string FormatPresetPath(std::string name) { - return fmt::format("{}/{}.json", presetFolder, name); + return fmt::format("{}/{}.json", presetFolder, SanitizeFilename(name)); } void applyPreset(std::string presetName, std::vector includeSections) { @@ -220,16 +246,19 @@ void LoadPresets() { } if (fs::exists(presetFolder)) { for (auto const& preset : fs::directory_iterator(presetFolder)) { - std::ifstream ifs(preset.path()); + try { + std::ifstream ifs(preset.path()); + if (auto json = nlohmann::json::parse(ifs); !json.contains("presetName")) { + spdlog::error(fmt::format("Attempted to load file {} as a preset, but was not a preset file.", + preset.path().filename().string())); + } else { + ParsePreset(json, preset.path().filename().stem().string()); + } - auto json = nlohmann::json::parse(ifs); - if (!json.contains("presetName")) { - spdlog::error(fmt::format("Attempted to load file {} as a preset, but was not a preset file.", - preset.path().filename().string())); - } else { - ParsePreset(json, preset.path().filename().stem().string()); + ifs.close(); + } catch (const std::exception& e) { + spdlog::error("Failed to load preset {}: {}", preset.path().filename().string(), e.what()); } - ifs.close(); } } auto initData = std::make_shared(); @@ -255,8 +284,16 @@ void SavePreset(std::string& presetName) { } presets[presetName].presetValues["presetName"] = presetName; presets[presetName].presetValues["fileType"] = FILE_TYPE_PRESET; + + std::string safeFilename = SanitizeFilename(presetName); std::ofstream file( - fmt::format("{}/{}.json", Ship::Context::GetRawInstance()->LocateFileAcrossAppDirs("presets"), presetName)); + fmt::format("{}/{}.json", Ship::Context::GetRawInstance()->LocateFileAcrossAppDirs("presets"), safeFilename)); + + if (!file.is_open()) { + spdlog::error("Failed to save preset '{}': Could not create file", presetName); + return; + } + file << presets[presetName].presetValues.dump(4); file.close(); LoadPresets();