Skip to content

Commit 13a9723

Browse files
authored
Allow semicolon in file path for Swift URL v2 (swiftlang#1882)
* (174175082) Allow semicolon in file path for Swift URL v2 * Clarify 'compatibility' -> 'encodeSemicolons'
1 parent 503dc5a commit 13a9723

4 files changed

Lines changed: 31 additions & 14 deletions

File tree

Sources/FoundationEssentials/URL/URL_C+File.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ private func parseFileSystemRepresentation(
122122
isDirectory: isDirectory
123123
)
124124
let path = pathBuffer.span.extracting(first: finalLength)
125-
return URL.parseFinalFileSystemRepresentation(path: path, flags: &flags)
125+
return URL.parseFinalFileSystemRepresentation(path: path, flags: &flags, encodeSemicolons: true)
126126
}
127127
}
128128

@@ -159,7 +159,7 @@ private func parsePOSIX(_ path: CFString, flags: inout _URLFlags, isDirectory: B
159159
isDirectory: isDirectory
160160
)
161161
let path = pathBuffer.span.extracting(first: finalLength)
162-
return URL.parseFinalFileSystemRepresentation(path: path, flags: &flags)
162+
return URL.parseFinalFileSystemRepresentation(path: path, flags: &flags, encodeSemicolons: true)
163163
}
164164
}
165165

@@ -187,7 +187,7 @@ private func parseHFS(_ path: String, flags: inout _URLFlags, isDirectory: Bool)
187187
guard !path.isEmpty else {
188188
return ""
189189
}
190-
return URL.parseUTF8Path(path, flags: &flags, isDirectory: isDirectory)
190+
return URL.parseUTF8Path(path, flags: &flags, isDirectory: isDirectory, encodeSemicolons: true)
191191
}
192192

193193
// Note this does not percent-encode the path

Sources/FoundationEssentials/URL/URL_File.swift

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ extension URL {
7676
/// see `finalPathLength(updating:currentLength:flags:isDirectory)`.
7777
static func parseFinalFileSystemRepresentation(
7878
path: borrowing Span<UInt8>,
79-
flags: inout _URLFlags
79+
flags: inout _URLFlags,
80+
encodeSemicolons: Bool
8081
) -> String {
8182
guard path.count > 0 else {
8283
return ""
@@ -108,8 +109,8 @@ extension URL {
108109
URLEncoder.percentEncodeUnchecked(
109110
input: pathBuffer,
110111
output: .init(rebasing: encodedBuffer[pathStart...]),
111-
// Encode ";" for compatibility
112-
component: .pathNoSemicolon,
112+
// Encode ";" for CF/NSURL (encodeSemicolons: true)
113+
component: encodeSemicolons ? .pathNoSemicolon : .path,
113114
skipAlreadyEncoded: false
114115
)
115116
}
@@ -121,7 +122,7 @@ extension URL {
121122
}
122123
}
123124

124-
static func parseUTF8Path(_ path: String, flags: inout _URLFlags, isDirectory: Bool) -> String {
125+
static func parseUTF8Path(_ path: String, flags: inout _URLFlags, isDirectory: Bool, encodeSemicolons: Bool) -> String {
125126
var path = path
126127
return path.withUTF8 { pathBuffer in
127128
// Allocate an extra byte in case we need to append a directory slash.
@@ -133,7 +134,7 @@ extension URL {
133134
isDirectory: isDirectory
134135
)
135136
let path = $0.span.extracting(first: pathLength)
136-
return parseFinalFileSystemRepresentation(path: path, flags: &flags)
137+
return parseFinalFileSystemRepresentation(path: path, flags: &flags, encodeSemicolons: encodeSemicolons)
137138
}
138139
}
139140
}
@@ -144,7 +145,7 @@ extension URL {
144145
#if FOUNDATION_FRAMEWORK
145146
#if !os(watchOS)
146147
if path.utf8Span.isKnownASCII {
147-
return parseUTF8Path(path, flags: &flags, isDirectory: isDirectory)
148+
return parseUTF8Path(path, flags: &flags, isDirectory: isDirectory, encodeSemicolons: false)
148149
}
149150
#endif
150151
// Convert path to its decomposed file system representation
@@ -163,7 +164,7 @@ extension URL {
163164
isDirectory: isDirectory
164165
)
165166
let path = pathBuffer.span.extracting(first: finalLength)
166-
return parseFinalFileSystemRepresentation(path: path, flags: &flags)
167+
return parseFinalFileSystemRepresentation(path: path, flags: &flags, encodeSemicolons: false)
167168
}
168169

169170
// Decomposition failed or there was an embedded null byte.
@@ -176,10 +177,10 @@ extension URL {
176177
// to a "file not found" error, this is more practical and debuggable
177178
// than returning an empty URL or crashing with fatalError().
178179

179-
return parseUTF8Path(path, flags: &flags, isDirectory: isDirectory)
180+
return parseUTF8Path(path, flags: &flags, isDirectory: isDirectory, encodeSemicolons: false)
180181
}
181182
#else
182-
return parseUTF8Path(path, flags: &flags, isDirectory: isDirectory)
183+
return parseUTF8Path(path, flags: &flags, isDirectory: isDirectory, encodeSemicolons: false)
183184
#endif
184185
}
185186

@@ -190,6 +191,6 @@ extension URL {
190191
guard !path.isEmpty else {
191192
return ""
192193
}
193-
return parseUTF8Path(path, flags: &flags, isDirectory: isDirectory)
194+
return parseUTF8Path(path, flags: &flags, isDirectory: isDirectory, encodeSemicolons: false)
194195
}
195196
}

Sources/FoundationEssentials/URL/URL_Info.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ extension _URLInfo {
282282
isDirectory: isDirectory
283283
)
284284
let path = buffer.span.extracting(first: finalLength)
285-
return URL.parseFinalFileSystemRepresentation(path: path, flags: &flags)
285+
return URL.parseFinalFileSystemRepresentation(path: path, flags: &flags, encodeSemicolons: false)
286286
}
287287

288288
let base = resolveBaseURL(base, updating: &flags)

Tests/FoundationEssentialsTests/URLTests.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,22 @@ private struct URLTests {
561561
try FileManager.default.removeItem(at: URL(filePath: "\(tempDirectory.path)/tmp-dir"))
562562
}
563563

564+
@Test func filePathAPIsWithSemicolon() throws {
565+
// The NSURL and CFURL file path APIs encode ";" in file paths
566+
// for compatibility. URL and other modern parsers do not.
567+
var url = URL(filePath: "/path;to/file")
568+
#expect(url.path == "/path;to/file")
569+
#expect(url.relativeString == "file:///path;to/file")
570+
571+
url.append(path: "hello;world")
572+
#expect(url.path == "/path;to/file/hello;world")
573+
#expect(url.relativeString == "file:///path;to/file/hello;world")
574+
575+
url.appendPathExtension("some;ext")
576+
#expect(url.path == "/path;to/file/hello;world.some;ext")
577+
#expect(url.relativeString == "file:///path;to/file/hello;world.some;ext")
578+
}
579+
564580
#if FOUNDATION_FRAMEWORK
565581
@Test func fileSystemRepresentations() throws {
566582
let base = "/base/"

0 commit comments

Comments
 (0)