diff --git a/Sources/S3Client/String+asS3ObjectIdentifier.swift b/Sources/S3Client/String+asS3ObjectIdentifier.swift index 0b825543..1524f2a4 100644 --- a/Sources/S3Client/String+asS3ObjectIdentifier.swift +++ b/Sources/S3Client/String+asS3ObjectIdentifier.swift @@ -13,6 +13,7 @@ public struct S3ObjectIdentifer: Equatable { internal static let s3Prefix = "s3://" internal static let httpsPrefix = "https://" internal static let httpPrefix = "http://" + internal static let s3EndpointRegex = #"^https?:\/\/(.+\.)?s3[.-][a-z0-9-]+\."# public let bucketName: String public let keyPath: String @@ -41,43 +42,46 @@ public extension String { let nonPrefixedUrl = self.dropFirst(S3ObjectIdentifer.s3Prefix.count) return asS3ObjectIdentifierFromNonPrefixedUrl(nonPrefixedUrl: nonPrefixedUrl) - } else if self.starts(with: S3ObjectIdentifer.httpsPrefix) { - // get the url without the scheme - of the form {host}/{bucket}/{key+} - let droppedPrefix = self.dropFirst(S3ObjectIdentifer.httpsPrefix.count) - - // get the index of the separator between the host and the bucket - guard let nextUrlSeparator = getIndexOfNextUrlSeparator(url: droppedPrefix) else { - return nil - } - - let bucketStartIndex = droppedPrefix.index(nextUrlSeparator, - offsetBy: 1) - // get the url without the scheme or the host - - // of the form {bucket}/{key+} - let nonPrefixedUrl = droppedPrefix[bucketStartIndex...] - - return asS3ObjectIdentifierFromNonPrefixedUrl(nonPrefixedUrl: nonPrefixedUrl) - } else if self.starts(with: S3ObjectIdentifer.httpPrefix) { - // get the url without the scheme - of the form {host}/{bucket}/{key+} - let droppedPrefix = self.dropFirst(S3ObjectIdentifer.httpPrefix.count) - - // get the index of the separator between the host and the bucket - guard let nextUrlSeparator = getIndexOfNextUrlSeparator(url: droppedPrefix) else { - return nil - } - - let bucketStartIndex = droppedPrefix.index(nextUrlSeparator, - offsetBy: 1) - // get the url without the scheme or the host - - // of the form {bucket}/{key+} - let nonPrefixedUrl = droppedPrefix[bucketStartIndex...] - - return asS3ObjectIdentifierFromNonPrefixedUrl(nonPrefixedUrl: nonPrefixedUrl) + } else if self.starts(with: S3ObjectIdentifer.httpsPrefix) || self.starts(with: S3ObjectIdentifer.httpPrefix) { + return asS3ObjectIdentifierFromHttpOrHttps() } return nil } + /// Tries to parse the bucket and key names from an HTTP or HTTPS URL. + private func asS3ObjectIdentifierFromHttpOrHttps() -> S3ObjectIdentifer? { + guard let url = URL(string: self) else { + return nil + } + + let urlPath = url.path.dropFirst() + + guard let regex = try? NSRegularExpression(pattern: S3ObjectIdentifer.s3EndpointRegex, options: []) else { + return nil + } + + let searchRange = NSRange(self.startIndex.. 0, + let bucketRange = Range(match.range(at: 1), in: self), + !bucketRange.isEmpty, + self[bucketRange].count > 1 { + // If the capture group for the regex is not empty, the URL is the virtual hosted style, for example: + // https://bucket.s3.amazonaws.com/key + // The capture group is the bucket name (with trailing dot) and the URL path is the key name + let bucketName = String(self[bucketRange].dropLast()) + let keyName = String(urlPath) + return S3ObjectIdentifer(bucketName: bucketName, keyPath: keyName) + } + + // If the regex capture group is empty, the URL is in the path style, for example: + // https://s3.amazonaws.com/bucket/key + // Both the bucket and key names are in the URL path + return asS3ObjectIdentifierFromNonPrefixedUrl(nonPrefixedUrl: Substring(urlPath)) + } + private func getIndexOfNextUrlSeparator(url: Substring) -> String.Index? { #if swift(>=4.2) return url.firstIndex(of: "/") diff --git a/Tests/S3ClientTests/S3ClientTests.swift b/Tests/S3ClientTests/S3ClientTests.swift index 5625c419..1451daa4 100644 --- a/Tests/S3ClientTests/S3ClientTests.swift +++ b/Tests/S3ClientTests/S3ClientTests.swift @@ -26,25 +26,45 @@ class S3ClientTests: XCTestCase { } func testValidHttpsUri() throws { - let s3Uri = "https://host/bucketName/the/key/path" - - let identifier = s3Uri.asS3ObjectIdentifier() + let s3Uris = [ + "https://s3.amazonaws.com/bucketName/the/key/path", + "https://s3-abc.amazonaws.com/bucketName/the/key/path", + "https://s3.us-east-1.amazonaws.com/bucketName/the/key/path", + "https://s3-abc.us-east-1.amazonaws.com/bucketName/the/key/path", + "https://bucketName.s3.amazonaws.com/the/key/path", + "https://bucketName.s3-abc.amazonaws.com/the/key/path", + "https://bucketName.s3.us-east-1.amazonaws.com/the/key/path", + "https://bucketName.s3-abc.us-east-1.amazonaws.com/the/key/path", + ] let expected = S3ObjectIdentifer(bucketName: "bucketName", keyPath: "the/key/path") - XCTAssertEqual(expected, identifier) + for s3Uri in s3Uris { + let identifier = s3Uri.asS3ObjectIdentifier() + XCTAssertEqual(expected, identifier) + } } func testValidHttpUri() throws { - let s3Uri = "http://host/bucketName/the/key/path" - - let identifier = s3Uri.asS3ObjectIdentifier() + let s3Uris = [ + "http://s3.amazonaws.com/bucketName/the/key/path", + "http://s3-abc.amazonaws.com/bucketName/the/key/path", + "http://s3.us-east-1.amazonaws.com/bucketName/the/key/path", + "http://s3-abc.us-east-1.amazonaws.com/bucketName/the/key/path", + "http://bucketName.s3.amazonaws.com/the/key/path", + "http://bucketName.s3-abc.amazonaws.com/the/key/path", + "http://bucketName.s3.us-east-1.amazonaws.com/the/key/path", + "http://bucketName.s3-abc.us-east-1.amazonaws.com/the/key/path", + ] let expected = S3ObjectIdentifer(bucketName: "bucketName", keyPath: "the/key/path") - XCTAssertEqual(expected, identifier) + for s3Uri in s3Uris { + let identifier = s3Uri.asS3ObjectIdentifier() + XCTAssertEqual(expected, identifier) + } } func testInvalidS3UriPrefix() throws {