Skip to content

Commit

Permalink
Add URITests test cases with encoded and unencoded reserved characters
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrewFossAWS committed May 24, 2024
1 parent 8ea7af9 commit d61a21b
Show file tree
Hide file tree
Showing 2 changed files with 161 additions and 6 deletions.
62 changes: 56 additions & 6 deletions Sources/ClientRuntime/Message/URI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,29 @@ public final class URIBuilder {
return self
}

/// According to https://developer.apple.com/documentation/foundation/nsurlcomponents/1408161-percentencodedpath
/// "Although an unencoded semicolon is a valid character in a percent-encoded path,
/// for compatibility with the NSURL class, you should always percent-encode it."
///
/// URI also always return a percent-encoded path.
/// If an percent-encoded path is provided, we will replace the semicolon with %3B in the path.
/// If an unencoded path is provided, we should percent-encode the path including semicolon.
@discardableResult
public func withPath(_ value: String) -> URIBuilder {
if value.isPercentEncoded {
self.urlComponents.percentEncodedPath = value
if value.contains(";") {
let encodedPath = value.replacingOccurrences(
of: ";", with: "%3B", options: NSString.CompareOptions.literal, range: nil)
self.urlComponents.percentEncodedPath = encodedPath
} else {
self.urlComponents.percentEncodedPath = value
}
} else {
self.urlComponents.path = value
if value.contains(";") {
self.urlComponents.percentEncodedPath = value.percentEncodePathIncludingSemicolon()
} else {
self.urlComponents.path = value
}
}
return self
}
Expand Down Expand Up @@ -118,7 +135,14 @@ public final class URIBuilder {

@discardableResult
public func withQueryItems(_ value: [SDKURLQueryItem]) -> URIBuilder {
self.urlComponents.percentEncodedQueryItems = value.isEmpty ? nil : value.toURLQueryItems()
if value.isEmpty {
return self
}
if value.containsPercentEncode() {
self.urlComponents.percentEncodedQueryItems = value.toURLQueryItems()
} else {
self.urlComponents.queryItems = value.toURLQueryItems()
}
return self
}

Expand All @@ -129,7 +153,13 @@ public final class URIBuilder {
}
var queryItems = self.urlComponents.percentEncodedQueryItems ?? []
queryItems += items.toURLQueryItems()
self.urlComponents.percentEncodedQueryItems = queryItems

if queryItems.containsPercentEncode() {
self.urlComponents.percentEncodedQueryItems = queryItems
} else {
self.urlComponents.queryItems = queryItems
}

return self
}

Expand Down Expand Up @@ -201,6 +231,18 @@ extension String {
let decoded = self.removingPercentEncoding
return decoded != nil && decoded != self
}

public func percentEncodePathIncludingSemicolon() -> String {
let allowed =
// swiftlint:disable:next force_cast
(CharacterSet.urlPathAllowed as NSCharacterSet).mutableCopy() as! NSMutableCharacterSet
allowed.removeCharacters(in: ";")
return self.addingPercentEncoding(withAllowedCharacters: allowed as CharacterSet)!
}

public func percentEncodeQuery() -> String {
return self.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed as CharacterSet)!
}
}

