From b3a57860811c8022518e13af7e6b9ebe50243e87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Pra=C5=BAmo?= Date: Mon, 2 Feb 2026 22:26:41 +0100 Subject: [PATCH 1/4] Add path pre-verification before saving Provides a meaningful error dialog to the user if something is wrong with the filename. Also anticipates any unexpected behaviors that may occur if feeding such unverified paths to fopen()/_wfopen() (like e.g. allowing filenames with ':' on Windows that ends up writing to an alternate NTFS stream which is confusing to the user and useless for LibreSprite use-case in general). --- src/app/ui/file_selector.cpp | 21 ++++++++++++++++++ src/base/path.cpp | 41 ++++++++++++++++++++++++++++++++++-- src/base/path.h | 9 ++++++-- 3 files changed, 67 insertions(+), 4 deletions(-) diff --git a/src/app/ui/file_selector.cpp b/src/app/ui/file_selector.cpp index 9c5b0c08b..699166465 100644 --- a/src/app/ui/file_selector.cpp +++ b/src/app/ui/file_selector.cpp @@ -547,6 +547,16 @@ std::string FileSelector::show( } // else file-name specified in the entry is really a file to open... + std::string finalFilename = base::get_file_name(buf); + try { + base::verify_filename(finalFilename); + } catch (const std::exception& e) { + Alert::show("Error<text(); + try { + // Also disallows the use of path separators in the folder name, + // technically it would be valid, but might be confusing for the user. + base::verify_filename(dirname); + } catch (const std::exception& e) { + Alert::show("Error<createDirectory(dirname); diff --git a/src/base/path.cpp b/src/base/path.cpp index 319e96bb2..7c99e559d 100644 --- a/src/base/path.cpp +++ b/src/base/path.cpp @@ -1,5 +1,6 @@ -// Aseprite Base Library -// Copyright (c) 2001-2016 David Capello +// Base Library +// Aseprite | Copyright (C) 2001-2016 David Capello +// LibreSprite | Copyright (C) 2026 LibreSprite contributors // // This file is released under the terms of the MIT license. // Read LICENSE.txt for more information. @@ -243,4 +244,40 @@ int compare_filenames(const std::string& a, const std::string& b) return 1; } +std::string win32_verify_filename(const std::string& filename) +{ + // In general, _wfopen() would fail for most of these characters *except slashes and colon*, + // but returning a meaningful error message to the user is always a nice practice. + const std::string invalidChars = "\\/:?\"<>|*"; + + for (const char c : filename) { + if (invalidChars.find(c) != std::string::npos) { + return "The filename contains an invalid '" + + std::string(1,c) + "' character."; + } + } + return {}; +} + +std::string posix_verify_filename(const std::string& filename) +{ + return filename.find('/') != std::string::npos + ? "The filename contains an invalid '/' character." + : std::string{}; +} + +void verify_filename(const std::string& filename) +{ + std::string err; + // In general, filenames with path separators would be appended just fine to the selected folder, + // but we'd rather not let user do that to avoid confusion. +#ifdef _WIN32 + err = win32_verify_filename(const_cast(filename)); +#else + err = posix_verify_filename(const_cast(filename)); +#endif + if (!err.empty()) + throw std::runtime_error(err); +} + } // namespace base diff --git a/src/base/path.h b/src/base/path.h index 8af3ba0a9..e09f08618 100644 --- a/src/base/path.h +++ b/src/base/path.h @@ -1,5 +1,6 @@ -// Aseprite Base Library -// Copyright (c) 2001-2016 David Capello +// Base Library +// Aseprite | Copyright (C) 2001-2016 David Capello +// LibreSprite | Copyright (C) 2026 LibreSprite contributors // // This file is released under the terms of the MIT license. // Read LICENSE.txt for more information. @@ -52,4 +53,8 @@ namespace base { int compare_filenames(const std::string& a, const std::string& b); + // Does platform-specific filename pre-validation to avoid any unexpected edge-case + // behaviors caused by feeding garbage parameters to file APIs. + void verify_filename(const std::string& filename); + } From 766291326a1cbcbb7b1b4758a9bdd6e65da23380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Pra=C5=BAmo?= Date: Wed, 4 Feb 2026 21:14:37 +0100 Subject: [PATCH 2/4] Make the filename validation perform only on Save operation --- src/app/ui/file_selector.cpp | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/app/ui/file_selector.cpp b/src/app/ui/file_selector.cpp index 699166465..379edc35f 100644 --- a/src/app/ui/file_selector.cpp +++ b/src/app/ui/file_selector.cpp @@ -547,14 +547,17 @@ std::string FileSelector::show( } // else file-name specified in the entry is really a file to open... - std::string finalFilename = base::get_file_name(buf); - try { - base::verify_filename(finalFilename); - } catch (const std::exception& e) { - Alert::show("Error<text(); - try { - // Also disallows the use of path separators in the folder name, - // technically it would be valid, but might be confusing for the user. - base::verify_filename(dirname); - } catch (const std::exception& e) { - Alert::show("Error< Date: Wed, 4 Feb 2026 21:37:23 +0100 Subject: [PATCH 3/4] Remove the path separator validation (I came to the conclusion it doesn't really make sense to forcefully disallow user from doing it) --- src/app/ui/file_selector.cpp | 25 +++++++++++++------------ src/base/path.cpp | 23 ++--------------------- src/base/path.h | 4 +++- 3 files changed, 18 insertions(+), 34 deletions(-) diff --git a/src/app/ui/file_selector.cpp b/src/app/ui/file_selector.cpp index 379edc35f..8b7d46c2c 100644 --- a/src/app/ui/file_selector.cpp +++ b/src/app/ui/file_selector.cpp @@ -547,18 +547,19 @@ std::string FileSelector::show( } // else file-name specified in the entry is really a file to open... - if (m_type == FileSelectorType::Save) - { +#ifdef _WIN32 + if (m_type == FileSelectorType::Save) { std::string finalFilename = base::get_file_name(buf); - try { - base::verify_filename(finalFilename); - } catch (const std::exception& e) { - Alert::show("Error<text(); +#ifdef _WIN32 if (m_type == FileSelectorType::Save) { - try { - // Also disallows the use of path separators in the folder name, - // technically it would be valid, but might be confusing for the user. - base::verify_filename(dirname); - } catch (const std::exception& e) { - Alert::show("Error<|*"; + const std::string invalidChars = ":?\"<>|*"; for (const char c : filename) { if (invalidChars.find(c) != std::string::npos) { @@ -258,26 +259,6 @@ std::string win32_verify_filename(const std::string& filename) } return {}; } - -std::string posix_verify_filename(const std::string& filename) -{ - return filename.find('/') != std::string::npos - ? "The filename contains an invalid '/' character." - : std::string{}; -} - -void verify_filename(const std::string& filename) -{ - std::string err; - // In general, filenames with path separators would be appended just fine to the selected folder, - // but we'd rather not let user do that to avoid confusion. -#ifdef _WIN32 - err = win32_verify_filename(const_cast(filename)); -#else - err = posix_verify_filename(const_cast(filename)); #endif - if (!err.empty()) - throw std::runtime_error(err); -} } // namespace base diff --git a/src/base/path.h b/src/base/path.h index e09f08618..318c608ff 100644 --- a/src/base/path.h +++ b/src/base/path.h @@ -53,8 +53,10 @@ namespace base { int compare_filenames(const std::string& a, const std::string& b); +#ifdef _WIN32 // Does platform-specific filename pre-validation to avoid any unexpected edge-case // behaviors caused by feeding garbage parameters to file APIs. - void verify_filename(const std::string& filename); + std::string win32_verify_filename(const std::string& filename); +#endif } From a2daa65f69883bb8e19c5f26736bab6ff6a2bb9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Pra=C5=BAmo?= Date: Sat, 7 Feb 2026 17:48:01 +0100 Subject: [PATCH 4/4] Change verifier function return type for better integration with internationalization. --- src/app/ui/file_selector.cpp | 16 ++++++---------- src/base/path.cpp | 15 +++++++-------- src/base/path.h | 10 ++++------ 3 files changed, 17 insertions(+), 24 deletions(-) diff --git a/src/app/ui/file_selector.cpp b/src/app/ui/file_selector.cpp index 8b7d46c2c..9992d4e1b 100644 --- a/src/app/ui/file_selector.cpp +++ b/src/app/ui/file_selector.cpp @@ -547,19 +547,17 @@ std::string FileSelector::show( } // else file-name specified in the entry is really a file to open... -#ifdef _WIN32 if (m_type == FileSelectorType::Save) { std::string finalFilename = base::get_file_name(buf); - std::string fver = base::win32_verify_filename(finalFilename); - if (!fver.empty()) + if (const size_t fver = base::verify_filename(finalFilename); fver != std::string::npos) { - Alert::show("Error<text(); -#ifdef _WIN32 if (m_type == FileSelectorType::Save) { - std::string fver = base::win32_verify_filename(dirname); - if (!fver.empty()) + if (const size_t fver = base::verify_filename(dirname); fver != std::string::npos) { - Alert::show("Error<|*"; - for (const char c : filename) { - if (invalidChars.find(c) != std::string::npos) { - return "The filename contains an invalid '" - + std::string(1,c) + "' character."; + for (size_t it=0; it