Skip to content

Commit 26d30e3

Browse files
authored
Merge pull request #3096 from lha/URLComponents.percentEncodedQueryItems
2 parents dab9598 + 4b19ed5 commit 26d30e3

File tree

3 files changed

+91
-20
lines changed

3 files changed

+91
-20
lines changed

Sources/Foundation/NSURLComponents.swift

+48-19
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,32 @@ open class NSURLComponents: NSObject, NSCopying {
298298
return NSRange(_CFURLComponentsGetRangeOfFragment(_components))
299299
}
300300

301+
private func mapQueryItemsFromArray(array: CFArray) -> [URLQueryItem] {
302+
let count = CFArrayGetCount(array)
303+
return (0..<count).map { idx in
304+
let oneEntry = unsafeBitCast(CFArrayGetValueAtIndex(array, idx), to: NSDictionary.self)
305+
let swiftEntry = oneEntry._swiftObject
306+
let entryName = swiftEntry["name"] as! String
307+
let entryValue = swiftEntry["value"] as? String
308+
return URLQueryItem(name: entryName, value: entryValue)
309+
}
310+
}
311+
312+
private func mapURLQueryItemArrayToCFArrays(array: [URLQueryItem]) -> (names: [CFTypeRef], values: [CFTypeRef]) {
313+
// The CFURL implementation requires two CFArrays, one for names and one for values
314+
var names = [CFTypeRef]()
315+
var values = [CFTypeRef]()
316+
for entry in array {
317+
names.append(entry.name._cfObject)
318+
if let v = entry.value {
319+
values.append(v._cfObject)
320+
} else {
321+
values.append(kCFNull)
322+
}
323+
}
324+
return (names, values)
325+
}
326+
301327
// The getter method that underlies the queryItems property parses the query string based on these delimiters and returns an NSArray containing any number of NSURLQueryItem objects, each of which represents a single key-value pair, in the order in which they appear in the original query string. Note that a name may appear more than once in a single query string, so the name values are not guaranteed to be unique. If the NSURLComponents object has an empty query component, queryItems returns an empty NSArray. If the NSURLComponents object has no query component, queryItems returns nil.
302328
// The setter method that underlies the queryItems property combines an NSArray containing any number of NSURLQueryItem objects, each of which represents a single key-value pair, into a query string and sets the NSURLComponents' query property. Passing an empty NSArray to setQueryItems sets the query component of the NSURLComponents object to an empty string. Passing nil to setQueryItems removes the query component of the NSURLComponents object.
303329
// Note: If a name-value pair in a query is empty (i.e. the query string starts with '&', ends with '&', or has "&&" within it), you get a NSURLQueryItem with a zero-length name and and a nil value. If a query's name-value pair has nothing before the equals sign, you get a zero-length name. If a query's name-value pair has nothing after the equals sign, you get a zero-length value. If a query's name-value pair has no equals sign, the query name-value pair string is the name and you get a nil value.
@@ -308,35 +334,38 @@ open class NSURLComponents: NSObject, NSCopying {
308334
return nil
309335
}
310336

311-
let count = CFArrayGetCount(queryArray)
312-
return (0..<count).map { idx in
313-
let oneEntry = unsafeBitCast(CFArrayGetValueAtIndex(queryArray, idx), to: NSDictionary.self)
314-
let swiftEntry = oneEntry._swiftObject
315-
let entryName = swiftEntry["name"] as! String
316-
let entryValue = swiftEntry["value"] as? String
317-
return URLQueryItem(name: entryName, value: entryValue)
318-
}
337+
return mapQueryItemsFromArray(array: queryArray)
319338
}
320339
set(new) {
321340
guard let new = new else {
322341
self.percentEncodedQuery = nil
323342
return
324343
}
325344

326-
// The CFURL implementation requires two CFArrays, one for names and one for values
327-
var names = [CFTypeRef]()
328-
var values = [CFTypeRef]()
329-
for entry in new {
330-
names.append(entry.name._cfObject)
331-
if let v = entry.value {
332-
values.append(v._cfObject)
333-
} else {
334-
values.append(kCFNull)
335-
}
345+
let items = mapURLQueryItemArrayToCFArrays(array: new)
346+
_CFURLComponentsSetQueryItems(_components, items.names._cfObject, items.values._cfObject)
347+
}
348+
}
349+
350+
open var percentEncodedQueryItems: [URLQueryItem]? {
351+
get {
352+
guard let queryArray = _CFURLComponentsCopyPercentEncodedQueryItems(_components) else {
353+
return nil
354+
}
355+
356+
return mapQueryItemsFromArray(array: queryArray)
357+
}
358+
set(new) {
359+
guard let new = new else {
360+
self.percentEncodedQuery = nil
361+
return
336362
}
337-
_CFURLComponentsSetQueryItems(_components, names._cfObject, values._cfObject)
363+
364+
let items = mapURLQueryItemArrayToCFArrays(array: new)
365+
_CFURLComponentsSetPercentEncodedQueryItems(_components, items.names._cfObject, items.values._cfObject)
338366
}
339367
}
368+
340369
}
341370

