Skip to content

Commit f1e9f5a

Browse files
Copilotbdlukaa
andauthored
Fix URL handling for percent-encoded multi-byte characters and spaces in file paths (#55)
* Initial plan * fix: remove UrlDecode from createMediaSource to fix percent-encoded multi-byte URL handling Agent-Logs-Url: https://github.com/bdlukaa/just_audio_windows/sessions/42197e6a-5c65-4385-8dd1-d22dedea027f Co-authored-by: bdlukaa <45696119+bdlukaa@users.noreply.github.com> * fix: also encode literal spaces as %20 to fix local file paths with spaces (issue #26) Agent-Logs-Url: https://github.com/bdlukaa/just_audio_windows/sessions/65266179-292a-498b-b65f-ba56ab67e292 Co-authored-by: bdlukaa <45696119+bdlukaa@users.noreply.github.com> * test: add Windows unit tests for EncodeSpacesInUri (uri_utils.hpp) Agent-Logs-Url: https://github.com/bdlukaa/just_audio_windows/sessions/0b314835-e5e4-4875-b986-88a7e57063a2 Co-authored-by: bdlukaa <45696119+bdlukaa@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bdlukaa <45696119+bdlukaa@users.noreply.github.com> Co-authored-by: Bruno D'Luka <brunodlukaa@gmail.com>
1 parent ba00210 commit f1e9f5a

5 files changed

Lines changed: 140 additions & 5 deletions

File tree

just_audio_windows/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## [next]
2+
3+
* [fix]: Fix URL handling for percent-encoded multi-byte characters (e.g. curly apostrophes) in query strings
4+
* [fix]: Fix playback of local file paths containing spaces
5+
16
## 0.2.2
27

38
* [fix]: Seeking in long tracks (above 36m) ([#36](https://github.com/bdlukaa/just_audio_windows/pull/36))

just_audio_windows/windows/CMakeLists.txt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,38 @@ set(just_audio_windows_bundled_libraries
2525
""
2626
PARENT_SCOPE
2727
)
28+
29+
# === Tests ===
30+
31+
if (${include_${PROJECT_NAME}_tests})
32+
set(TEST_RUNNER "${PROJECT_NAME}_test")
33+
enable_testing()
34+
35+
# Download googletest. Using the same version as the camera_windows plugin
36+
# for consistency across Flutter Windows plugins.
37+
include(FetchContent)
38+
FetchContent_Declare(
39+
googletest
40+
URL https://github.com/google/googletest/archive/release-1.11.0.zip
41+
)
42+
# Prevent overriding the parent project's compiler/linker settings.
43+
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
44+
# Disable install commands for gtest so it doesn't end up in the bundle.
45+
set(INSTALL_GTEST OFF CACHE BOOL "Disable installation of googletest" FORCE)
46+
47+
FetchContent_MakeAvailable(googletest)
48+
49+
# uri_utils.hpp is a header-only, WinRT-free helper, so the test binary
50+
# needs no plugin sources or special Windows libraries.
51+
add_executable(${TEST_RUNNER}
52+
test/uri_utils_test.cpp
53+
)
54+
apply_standard_settings(${TEST_RUNNER})
55+
target_include_directories(${TEST_RUNNER} PRIVATE
56+
"${CMAKE_CURRENT_SOURCE_DIR}")
57+
target_link_libraries(${TEST_RUNNER} PRIVATE gtest_main)
58+
59+
include(GoogleTest)
60+
gtest_discover_tests(${TEST_RUNNER})
61+
endif()
62+

just_audio_windows/windows/player.hpp

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@
1010
#include <flutter/method_channel.h>
1111
#include <flutter/standard_method_codec.h>
1212

13+
#include "uri_utils.hpp"
14+
1315
#include <winrt/Windows.Foundation.Collections.h>
1416
#include <winrt/Windows.Foundation.h>
1517
#include <winrt/Windows.Media.Audio.h>
1618
#include <winrt/Windows.Media.Core.h>
1719
#include <winrt/Windows.Media.Playback.h>
1820
#include <winrt/Windows.System.h>
19-
#include "url_code.hpp"
20-
2121
#define TO_MILLISECONDS(timespan) timespan.count() / 10000
2222
#define TO_MICROSECONDS(timespan) TO_MILLISECONDS(timespan) * 1000
2323

@@ -521,10 +521,8 @@ class AudioPlayer {
521521
const std::string* type = std::get_if<std::string>(ValueOrNull(source, "type"));
522522
if (type->compare("progressive") == 0 || type->compare("dash") == 0 || type->compare("hls") == 0) {
523523
const auto* uri = std::get_if<std::string>(ValueOrNull(source, "uri"));
524-
std::string decodedUri;
525-
UrlDecode(*uri, decodedUri);
526524
return MediaSource::CreateFromUri(
527-
Uri(TO_WIDESTRING(decodedUri))
525+
Uri(TO_WIDESTRING(EncodeSpacesInUri(*uri)))
528526
);
529527
}
530528
else {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#include "../uri_utils.hpp"
2+
3+
#include <gtest/gtest.h>
4+
5+
namespace just_audio_windows {
6+
namespace test {
7+
8+
// ── EncodeSpacesInUri ────────────────────────────────────────────────────────
9+
10+
// A plain URL with no spaces should pass through unchanged.
11+
TEST(EncodeSpacesInUri, NoSpaces_ReturnsUnchanged) {
12+
EXPECT_EQ(EncodeSpacesInUri("https://example.com/audio.mp3"),
13+
"https://example.com/audio.mp3");
14+
}
15+
16+
// An empty string should stay empty.
17+
TEST(EncodeSpacesInUri, EmptyString_ReturnsEmpty) {
18+
EXPECT_EQ(EncodeSpacesInUri(""), "");
19+
}
20+
21+
// Literal spaces in a local file path must be encoded as %20.
22+
// This is the core regression for https://github.com/bdlukaa/just_audio_windows/issues/26.
23+
TEST(EncodeSpacesInUri, LiteralSpacesInFilePath_EncodedAsPercent20) {
24+
EXPECT_EQ(
25+
EncodeSpacesInUri("file:///C:/Users/My Files/song.mp3"),
26+
"file:///C:/Users/My%20Files/song.mp3");
27+
}
28+
29+
// Multiple consecutive spaces must each be individually encoded.
30+
TEST(EncodeSpacesInUri, MultipleSpaces_AllEncoded) {
31+
EXPECT_EQ(
32+
EncodeSpacesInUri(
33+
"C:/Users/HP/Downloads/Ve Kamleya Rocky Aur Rani.mp3"),
34+
"C:/Users/HP/Downloads/Ve%20Kamleya%20Rocky%20Aur%20Rani.mp3");
35+
}
36+
37+
// Already percent-encoded spaces (%20) must NOT be double-encoded.
38+
// This ensures a properly-encoded file:// URI like those produced by
39+
// Dart's Uri.file() is left intact.
40+
TEST(EncodeSpacesInUri, AlreadyEncodedSpaces_NotDoubleEncoded) {
41+
EXPECT_EQ(
42+
EncodeSpacesInUri("file:///C:/Users/My%20Files/song.mp3"),
43+
"file:///C:/Users/My%20Files/song.mp3");
44+
}
45+
46+
// Percent-encoded multi-byte UTF-8 sequences (e.g. U+2019 RIGHT SINGLE
47+
// QUOTATION MARK) must pass through unchanged so that Windows::Foundation::Uri
48+
// can decode them natively.
49+
// This is the core regression for the original apostrophe bug.
50+
TEST(EncodeSpacesInUri, MultiBytePercentEncoded_Unchanged) {
51+
EXPECT_EQ(
52+
EncodeSpacesInUri(
53+
"https://example.com/speech?text=I%E2%80%99d%20like%20a%20coffee"),
54+
"https://example.com/speech?text=I%E2%80%99d%20like%20a%20coffee");
55+
}
56+
57+
// A URL with both unencoded spaces and percent-encoded multi-byte chars:
58+
// spaces must be encoded while the percent-sequences stay intact.
59+
TEST(EncodeSpacesInUri, MixedLiteralSpacesAndPercentEncoded) {
60+
EXPECT_EQ(
61+
EncodeSpacesInUri(
62+
"https://example.com/speech?text=I%E2%80%99d like a coffee"),
63+
"https://example.com/speech?text=I%E2%80%99d%20like%20a%20coffee");
64+
}
65+
66+
// A URL with only query-string spaces should have them encoded.
67+
TEST(EncodeSpacesInUri, SpaceInQueryString) {
68+
EXPECT_EQ(
69+
EncodeSpacesInUri("https://example.com/tts?text=hello world"),
70+
"https://example.com/tts?text=hello%20world");
71+
}
72+
73+
} // namespace test
74+
} // namespace just_audio_windows
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#pragma once
2+
3+
#include <string>
4+
5+
// Encodes literal space characters in a URI string as "%20" so that
6+
// Windows::Foundation::Uri accepts URIs that contain unencoded spaces
7+
// (e.g. local file paths like "file:///C:/My Files/song.mp3").
8+
//
9+
// Already percent-encoded sequences such as "%20" or "%E2%80%99" are never
10+
// modified, because they contain no literal space character.
11+
inline std::string EncodeSpacesInUri(const std::string& uri) {
12+
std::string encoded;
13+
// Reserve worst-case capacity (every char is a space → 3 chars each).
14+
encoded.reserve(uri.length() * 3);
15+
for (char c : uri) {
16+
if (c == ' ') {
17+
encoded += "%20";
18+
} else {
19+
encoded += c;
20+
}
21+
}
22+
return encoded;
23+
}

0 commit comments

Comments
 (0)