From c3f1e6562b6cb5aefcb9edd490b303867917f25e Mon Sep 17 00:00:00 2001 From: Feng Chen Date: Wed, 16 Sep 2026 22:12:01 +0200 Subject: [PATCH] [common/fs] Preserve Windows roots during path sanitization (#4437) Commit 5b4c29b123 made SanitizePath rebuild paths from split components after resolving traversal. That reconstruction retained only one leading separator, collapsing UNC paths and breaking network-backed directories. Parse the path root separately from normal components. Preserve UNC prefixes, drive-absolute and drive-relative forms, and extended UNC/device namespaces while normalizing separators and dot components. Treat the UNC server/share portion as part of the root so parent traversal cannot escape into another share. Also preserve root-only results such as / and C:\\ instead of stripping their final separator. - [x] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions). - [x] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md) - [x] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability. ------------------- Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4437 Reviewed-by: lizzie Reviewed-by: CamilleLaVey --- src/common/fs/path_util.cpp | 77 +++++++++++++++++++++++++++++++------ src/common/fs/path_util.h | 5 ++- 2 files changed, 68 insertions(+), 14 deletions(-) diff --git a/src/common/fs/path_util.cpp b/src/common/fs/path_util.cpp index b895aac54c..82ea8f6cb2 100644 --- a/src/common/fs/path_util.cpp +++ b/src/common/fs/path_util.cpp @@ -5,6 +5,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include #include #include #include "common/container/unordered_map.h" @@ -480,28 +481,80 @@ std::string SanitizePath(std::string_view path_, DirectorySeparator directory_se [type2](char c1, char c2) { return c1 == type2 && c2 == type2; }), path.end()); - const bool absolute = !path.empty() && path[0] == type2; - std::vector parts; + std::string root; + std::string_view components{path}; + bool drive_relative = false; + +#ifdef _WIN32 + const bool network = path.size() > 1 && path[0] == type2 && path[1] == type2; + const bool drive = + path.size() > 1 && std::isalpha(static_cast(path[0])) && path[1] == ':'; + + if (network) { + root.assign(2, type2); + components.remove_prefix(2); + } else if (drive) { + root.assign(path.data(), 2); + components.remove_prefix(2); + if (!components.empty() && components.front() == type2) { + root += type2; + components.remove_prefix(1); + } else { + drive_relative = true; + } + } +#endif + + if (root.empty() && !components.empty() && components.front() == type2) { + root += type2; + components.remove_prefix(1); + } + + const auto path_parts = SplitPathComponents(components); + std::size_t root_component_count = 0; +#ifdef _WIN32 + if (network) { + root_component_count = 2; + + const auto is_unc = [](std::string_view part) { + return part.size() == 3 && (part[0] == 'U' || part[0] == 'u') && + (part[1] == 'N' || part[1] == 'n') && (part[2] == 'C' || part[2] == 'c'); + }; + if (path_parts.size() >= 2 && path_parts[0] == "?" && is_unc(path_parts[1])) { + root_component_count = 4; + } + } +#endif - for (const auto part : SplitPathComponents(path)) - { - if (part.empty() || part == ".") + std::vector parts; + for (std::size_t i = 0; i < path_parts.size(); ++i) { + const auto part = path_parts[i]; + if (i < root_component_count) { + parts.push_back(part); + } else if (part.empty() || part == ".") { continue; - if (part == ".." && !parts.empty() && parts.back() != "..") - parts.pop_back(); - else if (part != "..") parts.push_back(part); + } else if (part == "..") { + if (parts.size() > root_component_count) { + parts.pop_back(); + } + } else { + parts.push_back(part); + } } - std::string resolved = absolute ? std::string(1, type2) : std::string{}; - for (std::size_t i = 0; i < parts.size(); ++i) - { - if (i != 0) + const std::size_t root_length = root.size(); + std::string resolved = std::move(root); + for (std::size_t i = 0; i < parts.size(); ++i) { + if (i != 0 || (!resolved.empty() && resolved.back() != type2 && !drive_relative)) resolved += type2; resolved.append(parts[i].data(), parts[i].size()); } path = std::move(resolved); + if (!path.empty() && path.size() == root_length) { + return path; + } return std::string(RemoveTrailingSlash(path)); } diff --git a/src/common/fs/path_util.h b/src/common/fs/path_util.h index 983e4917e8..e8fa5614ae 100644 --- a/src/common/fs/path_util.h +++ b/src/common/fs/path_util.h @@ -347,8 +347,9 @@ enum class DirectorySeparator { // i.e. "C:\Users\Yuzu\Documents\save.bin" becomes {"C:", "Users", "Yuzu", "Documents", "save.bin" } [[nodiscard]] std::vector SplitPathComponentsCopy(std::string_view filename); -// Removes trailing slash, makes all '\\' into '/', and removes duplicate '/'. Makes '/' into '\\' -// depending if directory_separator is BackwardSlash or PlatformDefault and running on windows +// Normalizes directory separators, removes duplicate and non-root trailing separators, and resolves +// '.' and '..' components without traversing above the path root. Windows drive and UNC roots are +// preserved. [[nodiscard]] std::string SanitizePath( std::string_view path, DirectorySeparator directory_separator = DirectorySeparator::ForwardSlash);