342371
extension NSURLComponents: _StructTypeBridgeable {

Sources/Foundation/URLComponents.swift

+9-1
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,15 @@ public struct URLComponents : ReferenceConvertible, Hashable, Equatable, _Mutabl
268268
set { _applyMutation { $0.queryItems = newValue } }
269269
}
270270

271-
public func hash(into hasher: inout Hasher) {
271+
/// Returns an array of query items for this `URLComponents`, in the order in which they appear in the original query string. Any percent-encoding in a query item name or value is retained
272+
///
273+
/// The setter combines an array containing any number of `URLQueryItem`s, each of which represents a single key-value pair, into a query string and sets the `URLComponents` query property. This property assumes the query item names and values are already correctly percent-encoded, and that the query item names do not contain the query item delimiter characters '&' and '='. Attempting to set an incorrectly percent-encoded query item or a query item name with the query item delimiter characters '&' and '=' will cause a `fatalError`.
274+
public var percentEncodedQueryItems: [URLQueryItem]? {
275+
get { return _handle.map { $0.percentEncodedQueryItems } }
276+
set { _applyMutation { $0.percentEncodedQueryItems = newValue } }
277+
}
278+
279+
public func hash(into hasher: inout Hasher) {
272280
hasher.combine(_handle.map { $0 })
273281
}
274282

Tests/Foundation/Tests/TestURLComponents.swift

+34
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,20 @@ class TestURLComponents: XCTestCase {
4141
XCTAssertEqual(["bar": "baz"], query)
4242
}
4343

44+
func test_percentEncodedQueryItems() {
45+
let urlString = "http://localhost:8080/foo?feed%20me=feed%20me"
46+
let url = URL(string: urlString)!
47+
48+
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
49+
50+
var query = [String: String]()
51+
components?.percentEncodedQueryItems?.forEach {
52+
query[$0.name] = $0.value ?? ""
53+
}
54+
XCTAssertEqual(["feed%20me": "feed%20me"], query)
55+
}
56+
57+
4458
func test_string() {
4559
for obj in getTestData()! {
4660
let testDict = obj as! [String: Any]
@@ -207,6 +221,24 @@ class TestURLComponents: XCTestCase {
207221
XCTAssertEqual(urlComponents.queryItems?.count, 4)
208222
}
209223

224+
func test_createURLWithComponentsPercentEncoded() {
225+
let urlComponents = NSURLComponents()
226+
urlComponents.scheme = "https";
227+
urlComponents.host = "com.test.swift";
228+
urlComponents.path = "/test/path";
229+
let query = URLQueryItem(name: "simple%20string", value: "true%20is%20false")
230+
urlComponents.percentEncodedQueryItems = [query]
231+
XCTAssertNotNil(urlComponents.url?.query)
232+
XCTAssertEqual(urlComponents.queryItems?.count, 1)
233+
XCTAssertEqual(urlComponents.percentEncodedQueryItems?.count, 1)
234+
guard let item = urlComponents.percentEncodedQueryItems?[0] else {
235+
XCTFail("first element is missing")
236+
return
237+
}
238+
XCTAssertEqual(item.name, "simple%20string")
239+
XCTAssertEqual(item.value, "true%20is%20false")
240+
}
241+
210242
func test_path() {
211243
let c1 = URLComponents()
212244
XCTAssertEqual(c1.path, "")
@@ -250,12 +282,14 @@ class TestURLComponents: XCTestCase {
250282
static var allTests: [(String, (TestURLComponents) -> () throws -> Void)] {
251283
return [
252284
("test_queryItems", test_queryItems),
285+
("test_percentEncodedQueryItems", test_percentEncodedQueryItems),
253286
("test_string", test_string),
254287
("test_port", test_portSetter),
255288
("test_url", test_url),
256289
("test_copy", test_copy),
257290
("test_hash", test_hash),
258291
("test_createURLWithComponents", test_createURLWithComponents),
292+
("test_createURLWithComponentsPercentEncoded", test_createURLWithComponentsPercentEncoded),
259293
("test_path", test_path),
260294
("test_percentEncodedPath", test_percentEncodedPath),
261295
]

0 commit comments

Comments
 (0)