Skip to content

Commit d5a4147

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 95b8e5c commit d5a4147

File tree

2 files changed

+86
-6
lines changed

2 files changed

+86
-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: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,58 @@ private func getTestData() -> [Any]? {
4747
}
4848

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

606658
static var allTests: [(String, (TestURL) -> () throws -> Void)] {
607-
return [
659+
var tests: [(String, (TestURL) -> () throws -> Void)] = [
608660
("test_URLStrings", test_URLStrings),
609661
("test_fileURLWithPath_relativeTo", test_fileURLWithPath_relativeTo ),
610662
// TODO: these tests fail on linux, more investigation is needed
@@ -619,6 +671,15 @@ class TestURL : XCTestCase {
619671
("test_URLResourceValues", testExpectedToFail(test_URLResourceValues,
620672
"test_URLResourceValues: Except for .nameKey, we have no testable attributes that work in the environment Swift CI uses, for now. SR-XXXX")),
621673
]
674+
675+
#if os(Windows)
676+
tests.append(contentsOf: [
677+
("test_pathSeparator", test_pathSeparator),
678+
("test_pathSeparator2", test_pathSeparator2),
679+
])
680+
#endif
681+
682+
return tests
622683
}
623684
}
624685

0 commit comments

Comments
 (0)