aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFeng Chen <vonchenplus@gmail.com>2026-09-16 22:12:01 +0200
committercrueter <crueter@eden-emu.dev>2026-09-16 22:12:01 +0200
commitc3f1e6562b6cb5aefcb9edd490b303867917f25e (patch)
tree84c5d574b2f169aed05a9f5c79bbb6650cc312e0 /src
parentac35358b3ffed74aaac56b0e99b09e81bb60d2e1 (diff)
[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 <lizzie@eden-emu.dev> Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Diffstat (limited to 'src')
-rw-r--r--src/common/fs/path_util.cpp77
-rw-r--r--src/common/fs/path_util.h5
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 <algorithm>
+#include <cctype>
#include <iostream>
#include <sstream>
#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<std::string_view> 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<unsigned char>(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<std::string_view> 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<std::string> 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);