extension Array where Element == SDKURLQueryItem {
Expand All @@ -214,10 +256,18 @@ extension Array where Element == SDKURLQueryItem {
public func toURLQueryItems() -> [URLQueryItem] {
return self.map { URLQueryItem(name: $0.name, value: $0.value) }
}

public func containsPercentEncode() -> Bool {
return self.contains { item in
return item.name.isPercentEncoded || (item.value?.isPercentEncoded ?? false)
}
}
}

extension Array where Element == URLQueryItem {
public func toSDKURLQueryItems() -> [SDKURLQueryItem] {
return self.map { SDKURLQueryItem(name: $0.name, value: $0.value) }
public func containsPercentEncode() -> Bool {
return self.contains { item in
return item.name.isPercentEncoded || (item.value?.isPercentEncoded ?? false)
}
}
}
105 changes: 105 additions & 0 deletions Tests/ClientRuntimeTests/MessageTests/URITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import XCTest
class URITests: XCTestCase {
let url = URL(string: "https://xctest.amazonaws.com?abc=def&ghi=jkl&mno=pqr")!

let unencodedReservedCharacters: String = "!$&'()*+,;="

let encodedReservedCharacters: String = "%21%24%26%27%28%29%2A%2B%2C%3B%3D"

func test_queryItems_setsQueryItemsFromURLInOrder() throws {
let uri = URIBuilder()
.withScheme(Scheme(rawValue: url.scheme!)!)
Expand Down Expand Up @@ -91,4 +95,105 @@ class URITests: XCTestCase {
XCTAssertEqual(uri.url?.absoluteString,
"https://dan%21:%24008@+xctest2.com/x%2Dy%2Dz?abc=def&ghi=jkl&mno=pqr&test=1%2B2#fragment%21")
}

func test_host_unencodedReservedCharacters() throws {
let uri = URIBuilder()
.withScheme(.https)
.withHost("xctest.\(unencodedReservedCharacters).com")
.withPath("/")
.build()
XCTAssertEqual(uri.url?.absoluteString, "https://xctest.!$&\'()*+,;=.com/")
}

func test_host_encodedReservedCharacters() throws {
let uri = URIBuilder()
.withScheme(.https)
.withHost("xctest.\(encodedReservedCharacters).com")
.withPath("/")
.build()
XCTAssertEqual(uri.url?.absoluteString, "https://xctest.!$&\'()*+,;=.com/")
}

func test_host_encodedAndUnencodedReservedCharacters() throws {
let uri = URIBuilder()
.withScheme(.https)
.withHost("xctest.\(unencodedReservedCharacters)\(encodedReservedCharacters).com")
.withPath("/")
.build()
XCTAssertEqual(uri.url?.absoluteString, "https://xctest.!$&\'()*+,;=!$&\'()*+,;=.com/")
}

func test_path_unencodedReservedCharacters() throws {
let uri = URIBuilder()
.withScheme(.https)
.withHost("xctest.com")
.withPath("/:@\(unencodedReservedCharacters)")
.build()
XCTAssertEqual(uri.url?.absoluteString, "https://xctest.com/:@!$&\'()*+,%3B=")
}

func test_path_encodedReservedCharacters() throws {
let uri = URIBuilder()
.withScheme(.https)
.withHost("xctest.com")
.withPath("/\(encodedReservedCharacters)")
.build()
XCTAssertEqual(uri.url?.absoluteString, "https://xctest.com/%21%24%26%27%28%29%2A%2B%2C%3B%3D")
}

func test_path_encodedAndUnencodedReservedCharacters() throws {
let uri = URIBuilder()
.withScheme(.https)
.withHost("xctest.com")
.withPath("/:@\(unencodedReservedCharacters)\(encodedReservedCharacters)")
.build()
XCTAssertEqual(uri.url?.absoluteString, "https://xctest.com/:@!$&\'()*+,%3B=%21%24%26%27%28%29%2A%2B%2C%3B%3D")
}

func test_query_unencodedReservedCharacters() throws {
let uri = URIBuilder()
.withScheme(.https)
.withHost("xctest.com")
.withPath("/")
.withQueryItems([
SDKURLQueryItem(
name: "key:@\(unencodedReservedCharacters))",
value: "value:@\(unencodedReservedCharacters)"
),
])
.build()
XCTAssertEqual(uri.url?.absoluteString, "https://xctest.com/?key:@!$%26\'()*+,;%3D)=value:@!$%26\'()*+,;%3D")
}

func test_query_encodedReservedCharacters() throws {
let uri = URIBuilder()
.withScheme(.https)
.withHost("xctest.com")
.withPath("/")
.withQueryItems([
SDKURLQueryItem(
name: "key:@\(encodedReservedCharacters))",
value: "value:@\(encodedReservedCharacters)"
),
])
.build()
XCTAssertEqual(uri.url?.absoluteString,
"https://xctest.com/?key:@%21%24%26%27%28%29%2A%2B%2C%3B%3D)=value:@%21%24%26%27%28%29%2A%2B%2C%3B%3D")
}

func test_query_unencodedAndEncodedReservedCharacters() throws {
let uri = URIBuilder()
.withScheme(.https)
.withHost("xctest.com")
.withPath("/")
.withQueryItems([
SDKURLQueryItem(
name: "key:@\(encodedReservedCharacters))",
value: "value:@\(unencodedReservedCharacters)"
),
])
.build()
XCTAssertEqual(uri.url?.absoluteString,
"https://xctest.com/?key:@%21%24%26%27%28%29%2A%2B%2C%3B%3D)=value:@!$&\'()*+,;=")
}
}

0 comments on commit d61a21b

Please sign in to comment.