[url_launcher] Decode file URLs before passing it to ShellExecuteW (#7774)
- ShellExecuteW does not handle file: urls that contain %-encoded UTF-8 strings correctly. %-encoded ASCII
strings are handled correctly, as are file "urls" that contain Unicode strings in its path component.
- This change perform URL decode on file: urls before passing to ShellExecuteW.
Fixes https://github.com/flutter/flutter/issues/156790
diff --git a/packages/url_launcher/url_launcher_windows/CHANGELOG.md b/packages/url_launcher/url_launcher_windows/CHANGELOG.md
index 1340552..668ee6e 100644
--- a/packages/url_launcher/url_launcher_windows/CHANGELOG.md
+++ b/packages/url_launcher/url_launcher_windows/CHANGELOG.md
@@ -1,6 +1,7 @@
-## NEXT
+## 3.1.3
* Updates minimum supported SDK version to Flutter 3.19/Dart 3.3.
+* Fixes handling of `file:` URLs that contain UTF-8 encoded paths.
## 3.1.2
diff --git a/packages/url_launcher/url_launcher_windows/pubspec.yaml b/packages/url_launcher/url_launcher_windows/pubspec.yaml
index addd536..629e9dc 100644
--- a/packages/url_launcher/url_launcher_windows/pubspec.yaml
+++ b/packages/url_launcher/url_launcher_windows/pubspec.yaml
@@ -2,7 +2,7 @@
description: Windows implementation of the url_launcher plugin.
repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_windows
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22
-version: 3.1.2
+version: 3.1.3
environment:
sdk: ^3.3.0
diff --git a/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt b/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt
index da39522..2b07f3a 100644
--- a/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt
+++ b/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt
@@ -25,7 +25,7 @@
target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL)
target_include_directories(${PLUGIN_NAME} INTERFACE
"${CMAKE_CURRENT_SOURCE_DIR}/include")
-target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin)
+target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin shlwapi.lib)
# List of absolute paths to libraries that should be bundled with the plugin
set(file_chooser_bundled_libraries
@@ -62,7 +62,7 @@
)
apply_standard_settings(${TEST_RUNNER})
target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
-target_link_libraries(${TEST_RUNNER} PRIVATE flutter_wrapper_plugin)
+target_link_libraries(${TEST_RUNNER} PRIVATE flutter_wrapper_plugin shlwapi.lib)
target_link_libraries(${TEST_RUNNER} PRIVATE gtest_main gmock)
# flutter_wrapper_plugin has link dependencies on the Flutter DLL.
add_custom_command(TARGET ${TEST_RUNNER} POST_BUILD
diff --git a/packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp b/packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp
index 2672957..db46e1a 100644
--- a/packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp
+++ b/packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp
@@ -22,10 +22,12 @@
using flutter::EncodableMap;
using flutter::EncodableValue;
+using ::testing::_;
using ::testing::DoAll;
using ::testing::Pointee;
using ::testing::Return;
using ::testing::SetArgPointee;
+using ::testing::StrEq;
class MockSystemApis : public SystemApis {
public:
@@ -135,5 +137,28 @@
EXPECT_TRUE(result.has_error());
}
+TEST(UrlLauncherPlugin, LaunchUTF8EncodedFileURLSuccess) {
+ std::unique_ptr<MockSystemApis> system = std::make_unique<MockSystemApis>();
+
+ // Return a success value (>32) from launching.
+ EXPECT_CALL(
+ *system,
+ ShellExecuteW(
+ _, StrEq(L"open"),
+ // 家の管理/スキャナ"),
+ StrEq(
+ L"file:///G:/\x5bb6\x306e\x7ba1\x7406/\x30b9\x30ad\x30e3\x30ca"),
+ _, _, _))
+ .WillOnce(Return(reinterpret_cast<HINSTANCE>(33)));
+
+ UrlLauncherPlugin plugin(std::move(system));
+ ErrorOr<bool> result = plugin.LaunchUrl(
+ "file:///G:/%E5%AE%B6%E3%81%AE%E7%AE%A1%E7%90%86/"
+ "%E3%82%B9%E3%82%AD%E3%83%A3%E3%83%8A");
+
+ ASSERT_FALSE(result.has_error());
+ EXPECT_TRUE(result.value());
+}
+
} // namespace test
} // namespace url_launcher_windows
diff --git a/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp b/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp
index c8e7b2f..b737b45 100644
--- a/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp
+++ b/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp
@@ -6,6 +6,7 @@
#include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h>
#include <flutter/standard_method_codec.h>
+#include <shlwapi.h>
#include <windows.h>
#include <memory>
@@ -98,7 +99,19 @@
}
ErrorOr<bool> UrlLauncherPlugin::LaunchUrl(const std::string& url) {
- std::wstring url_wide = Utf16FromUtf8(url);
+ std::wstring url_wide;
+ if (url.find("file:") == 0) {
+ // ShellExecuteW does not process %-encoded UTF8 strings in file URLs.
+ DWORD unescaped_len = 0;
+ std::string unescaped_url = url;
+ if (FAILED(::UrlUnescapeA(unescaped_url.data(), /*pszUnescaped=*/nullptr,
+ &unescaped_len, URL_UNESCAPE_INPLACE))) {
+ return FlutterError("open_error", "Failed to unescape file URL");
+ }
+ url_wide = Utf16FromUtf8(unescaped_url);
+ } else {
+ url_wide = Utf16FromUtf8(url);
+ }
int status = static_cast<int>(reinterpret_cast<INT_PTR>(
system_apis_->ShellExecuteW(nullptr, TEXT("open"), url_wide.c_str(),