Skip to content

Commit d64f5c1

Browse files
SWIFT-1407 / SWIFT-1174 ipv4 address and UNIX domain socket support (#711)
1 parent 2c74066 commit d64f5c1

File tree

2 files changed

+59
-28
lines changed

2 files changed

+59
-28
lines changed

Sources/MongoSwift/MongoConnectionString.swift

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public struct MongoConnectionString: Codable, LosslessStringConvertible {
9292
return port
9393
}
9494

95-
private enum HostType: String {
95+
internal enum HostType: String {
9696
case ipv4
9797
case ipLiteral = "ip_literal"
9898
case hostname
@@ -105,7 +105,7 @@ public struct MongoConnectionString: Codable, LosslessStringConvertible {
105105
/// The port number.
106106
public let port: UInt16?
107107

108-
private let type: HostType
108+
internal let type: HostType
109109

110110
/// Initializes a ServerAddress, using the default localhost:27017 if a host/port is not provided.
111111
internal init(_ hostAndPort: String = "localhost:27017") throws {
@@ -114,7 +114,6 @@ public struct MongoConnectionString: Codable, LosslessStringConvertible {
114114
}
115115

116116
// Check if host is an IPv6 literal.
117-
// TODO: SWIFT-1407: support IPv4 address parsing.
118117
if hostAndPort.first == "[" {
119118
let ipLiteralRegex = try NSRegularExpression(pattern: #"^\[(.*)\](?::([0-9]+))?$"#)
120119
guard
@@ -135,18 +134,29 @@ public struct MongoConnectionString: Codable, LosslessStringConvertible {
135134
self.type = .ipLiteral
136135
} else {
137136
let parts = hostAndPort.components(separatedBy: ":")
138-
self.host = String(parts[0])
139137
guard parts.count <= 2 else {
140138
throw MongoError.InvalidArgumentError(
141139
message: "expected only a single port delimiter ':' in \(hostAndPort)"
142140
)
143141
}
142+
143+
let host = parts[0]
144+
if host.hasSuffix(".sock") {
145+
self.host = try host.getPercentDecoded(forKey: "UNIX domain socket")
146+
self.type = .unixDomainSocket
147+
} else if host.isIPv4() {
148+
self.host = host
149+
self.type = .ipv4
150+
} else {
151+
self.host = try host.getPercentDecoded(forKey: "hostname")
152+
self.type = .hostname
153+
}
154+
144155
if parts.count > 1 {
145156
self.port = try HostIdentifier.parsePort(from: parts[1])
146157
} else {
147158
self.port = nil
148159
}
149-
self.type = .hostname
150160
}
151161
}
152162

@@ -420,7 +430,6 @@ public struct MongoConnectionString: Codable, LosslessStringConvertible {
420430
throw MongoError.InvalidArgumentError(message: "Invalid connection string")
421431
}
422432
let identifiersAndOptions = schemeAndRest[1].components(separatedBy: "/")
423-
// TODO: SWIFT-1174: handle unescaped slashes in unix domain sockets.
424433
guard identifiersAndOptions.count <= 2 else {
425434
throw MongoError.InvalidArgumentError(
426435
message: "Connection string contains an unescaped slash"
@@ -484,6 +493,9 @@ public struct MongoConnectionString: Codable, LosslessStringConvertible {
484493
self.defaultAuthDB = decoded
485494
// If no other authentication options were provided, we should use the defaultAuthDB as the credential
486495
// source. This will be overwritten later if an authSource is provided.
496+
if self.credential == nil {
497+
self.credential = MongoCredential()
498+
}
487499
self.credential?.source = decoded
488500
}
489501

@@ -1033,6 +1045,19 @@ extension StringProtocol {
10331045
return decoded
10341046
}
10351047

1048+
fileprivate func isIPv4() -> Bool {
1049+
let numbers = self.components(separatedBy: ".")
1050+
guard numbers.count == 4 else {
1051+
return false
1052+
}
1053+
for number in numbers {
1054+
guard let n = Int(number), (0...255).contains(n) else {
1055+
return false
1056+
}
1057+
}
1058+
return true
1059+
}
1060+
10361061
fileprivate func getValidatedUserInfo(forKey key: String) throws -> String {
10371062
for character in MongoConnectionString.forbiddenUserInfoCharacters {
10381063
if self.contains(character) {

Tests/MongoSwiftTests/ConnectionStringTests.swift

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,6 @@ let skipUnsupported: [String: [String]] = [
8181
"valid-options.json": ["option names are normalized to lowercase"] // we don't support MONGODB-CR authentication
8282
]
8383

84-
// Tests for options that are not yet supported on MongoConnectionString.
85-
// TODO: delete this when MongoConnectionString is fully implemented
86-
let skipMongoConnectionStringUnsupported: [String: [String]] = [
87-
// TODO: SWIFT-1174: unskip these tests
88-
"valid-db-with-dotted-name.json": ["*"],
89-
"valid-unix_socket-absolute.json": ["*"],
90-
"valid-unix_socket-relative.json": ["*"]
91-
]
92-
9384
func shouldSkip(file: String, test: String) -> Bool {
9485
if let skipList = skipUnsupported[file],
9586
skipList.contains(where: test.lowercased().contains) || skipList.contains("*")
@@ -100,24 +91,12 @@ func shouldSkip(file: String, test: String) -> Bool {
10091
return false
10192
}
10293

103-
func shouldSkipMongoConnectionString(file: String, test: String) -> Bool {
104-
if let skipList = skipMongoConnectionStringUnsupported[file],
105-
skipList.contains(where: test.lowercased().contains) || skipList.contains("*")
106-
{
107-
return true
108-
}
109-
110-
return false
111-
}
112-
11394
final class ConnectionStringTests: MongoSwiftTestCase {
11495
func runTests(_ specName: String) throws {
11596
let testFiles = try retrieveSpecTestFiles(specName: specName, asType: ConnectionStringTestFile.self)
11697
for (filename, file) in testFiles {
11798
for testCase in file.tests {
118-
guard !shouldSkip(file: filename, test: testCase.description)
119-
&& !shouldSkipMongoConnectionString(file: filename, test: testCase.description)
120-
else {
99+
guard !shouldSkip(file: filename, test: testCase.description) else {
121100
continue
122101
}
123102

@@ -602,4 +581,31 @@ final class ConnectionStringTests: MongoSwiftTestCase {
602581
expect(try ConnectionString("mongodb://localhost:27017/?directConnection=true", options: opts))
603582
.to(throwError(errorType: MongoError.InvalidArgumentError.self))
604583
}
584+
585+
func testIPv4AddressParsing() throws {
586+
// valid IPv4
587+
let connString1 = try MongoConnectionString(throwsIfInvalid: "mongodb://1.2.3.4")
588+
guard let host = connString1.hosts.first else {
589+
XCTFail("connection string should contain one host")
590+
return
591+
}
592+
expect(host.host).to(equal("1.2.3.4"))
593+
expect(host.type).to(equal(.ipv4))
594+
// invalid IPv4 should fall back to hostname (only three numbers)
595+
let connString2 = try MongoConnectionString(throwsIfInvalid: "mongodb://1.2.3")
596+
guard let host = connString2.hosts.first else {
597+
XCTFail("connection string should contain one host")
598+
return
599+
}
600+
expect(host.host).to(equal("1.2.3"))
601+
expect(host.type).to(equal(.hostname))
602+
// invalid IPv4 should fall back to hostname (numbers out of bounds)
603+
let connString3 = try MongoConnectionString(throwsIfInvalid: "mongodb://256.1.2.3")
604+
guard let host = connString3.hosts.first else {
605+
XCTFail("connection string should contain one host")
606+
return
607+
}
608+
expect(host.host).to(equal("256.1.2.3"))
609+
expect(host.type).to(equal(.hostname))
610+
}
605611
}

0 commit comments

Comments
 (0)