diff --git a/core/string/ustring.cpp b/core/string/ustring.cpp
index 854d32af967..e8c1ea42edd 100644
--- a/core/string/ustring.cpp
+++ b/core/string/ustring.cpp
@@ -4707,6 +4707,29 @@ String String::uri_decode() const {
return String::utf8(res);
}
+String String::uri_file_decode() const {
+ CharString src = utf8();
+ CharString res;
+ for (int i = 0; i < src.length(); ++i) {
+ if (src[i] == '%' && i + 2 < src.length()) {
+ char ord1 = src[i + 1];
+ if (is_digit(ord1) || is_ascii_upper_case(ord1)) {
+ char ord2 = src[i + 2];
+ if (is_digit(ord2) || is_ascii_upper_case(ord2)) {
+ char bytes[3] = { (char)ord1, (char)ord2, 0 };
+ res += (char)strtol(bytes, nullptr, 16);
+ i += 2;
+ }
+ } else {
+ res += src[i];
+ }
+ } else {
+ res += src[i];
+ }
+ }
+ return String::utf8(res);
+}
+
String String::c_unescape() const {
String escaped = *this;
escaped = escaped.replace("\\a", "\a");
diff --git a/core/string/ustring.h b/core/string/ustring.h
index e828456df12..75c12ff6650 100644
--- a/core/string/ustring.h
+++ b/core/string/ustring.h
@@ -572,6 +572,7 @@ public:
String xml_unescape() const;
String uri_encode() const;
String uri_decode() const;
+ String uri_file_decode() const;
String c_escape() const;
String c_escape_multiline() const;
String c_unescape() const;
diff --git a/core/variant/variant_call.cpp b/core/variant/variant_call.cpp
index 212091dec38..a6a68f5ed6a 100644
--- a/core/variant/variant_call.cpp
+++ b/core/variant/variant_call.cpp
@@ -1790,6 +1790,7 @@ static void _register_variant_builtin_methods_string() {
bind_string_method(xml_unescape, sarray(), varray());
bind_string_method(uri_encode, sarray(), varray());
bind_string_method(uri_decode, sarray(), varray());
+ bind_string_method(uri_file_decode, sarray(), varray());
bind_string_method(c_escape, sarray(), varray());
bind_string_method(c_unescape, sarray(), varray());
bind_string_method(json_escape, sarray(), varray());
diff --git a/doc/classes/String.xml b/doc/classes/String.xml
index a3cf6c21490..f3d23ed3e2b 100644
--- a/doc/classes/String.xml
+++ b/doc/classes/String.xml
@@ -1130,6 +1130,7 @@
GD.Print(url.URIDecode()) // Prints "$DOCS_URL/?highlight=Godot Engine:docs"
[/csharp]
[/codeblocks]
+ [b]Note:[/b] This method decodes [code]+[/code] as space.
@@ -1152,6 +1153,12 @@
[/codeblocks]
+
+
+
+ Decodes the file path from its URL-encoded format. Unlike [method uri_decode] this method leaves [code]+[/code] as is.
+
+
diff --git a/doc/classes/StringName.xml b/doc/classes/StringName.xml
index e0c84bd6708..f3eebcffdb7 100644
--- a/doc/classes/StringName.xml
+++ b/doc/classes/StringName.xml
@@ -1038,6 +1038,7 @@
GD.Print(url.URIDecode()) // Prints "$DOCS_URL/?highlight=Godot Engine:docs"
[/csharp]
[/codeblocks]
+ [b]Note:[/b] This method decodes [code]+[/code] as space.
@@ -1060,6 +1061,12 @@
[/codeblocks]
+
+
+
+ Decodes the file path from its URL-encoded format. Unlike [method uri_decode] this method leaves [code]+[/code] as is.
+
+
diff --git a/drivers/unix/dir_access_unix.cpp b/drivers/unix/dir_access_unix.cpp
index 1b3757e0b00..93491a96b14 100644
--- a/drivers/unix/dir_access_unix.cpp
+++ b/drivers/unix/dir_access_unix.cpp
@@ -257,7 +257,7 @@ static void _get_drives(List *list) {
// Parse only file:// links
if (strncmp(string, "file://", 7) == 0) {
// Strip any unwanted edges on the strings and push_back if it's not a duplicate.
- String fpath = String::utf8(string + 7).strip_edges().split_spaces()[0].uri_decode();
+ String fpath = String::utf8(string + 7).strip_edges().split_spaces()[0].uri_file_decode();
if (!list->find(fpath)) {
list->push_back(fpath);
}
diff --git a/editor/import/3d/collada.cpp b/editor/import/3d/collada.cpp
index b403c53ef18..9aa0a72f03b 100644
--- a/editor/import/3d/collada.cpp
+++ b/editor/import/3d/collada.cpp
@@ -288,7 +288,7 @@ void Collada::_parse_image(XMLParser &p_parser) {
String path = p_parser.get_named_attribute_value("source").strip_edges();
if (!path.contains("://") && path.is_relative_path()) {
// path is relative to file being loaded, so convert to a resource path
- image.path = ProjectSettings::get_singleton()->localize_path(state.local_path.get_base_dir().path_join(path.uri_decode()));
+ image.path = ProjectSettings::get_singleton()->localize_path(state.local_path.get_base_dir().path_join(path.uri_file_decode()));
}
} else {
while (p_parser.read() == OK) {
@@ -297,7 +297,7 @@ void Collada::_parse_image(XMLParser &p_parser) {
if (name == "init_from") {
p_parser.read();
- String path = p_parser.get_node_data().strip_edges().uri_decode();
+ String path = p_parser.get_node_data().strip_edges().uri_file_decode();
if (!path.contains("://") && path.is_relative_path()) {
// path is relative to file being loaded, so convert to a resource path
diff --git a/modules/gdscript/language_server/gdscript_workspace.cpp b/modules/gdscript/language_server/gdscript_workspace.cpp
index 6806c801611..b891c42eaaa 100644
--- a/modules/gdscript/language_server/gdscript_workspace.cpp
+++ b/modules/gdscript/language_server/gdscript_workspace.cpp
@@ -559,8 +559,8 @@ Error GDScriptWorkspace::parse_local_script(const String &p_path) {
}
String GDScriptWorkspace::get_file_path(const String &p_uri) const {
- String path = p_uri.uri_decode();
- String base_uri = root_uri.uri_decode();
+ String path = p_uri.uri_file_decode();
+ String base_uri = root_uri.uri_file_decode();
path = path.replacen(base_uri + "/", "res://");
return path;
}
diff --git a/modules/gltf/gltf_document.cpp b/modules/gltf/gltf_document.cpp
index 14b4f96c8df..59e61eb9e0f 100644
--- a/modules/gltf/gltf_document.cpp
+++ b/modules/gltf/gltf_document.cpp
@@ -812,7 +812,7 @@ Error GLTFDocument::_parse_buffers(Ref p_state, const String &p_base_
buffer_data = _parse_base64_uri(uri);
} else { // Relative path to an external image file.
ERR_FAIL_COND_V(p_base_path.is_empty(), ERR_INVALID_PARAMETER);
- uri = uri.uri_decode();
+ uri = uri.uri_file_decode();
uri = p_base_path.path_join(uri).replace("\\", "/"); // Fix for Windows.
ERR_FAIL_COND_V_MSG(!FileAccess::exists(uri), ERR_FILE_NOT_FOUND, "glTF: Binary file not found: " + uri);
buffer_data = FileAccess::get_file_as_bytes(uri);
@@ -4123,7 +4123,7 @@ Error GLTFDocument::_parse_images(Ref p_state, const String &p_base_p
}
} else { // Relative path to an external image file.
ERR_FAIL_COND_V(p_base_path.is_empty(), ERR_INVALID_PARAMETER);
- uri = uri.uri_decode();
+ uri = uri.uri_file_decode();
uri = p_base_path.path_join(uri).replace("\\", "/"); // Fix for Windows.
resource_uri = uri.simplify_path();
// ResourceLoader will rely on the file extension to use the relevant loader.
diff --git a/platform/linuxbsd/freedesktop_portal_desktop.cpp b/platform/linuxbsd/freedesktop_portal_desktop.cpp
index cafba278336..21e18b305bc 100644
--- a/platform/linuxbsd/freedesktop_portal_desktop.cpp
+++ b/platform/linuxbsd/freedesktop_portal_desktop.cpp
@@ -473,7 +473,7 @@ bool FreeDesktopPortalDesktop::file_chooser_parse_response(DBusMessageIter *p_it
while (dbus_message_iter_get_arg_type(&uri_iter) == DBUS_TYPE_STRING) {
const char *value;
dbus_message_iter_get_basic(&uri_iter, &value);
- r_urls.push_back(String::utf8(value).trim_prefix("file://").uri_decode());
+ r_urls.push_back(String::utf8(value).trim_prefix("file://").uri_file_decode());
if (!dbus_message_iter_next(&uri_iter)) {
break;
}
diff --git a/platform/linuxbsd/wayland/wayland_thread.cpp b/platform/linuxbsd/wayland/wayland_thread.cpp
index 0e02656941b..30b90b7b4af 100644
--- a/platform/linuxbsd/wayland/wayland_thread.cpp
+++ b/platform/linuxbsd/wayland/wayland_thread.cpp
@@ -2147,7 +2147,7 @@ void WaylandThread::_wl_data_device_on_drop(void *data, struct wl_data_device *w
msg->files = String::utf8((const char *)list_data.ptr(), list_data.size()).split("\r\n", false);
for (int i = 0; i < msg->files.size(); i++) {
- msg->files.write[i] = msg->files[i].replace("file://", "").uri_decode();
+ msg->files.write[i] = msg->files[i].replace("file://", "").uri_file_decode();
}
wayland_thread->push_message(msg);
diff --git a/platform/linuxbsd/x11/display_server_x11.cpp b/platform/linuxbsd/x11/display_server_x11.cpp
index 5f2850c83c5..c0717db29dc 100644
--- a/platform/linuxbsd/x11/display_server_x11.cpp
+++ b/platform/linuxbsd/x11/display_server_x11.cpp
@@ -5321,7 +5321,7 @@ void DisplayServerX11::process_events() {
Vector files = String((char *)p.data).split("\r\n", false);
XFree(p.data);
for (int i = 0; i < files.size(); i++) {
- files.write[i] = files[i].replace("file://", "").uri_decode();
+ files.write[i] = files[i].replace("file://", "").uri_file_decode();
}
if (windows[window_id].drop_files_callback.is_valid()) {
diff --git a/tests/core/string/test_string.h b/tests/core/string/test_string.h
index 4e08e6ac09c..314c926e1f1 100644
--- a/tests/core/string/test_string.h
+++ b/tests/core/string/test_string.h
@@ -1774,6 +1774,7 @@ TEST_CASE("[String] uri_encode/unescape") {
static const uint8_t u8str[] = { 0x54, 0xC4, 0x93, 0xC5, 0xA1, 0x74, 0x00 };
String x2 = String::utf8((const char *)u8str);
String x3 = U"Tēšt";
+ String x4 = U"file+name";
CHECK(x1.uri_decode() == x2);
CHECK(x1.uri_decode() == x3);
@@ -1783,6 +1784,8 @@ TEST_CASE("[String] uri_encode/unescape") {
CHECK(s.uri_encode() == t);
CHECK(t.uri_decode() == s);
+ CHECK(x4.uri_file_decode() == x4);
+ CHECK(x4.uri_decode() == U"file name");
}
TEST_CASE("[String] xml_escape/unescape") {