Skip to content

Commit 96cb3d6

Browse files
authored
fix: RFC3339 date parsing (#2119)
* Add handling for miliseconds in RFC3339 date parsing across identity reoslvers. * Add swift file extension. * Remove stale reference * Add newline at end of file. * Fix swift 6 compilation errors & add unit tests. * Add preconcurrency import for ISO8601DateFormatter --------- Co-authored-by: Sichan Yoo <chanyoo@amazon.com>
1 parent 7d47315 commit 96cb3d6

7 files changed

Lines changed: 65 additions & 22 deletions

File tree

Sources/Core/AWSSDKIdentity/Sources/AWSSDKIdentity/AWSCredentialIdentityResolvers/ECSAWSCredentialIdentityResolver.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,7 @@ private struct JSONCredentialResponse: Codable {
218218

219219
// Handle the Expiration field which is a string in ISO8601 format.
220220
if let expirationString = try container.decodeIfPresent(String.self, forKey: .expiration) {
221-
let formatter = ISO8601DateFormatter()
222-
expiration = formatter.date(from: expirationString)
221+
expiration = RFC3339DateParser.parse(expirationString)
223222
} else {
224223
expiration = nil
225224
}

Sources/Core/AWSSDKIdentity/Sources/AWSSDKIdentity/AWSCredentialIdentityResolvers/IMDSAWSCredentialIdentityResolver.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,12 +156,11 @@ private struct JSONCredentialResponse: Codable {
156156

157157
init(from decoder: Decoder) throws {
158158
let container = try decoder.container(keyedBy: CodingKeys.self)
159-
let formatter = ISO8601DateFormatter()
160159

161160
code = try container.decode(String.self, forKey: .code)
162161

163162
let lastUpdatedString = try container.decode(String.self, forKey: .lastUpdated)
164-
guard let lastUpdatedDate = formatter.date(from: lastUpdatedString) else {
163+
guard let lastUpdatedDate = RFC3339DateParser.parse(lastUpdatedString) else {
165164
throw DecodingError.dataCorruptedError(forKey: .lastUpdated,
166165
in: container,
167166
debugDescription: "Invalid ISO8601 date string: \(lastUpdatedString)")
@@ -174,7 +173,7 @@ private struct JSONCredentialResponse: Codable {
174173
token = try container.decode(String.self, forKey: .token)
175174

176175
let expirationString = try container.decode(String.self, forKey: .expiration)
177-
guard let expirationDate = formatter.date(from: expirationString) else {
176+
guard let expirationDate = RFC3339DateParser.parse(expirationString) else {
178177
throw DecodingError.dataCorruptedError(forKey: .expiration,
179178
in: container,
180179
debugDescription: "Invalid ISO8601 date string: \(expirationString)")

Sources/Core/AWSSDKIdentity/Sources/AWSSDKIdentity/AWSCredentialIdentityResolvers/LoginAWSCredentialIdentityResolver.swift

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -143,17 +143,11 @@ public struct LoginAWSCredentialIdentityResolver: AWSCredentialIdentityResolver
143143
decoder.dateDecodingStrategy = .custom { decoder in
144144
let container = try decoder.singleValueContainer()
145145
let dateString = try container.decode(String.self)
146-
let formatter = ISO8601DateFormatter()
147-
// Powershell saves expiration date with milliseconds.
148-
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
149-
if let date = formatter.date(from: dateString) {
150-
return date
151-
}
152-
// AWS CLI saves expiration date without milliseconds.
153-
// Because setting .withFractionalSeconds is a strict requirement, must remove it for AWS CLI expiration date.
154-
formatter.formatOptions = [.withInternetDateTime]
155-
guard let date = formatter.date(from: dateString) else {
156-
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date format")
146+
guard let date = RFC3339DateParser.parse(dateString) else {
147+
throw DecodingError.dataCorruptedError(
148+
in: container,
149+
debugDescription: "Invalid RFC3339 date format"
150+
)
157151
}
158152
return date
159153
}

Sources/Core/AWSSDKIdentity/Sources/AWSSDKIdentity/AWSCredentialIdentityResolvers/ProcessAWSCredentialIdentityResolver.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,7 @@ private struct ProcessJSONCredentialResponse: Codable {
187187

188188
// Handle the Expiration field which is a string in ISO8601 format.
189189
if let expirationString = try container.decodeIfPresent(String.self, forKey: .expiration) {
190-
let formatter = ISO8601DateFormatter()
191-
expiration = formatter.date(from: expirationString)
190+
expiration = RFC3339DateParser.parse(expirationString)
192191
} else {
193192
expiration = nil
194193
}

Sources/Core/AWSSDKIdentity/Sources/AWSSDKIdentity/BearerTokenIdentityResolvers/SSOBearerTokenIdentityResolver.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -236,10 +236,8 @@ struct SSOToken: Codable {
236236
// Required fields.
237237

238238
accessToken = try container.decode(String.self, forKey: .accessToken)
239-
let dateFormatter = ISO8601DateFormatter()
240-
dateFormatter.formatOptions = [.withInternetDateTime]
241239
let expiresAtString = try container.decode(String.self, forKey: .expiresAt)
242-
guard let expiresAtDate = dateFormatter.date(from: expiresAtString) else {
240+
guard let expiresAtDate = RFC3339DateParser.parse(expiresAtString) else {
243241
throw DecodingError.dataCorruptedError(
244242
forKey: .expiresAt,
245243
in: container,
@@ -259,7 +257,7 @@ struct SSOToken: Codable {
259257
String.self,
260258
forKey: .registrationExpiresAt
261259
) {
262-
registrationExpiresAt = dateFormatter.date(from: registrationExpiresAtString)
260+
registrationExpiresAt = RFC3339DateParser.parse(registrationExpiresAtString)
263261
} else {
264262
registrationExpiresAt = nil
265263
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// ISO8601DateFormatter isn't Sendable yet.
2+
@preconcurrency import Foundation
3+
4+
public enum RFC3339DateParser {
5+
private static let withFractional: ISO8601DateFormatter = {
6+
let f = ISO8601DateFormatter()
7+
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
8+
return f
9+
}()
10+
11+
private static let withoutFractional: ISO8601DateFormatter = {
12+
let f = ISO8601DateFormatter()
13+
f.formatOptions = [.withInternetDateTime]
14+
return f
15+
}()
16+
17+
public static func parse(_ string: String) -> Date? {
18+
withoutFractional.date(from: string)
19+
?? withFractional.date(from: string)
20+
}
21+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import XCTest
9+
import enum AWSSDKIdentity.RFC3339DateParser
10+
11+
class RFC3339DateParserTests: XCTestCase {
12+
func testWithFractionalSecond() {
13+
let dateString = "2024-07-15T12:30:45.123Z"
14+
let parsedDate = RFC3339DateParser.parse(dateString)
15+
// 2024-07-15T12:30:45.123Z = 1721046645.123 seconds since Unix epoch
16+
let expectedDate = Date(timeIntervalSince1970: 1_721_046_645.123)
17+
XCTAssertEqual(expectedDate, parsedDate)
18+
}
19+
20+
func testWithoutFractionalSecond() {
21+
let dateString = "2024-07-15T12:30:45Z"
22+
let parsedDate = RFC3339DateParser.parse(dateString)
23+
// 2024-07-15T12:30:45Z = 1721046645 seconds since Unix epoch
24+
let expectedDate = Date(timeIntervalSince1970: 1_721_046_645)
25+
XCTAssertEqual(expectedDate, parsedDate)
26+
}
27+
28+
func testInvalidDateReturnsNil() {
29+
let dateString = "not-a-date"
30+
let parsedDate = RFC3339DateParser.parse(dateString)
31+
XCTAssertNil(parsedDate)
32+
}
33+
}

0 commit comments

Comments
 (0)