Skip to content

[5.5] Url components.percent encoded query items #3102

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 48 additions & 19 deletions Sources/Foundation/NSURLComponents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,32 @@ open class NSURLComponents: NSObject, NSCopying {
return NSRange(_CFURLComponentsGetRangeOfFragment(_components))
}

private func mapQueryItemsFromArray(array: CFArray) -> [URLQueryItem] {
let count = CFArrayGetCount(array)
return (0..<count).map { idx in
let oneEntry = unsafeBitCast(CFArrayGetValueAtIndex(array, idx), to: NSDictionary.self)
let swiftEntry = oneEntry._swiftObject
let entryName = swiftEntry["name"] as! String
let entryValue = swiftEntry["value"] as? String
return URLQueryItem(name: entryName, value: entryValue)
}
}

private func mapURLQueryItemArrayToCFArrays(array: [URLQueryItem]) -> (names: [CFTypeRef], values: [CFTypeRef]) {
// The CFURL implementation requires two CFArrays, one for names and one for values
var names = [CFTypeRef]()
var values = [CFTypeRef]()
for entry in array {
names.append(entry.name._cfObject)
if let v = entry.value {
values.append(v._cfObject)
} else {
values.append(kCFNull)
}
}
return (names, values)
}

// 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.
// 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.
// 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.
Expand All @@ -308,35 +334,38 @@ open class NSURLComponents: NSObject, NSCopying {
return nil
}

let count = CFArrayGetCount(queryArray)
return (0..<count).map { idx in
let oneEntry = unsafeBitCast(CFArrayGetValueAtIndex(queryArray, idx), to: NSDictionary.self)
let swiftEntry = oneEntry._swiftObject
let entryName = swiftEntry["name"] as! String
let entryValue = swiftEntry["value"] as? String
return URLQueryItem(name: entryName, value: entryValue)
}
return mapQueryItemsFromArray(array: queryArray)
}
set(new) {
guard let new = new else {
self.percentEncodedQuery = nil
return
}

// The CFURL implementation requires two CFArrays, one for names and one for values
var names = [CFTypeRef]()
var values = [CFTypeRef]()
for entry in new {
names.append(entry.name._cfObject)
if let v = entry.value {
values.append(v._cfObject)
} else {
values.append(kCFNull)
}
let items = mapURLQueryItemArrayToCFArrays(array: new)
_CFURLComponentsSetQueryItems(_components, items.names._cfObject, items.values._cfObject)
}
}

open var percentEncodedQueryItems: [URLQueryItem]? {
get {
guard let queryArray = _CFURLComponentsCopyPercentEncodedQueryItems(_components) else {
return nil
}

return mapQueryItemsFromArray(array: queryArray)
}
set(new) {
guard let new = new else {
self.percentEncodedQuery = nil
return
}
_CFURLComponentsSetQueryItems(_components, names._cfObject, values._cfObject)

let items = mapURLQueryItemArrayToCFArrays(array: new)
_CFURLComponentsSetPercentEncodedQueryItems(_components, items.names._cfObject, items.values._cfObject)
}
}

}

extension NSURLComponents: _StructTypeBridgeable {
Expand Down
10 changes: 9 additions & 1 deletion Sources/Foundation/URLComponents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,15 @@ public struct URLComponents : ReferenceConvertible, Hashable, Equatable, _Mutabl
set { _applyMutation { $0.queryItems = newValue } }
}

public func hash(into hasher: inout Hasher) {
/// 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
///
/// 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`.
public var percentEncodedQueryItems: [URLQueryItem]? {
get { return _handle.map { $0.percentEncodedQueryItems } }
set { _applyMutation { $0.percentEncodedQueryItems = newValue } }
}

public func hash(into hasher: inout Hasher) {
hasher.combine(_handle.map { $0 })
}

Expand Down
34 changes: 34 additions & 0 deletions Tests/Foundation/Tests/TestURLComponents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@ class TestURLComponents: XCTestCase {
XCTAssertEqual(["bar": "baz"], query)
}

func test_percentEncodedQueryItems() {
let urlString = "http://localhost:8080/foo?feed%20me=feed%20me"
let url = URL(string: urlString)!

let components = URLComponents(url: url, resolvingAgainstBaseURL: false)

var query = [String: String]()
components?.percentEncodedQueryItems?.forEach {
query[$0.name] = $0.value ?? ""
}
XCTAssertEqual(["feed%20me": "feed%20me"], query)
}


func test_string() {
for obj in getTestData()! {
let testDict = obj as! [String: Any]
Expand Down Expand Up @@ -207,6 +221,24 @@ class TestURLComponents: XCTestCase {
XCTAssertEqual(urlComponents.queryItems?.count, 4)
}

func test_createURLWithComponentsPercentEncoded() {
let urlComponents = NSURLComponents()
urlComponents.scheme = "https";
urlComponents.host = "com.test.swift";
urlComponents.path = "/test/path";
let query = URLQueryItem(name: "simple%20string", value: "true%20is%20false")
urlComponents.percentEncodedQueryItems = [query]
XCTAssertNotNil(urlComponents.url?.query)
XCTAssertEqual(urlComponents.queryItems?.count, 1)
XCTAssertEqual(urlComponents.percentEncodedQueryItems?.count, 1)
guard let item = urlComponents.percentEncodedQueryItems?[0] else {
XCTFail("first element is missing")
return
}
XCTAssertEqual(item.name, "simple%20string")
XCTAssertEqual(item.value, "true%20is%20false")
}

func test_path() {
let c1 = URLComponents()
XCTAssertEqual(c1.path, "")
Expand Down Expand Up @@ -250,12 +282,14 @@ class TestURLComponents: XCTestCase {
static var allTests: [(String, (TestURLComponents) -> () throws -> Void)] {
return [
("test_queryItems", test_queryItems),
("test_percentEncodedQueryItems", test_percentEncodedQueryItems),
("test_string", test_string),
("test_port", test_portSetter),
("test_url", test_url),
("test_copy", test_copy),
("test_hash", test_hash),
("test_createURLWithComponents", test_createURLWithComponents),
("test_createURLWithComponentsPercentEncoded", test_createURLWithComponentsPercentEncoded),
("test_path", test_path),
("test_percentEncodedPath", test_percentEncodedPath),
]
Expand Down