From bbd69496b4d4d64c7135cdb2b7adba4d3db5b000 Mon Sep 17 00:00:00 2001 From: Tom Lankhorst Date: Sun, 3 Feb 2019 22:59:28 +0100 Subject: [PATCH] Sanitize screenshot path --- distribution/changelog.txt | 1 + src/openrct2/interface/Screenshot.cpp | 152 ++++++++++++++------------ src/openrct2/platform/Shared.cpp | 19 ++++ src/openrct2/platform/Windows.cpp | 18 +++ src/openrct2/platform/platform.h | 1 + 5 files changed, 119 insertions(+), 72 deletions(-) diff --git a/distribution/changelog.txt b/distribution/changelog.txt index 42da6c34a4..7cbd17abbe 100644 --- a/distribution/changelog.txt +++ b/distribution/changelog.txt @@ -116,6 +116,7 @@ - Fix: [#8588] Guest list scrolling breaks above ~2000 guests. - Fix: [#8591] Game loop does not run at a consistent tick rate of 40 Hz. - Fix: [#8647] Marketing campaigns check for entry fees below £1 (original bug). +- Fix: [#8598] Sanitize screenshot parknames. - Fix: [#8653] Crash when peeps attempt to enter a ride with no vehicles. - Fix: [#8720] Desync due to boats colliding with ghost pieces. - Fix: [#8736] Incomplete warning when all ride slots are full. diff --git a/src/openrct2/interface/Screenshot.cpp b/src/openrct2/interface/Screenshot.cpp index b64b5989bb..18b49c75d1 100644 --- a/src/openrct2/interface/Screenshot.cpp +++ b/src/openrct2/interface/Screenshot.cpp @@ -17,6 +17,7 @@ #include "../audio/audio.h" #include "../core/Console.hpp" #include "../core/Imaging.h" +#include "../core/Optional.hpp" #include "../drawing/Drawing.h" #include "../drawing/X8DrawingEngine.h" #include "../localisation/Localisation.h" @@ -28,10 +29,13 @@ #include "../world/Surface.h" #include "Viewport.h" +#include #include #include #include +#include +using namespace std::literals::string_literals; using namespace OpenRCT2; using namespace OpenRCT2::Drawing; @@ -96,88 +100,93 @@ static void screenshot_get_rendered_palette(rct_palette* palette) } } -static int32_t screenshot_get_next_path(char* path, size_t size) +static std::string screenshot_get_park_name() +{ + char buffer[512]; + format_string(buffer, sizeof(buffer), gParkName, &gParkNameArgs); + return buffer; +} + +static opt::optional screenshot_get_directory() { char screenshotPath[MAX_PATH]; - platform_get_user_directory(screenshotPath, "screenshot", sizeof(screenshotPath)); - if (!platform_ensure_directory_exists(screenshotPath)) + + if (platform_ensure_directory_exists(screenshotPath)) { - log_error("Unable to save screenshots in OpenRCT2 screenshot directory.\n"); - return -1; + return opt::make_optional(screenshotPath); } - char park_name[128]; - format_string(park_name, 128, gParkName, &gParkNameArgs); + log_error("Unable to save screenshots in OpenRCT2 screenshot directory.\n"); + return opt::nullopt; +} - // Retrieve current time - rct2_date currentDate; - platform_get_date_local(¤tDate); - rct2_time currentTime; - platform_get_time_local(¤tTime); +static std::tuple screenshot_get_date_time() +{ + rct2_date date; + platform_get_date_local(&date); -#ifdef _WIN32 - // On NTFS filesystems, a colon (:) in a path - // indicates you want to write a file stream - // (hidden metadata). This will pass the - // file_exists and fopen checks, since it is - // technically valid. We don't want that, so - // replace colons with hyphens in the park name. - char* foundColon = park_name; - while ((foundColon = strchr(foundColon, ':')) != nullptr) - { - *foundColon = '-'; - } -#endif + rct2_time time; + platform_get_time_local(&time); - // Glue together path and filename - safe_strcpy(path, screenshotPath, size); - path_end_with_separator(path, size); - auto fileNameCh = strchr(path, '\0'); - if (fileNameCh == nullptr) - { - log_error("Unable to generate a screenshot filename."); - return -1; - } - const size_t leftBytes = size - strlen(path); + return { date, time }; +} + +static std::string screenshot_get_formatted_date_time() +{ + auto [date, time] = screenshot_get_date_time(); + char formatted[64]; snprintf( - fileNameCh, leftBytes, "%s %d-%02d-%02d %02d-%02d-%02d.png", park_name, currentDate.year, currentDate.month, - currentDate.day, currentTime.hour, currentTime.minute, currentTime.second); + formatted, sizeof(formatted), "%4d-%02d-%02d %02d-%02d-%02d", date.year, date.month, date.day, time.hour, time.minute, + time.second); + return formatted; +} - if (!platform_file_exists(path)) +static opt::optional screenshot_get_next_path() +{ + std::string dir, name, suffix = ".png", path; + + auto screenshotDirectory = screenshot_get_directory(); + + if (screenshotDirectory == opt::nullopt) { - return 0; // path ok + return opt::nullopt; } - // multiple screenshots with same timestamp - // might be possible when switching timezones - // in the unlikely case that this does happen, - // append (%d) to the filename and increment - // this int32_t until it doesn't overwrite any - // other file in the directory. - int32_t i; - for (i = 1; i < 1000; i++) - { - // Glue together path and filename - snprintf( - fileNameCh, leftBytes, "%s %d-%02d-%02d %02d-%02d-%02d (%d).png", park_name, currentDate.year, currentDate.month, - currentDate.day, currentTime.hour, currentTime.minute, currentTime.second, i); + auto parkName = screenshot_get_park_name(); + auto dateTime = screenshot_get_formatted_date_time(); - if (!platform_file_exists(path)) - { - return i; - } + dir = *screenshotDirectory; + name = parkName + " " + dateTime; + + // Generate a path with a `tries` number + auto path_composer = [&dir, &name, &suffix ](int tries) -> auto + { + auto composed_filename = platform_sanitise_filename( + name + ((tries > 0) ? " ("s + std::to_string(tries) + ")" : ""s) + suffix); + return dir + PATH_SEPARATOR + composed_filename; + }; + + for (int tries = 0; tries < 100; tries++) + { + path = path_composer(tries); + if (platform_file_exists(path.c_str())) + continue; + + return path; } log_error("You have too many saved screenshots saved at exactly the same date and time.\n"); - return -1; -} + + return opt::nullopt; +}; std::string screenshot_dump_png(rct_drawpixelinfo* dpi) { // Get a free screenshot path - char path[MAX_PATH] = ""; - if (screenshot_get_next_path(path, MAX_PATH) == -1) + auto path = screenshot_get_next_path(); + + if (path == opt::nullopt) { return ""; } @@ -185,9 +194,9 @@ std::string screenshot_dump_png(rct_drawpixelinfo* dpi) rct_palette renderedPalette; screenshot_get_rendered_palette(&renderedPalette); - if (WriteDpiToFile(path, dpi, renderedPalette)) + if (WriteDpiToFile(path->c_str(), dpi, renderedPalette)) { - return std::string(path); + return *path; } else { @@ -197,9 +206,9 @@ std::string screenshot_dump_png(rct_drawpixelinfo* dpi) std::string screenshot_dump_png_32bpp(int32_t width, int32_t height, const void* pixels) { - // Get a free screenshot path - char path[MAX_PATH] = ""; - if (screenshot_get_next_path(path, MAX_PATH) == -1) + auto path = screenshot_get_next_path(); + + if (path == opt::nullopt) { return ""; } @@ -215,8 +224,8 @@ std::string screenshot_dump_png_32bpp(int32_t width, int32_t height, const void* image.Depth = 32; image.Stride = width * 4; image.Pixels = std::vector(pixels8, pixels8 + pixelsLen); - Imaging::WriteToFile(path, image, IMAGE_FORMAT::PNG_32); - return std::string(path); + Imaging::WriteToFile(path->c_str(), image, IMAGE_FORMAT::PNG_32); + return *path; } catch (const std::exception& e) { @@ -301,9 +310,8 @@ void screenshot_giant() viewport_render(&dpi, &viewport, 0, 0, viewport.width, viewport.height); - // Get a free screenshot path - char path[MAX_PATH]; - if (screenshot_get_next_path(path, MAX_PATH) == -1) + auto path = screenshot_get_next_path(); + if (path == opt::nullopt) { log_error("Giant screenshot failed, unable to find a suitable destination path."); context_show_error(STR_SCREENSHOT_FAILED, STR_NONE); @@ -313,13 +321,13 @@ void screenshot_giant() rct_palette renderedPalette; screenshot_get_rendered_palette(&renderedPalette); - WriteDpiToFile(path, &dpi, renderedPalette); + WriteDpiToFile(path->c_str(), &dpi, renderedPalette); free(dpi.bits); // Show user that screenshot saved successfully set_format_arg(0, rct_string_id, STR_STRING); - set_format_arg(2, char*, path_get_filename(path)); + set_format_arg(2, char*, path_get_filename(path->c_str())); context_show_error(STR_SCREENSHOT_SAVED_AS, STR_NONE); } diff --git a/src/openrct2/platform/Shared.cpp b/src/openrct2/platform/Shared.cpp index 6cc2ec2688..050cb2ac13 100644 --- a/src/openrct2/platform/Shared.cpp +++ b/src/openrct2/platform/Shared.cpp @@ -28,6 +28,7 @@ #include "../world/Climate.h" #include "platform.h" +#include #include #include @@ -198,6 +199,24 @@ uint8_t platform_get_currency_value(const char* currCode) return CURRENCY_POUNDS; } +#ifndef _WIN32 +std::string platform_sanitise_filename(const std::string& path) +{ + auto sanitised = path; + + std::vector prohibited = { '/' }; + + std::replace_if( + sanitised.begin(), sanitised.end(), + [&prohibited](const std::string::value_type& ch) { + return std::find(prohibited.begin(), prohibited.end(), ch) != prohibited.end(); + }, + '_'); + + return sanitised; +} +#endif + #ifndef __ANDROID__ float platform_get_default_scale() { diff --git a/src/openrct2/platform/Windows.cpp b/src/openrct2/platform/Windows.cpp index 57c1ca7f64..029014dbf4 100644 --- a/src/openrct2/platform/Windows.cpp +++ b/src/openrct2/platform/Windows.cpp @@ -28,8 +28,10 @@ # include "../util/Util.h" # include "platform.h" +# include # include # include +# include # include # include # include @@ -253,6 +255,22 @@ std::string platform_get_rct2_steam_dir() return "Rollercoaster Tycoon 2"; } +std::string platform_sanitise_filename(const std::string& path) +{ + auto sanitised = path; + + std::vector prohibited = { '<', '>', '*', '\\', ':', '|', '?', '"', '/' }; + + std::replace_if( + sanitised.begin(), sanitised.end(), + [](const std::string::value_type& ch) { + return std::find(prohibited.begin(), prohibited.end(), ch) != prohibited.end(); + }, + '_'); + + return sanitised; +} + uint16_t platform_get_locale_language() { CHAR langCode[4]; diff --git a/src/openrct2/platform/platform.h b/src/openrct2/platform/platform.h index 7e2ec421fd..bad0ad63eb 100644 --- a/src/openrct2/platform/platform.h +++ b/src/openrct2/platform/platform.h @@ -126,6 +126,7 @@ bool platform_process_is_elevated(); bool platform_get_steam_path(utf8* outPath, size_t outSize); std::string platform_get_rct1_steam_dir(); std::string platform_get_rct2_steam_dir(); +std::string platform_sanitise_filename(const std::string&); #ifndef NO_TTF bool platform_get_font_path(TTFFontDescriptor* font, utf8* buffer, size_t size);