Skip to content

Commit 365fe8b

Browse files
committed
CoreFoundation: correct CreateStringFromFileSystemRepresentationByAddingPercentEscapes
On Windows, `CreateStringFromFileSystemRepresentationByAddingPercentEscapes` would only partially convert the slash character to %2F. This would result in paths being truncated. Fix this behaviour to canonicalise the separator properly always. Unfortunately, this uncovered some new cases of file path handling being broken on Windows. Add tests to record those cases so that we can fix them subsequently.
1 parent f4165ce commit 365fe8b

File tree

2 files changed

+88
-6
lines changed

2 files changed

+88
-6
lines changed

CoreFoundation/URL.subproj/CFURL.c

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -894,15 +894,34 @@ static CFStringRef CreateStringFromFileSystemRepresentationByAddingPercentEscape
894894
if ( idx == numBytes ) {
895895
if ( isDirectory ) {
896896
// if it is a directory and it doesn't end with PATH_SEP, append a PATH_SEP.
897-
if ( bytes[numBytes-1] != '/' ) {
898-
*bufBytePtr++ = '/';
897+
if ( windowsPath ) {
898+
if ( bufBytePtr - bufStartPtr > 3 ) {
899+
if ( strncmp((const char *)(bufBytePtr - 3), "%2F", 3) ) {
900+
*bufBytePtr++ = '%';
901+
*bufBytePtr++ = '2';
902+
*bufBytePtr++ = 'F';
903+
}
904+
}
905+
}
906+
else {
907+
if ( bytes[numBytes-1] != '/' ) {
908+
*bufBytePtr++ = '/';
909+
}
899910
}
900911
}
901912
else {
902913
// it is not a directory: remove any pathDelim characters at end (leaving at least one character)
903-
while ( (numBytes > 1) && (bytes[numBytes-1] == '/') ) {
904-
--bufBytePtr;
905-
--numBytes;
914+
if ( windowsPath ) {
915+
while ( (numBytes > 1) && (bufBytePtr - bufStartPtr > 3) && (strncmp((const char *)(bufBytePtr - 3), "%2F", 3) == 0) ) {
916+
bufBytePtr -= 3;
917+
--numBytes;
918+
}
919+
}
920+
else {
921+
while ( (numBytes > 1) && (bytes[numBytes-1] == '/') ) {
922+
--bufBytePtr;
923+
--numBytes;
924+
}
906925
}
907926
}
908927

TestFoundation/TestURL.swift

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,60 @@ private func getTestData() -> [Any]? {
4747
}
4848

4949
class TestURL : XCTestCase {
50+
#if os(Windows)
51+
func test_WindowsPathSeparator() {
52+
// ensure that the mixed slashes are handled properly
53+
// e.g. NOT file:///S:/b/u1%2/
54+
let u1 = URL(fileURLWithPath: "S:\\b\\u1/")
55+
XCTAssertEqual(u1.absoluteString, "file:///S:/b/u1/")
56+
57+
// ensure that trailing slashes are compressed
58+
// e.g. NOT file:///S:/b/u2%2F%2F%2F%/
59+
let u2 = URL(fileURLWithPath: "S:\\b\\u2/////")
60+
XCTAssertEqual(u2.absoluteString, "file:///S:/b/u2/")
61+
62+
// ensure that the trailing slashes are compressed even when mixed
63+
// e.g. NOT file:///S:/b/u3%2F%/%2F%2/
64+
let u3 = URL(fileURLWithPath: "S:\\b\\u3//\\//")
65+
// XCTAssertEqual(u3.absoluteString, "file:///S:/b/u3/%2F/")
66+
XCTAssertEqual(u3.path, "S:\\b\\u3\\")
67+
68+
// ensure that the regular conversion works
69+
let u4 = URL(fileURLWithPath: "S:\\b\\u4")
70+
XCTAssertEqual(u4.absoluteString, "file:///S:/b/u4")
71+
72+
// ensure that the trailing slash is added
73+
let u5 = URL(fileURLWithPath: "S:\\b\\u5", isDirectory: true)
74+
XCTAssertEqual(u5.absoluteString, "file:///S:/b/u5/")
75+
76+
// ensure that the trailing slash is preserved
77+
let u6 = URL(fileURLWithPath: "S:\\b\\u6\\")
78+
XCTAssertEqual(u6.absoluteString, "file:///S:/b/u6/")
79+
80+
// ensure that we do not index beyond the start of the string
81+
let u7 = URL(fileURLWithPath: "eh", relativeTo: URL(fileURLWithPath: "S:\\b"))
82+
XCTAssertEqual(u7.absoluteString, "file:///S:/b/eh")
83+
84+
// ensure that / is handled properly
85+
let u8 = URL(fileURLWithPath: "/")
86+
XCTAssertEqual(u8.absoluteString, "file:///")
87+
}
88+
89+
func test_WindowsPathSeparator2() {
90+
let u1 = URL(fileURLWithPath: "S:\\b\\u1\\", isDirectory: false)
91+
XCTAssertEqual(u1.absoluteString, "file:///S:/b/u1")
92+
93+
let u2 = URL(fileURLWithPath: "/", isDirectory: false)
94+
XCTAssertEqual(u2.absoluteString, "file:///")
95+
96+
let u3 = URL(fileURLWithPath: "\\", isDirectory: false)
97+
XCTAssertEqual(u3.absoluteString, "file:///")
98+
99+
let u4 = URL(fileURLWithPath: "S:\\b\\u3//\\//")
100+
XCTAssertEqual(u4.absoluteString, "file:///S:/b/u3/")
101+
}
102+
#endif
103+
50104
func test_fileURLWithPath_relativeTo() {
51105
let homeDirectory = NSHomeDirectory()
52106
let homeURL = URL(fileURLWithPath: homeDirectory, isDirectory: true)
@@ -604,7 +658,7 @@ class TestURL : XCTestCase {
604658
}
605659

606660
static var allTests: [(String, (TestURL) -> () throws -> Void)] {
607-
return [
661+
var tests: [(String, (TestURL) -> () throws -> Void)] = [
608662
("test_URLStrings", test_URLStrings),
609663
("test_fileURLWithPath_relativeTo", test_fileURLWithPath_relativeTo ),
610664
// TODO: these tests fail on linux, more investigation is needed
@@ -619,6 +673,15 @@ class TestURL : XCTestCase {
619673
("test_URLResourceValues", testExpectedToFail(test_URLResourceValues,
620674
"test_URLResourceValues: Except for .nameKey, we have no testable attributes that work in the environment Swift CI uses, for now. SR-XXXX")),
621675
]
676+
677+
#if os(Windows)
678+
tests.append(contentsOf: [
679+
("test_WindowsPathSeparator", test_WindowsPathSeparator),
680+
("test_WindowsPathSeparator2", test_WindowsPathSeparator2),
681+
])
682+
#endif
683+
684+
return tests
622685
}
623686
}
624687

0 commit comments

Comments
 (0)