Skip to content

Commit

Permalink
Fix 5.9/5.10
Browse files Browse the repository at this point in the history
  • Loading branch information
adam-fowler committed Jan 7, 2025
1 parent 08b2906 commit 5761ec6
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 26 deletions.
6 changes: 3 additions & 3 deletions Sources/Hummingbird/HTTP/Cookie.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public struct Cookie: Sendable, CustomStringConvertible {
public let properties: Properties

/// indicates the maximum lifetime of the cookie
public var expires: Date? { self.properties[.expires].flatMap { try? Date($0, strategy: .rfc1123) } }
public var expires: Date? { self.properties[.expires].flatMap { Date(httpHeaderDate: $0) } }
/// indicates the maximum lifetime of the cookie in seconds. Max age has precedence over expires
/// (not all user agents support max-age)
public var maxAge: Int? { self.properties[.maxAge].map { Int($0) } ?? nil }
Expand Down Expand Up @@ -75,7 +75,7 @@ public struct Cookie: Sendable, CustomStringConvertible {
self.name = name
self.value = value
var properties = Properties()
properties[.expires] = expires?.formatted(.rfc1123)
properties[.expires] = expires?.httpHeaderDate
properties[.maxAge] = maxAge?.description
properties[.domain] = domain
properties[.path] = path
Expand Down Expand Up @@ -110,7 +110,7 @@ public struct Cookie: Sendable, CustomStringConvertible {
self.name = name
self.value = value
var properties = Properties()
properties[.expires] = expires?.formatted(.rfc1123)
properties[.expires] = expires?.httpHeaderDate
properties[.maxAge] = maxAge?.description
properties[.domain] = domain
properties[.path] = path
Expand Down
4 changes: 2 additions & 2 deletions Sources/Hummingbird/Middleware/FileMiddleware.swift
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ extension FileMiddleware {
// content-length
headers[.contentLength] = String(describing: attributes.size)
// modified-date
let modificationDateString = attributes.modificationDate.formatted(.rfc1123)
let modificationDateString = attributes.modificationDate.httpHeaderDate
headers[.lastModified] = modificationDateString
// eTag (constructed from modification date and content size)
headers[.eTag] = eTag
Expand Down Expand Up @@ -244,7 +244,7 @@ extension FileMiddleware {
}
// verify if-modified-since
else if let ifModifiedSince = request.headers[.ifModifiedSince] {
if let ifModifiedSinceDate = try? Date(ifModifiedSince, strategy: .rfc1123) {
if let ifModifiedSinceDate = Date(httpHeaderDate: ifModifiedSince) {
// round modification date of file down to seconds for comparison
let modificationDateTimeInterval = attributes.modificationDate.timeIntervalSince1970.rounded(.down)
let ifModifiedSinceDateTimeInterval = ifModifiedSinceDate.timeIntervalSince1970
Expand Down
6 changes: 3 additions & 3 deletions Sources/Hummingbird/Utils/DateCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import Darwin.C
/// Current date formatted cache service
///
/// Getting the current date formatted is an expensive operation. This creates a task that will
/// update a cached version of the date in the format as detailed in RFC1123 once every second.
/// update a cached version of the date in the format as detailed in RFC9110 once every second.
final class DateCache: Service {
final class DateContainer: AtomicReference, Sendable {
let date: String
Expand All @@ -50,14 +50,14 @@ final class DateCache: Service {
let dateContainer: ManagedAtomic<DateContainer>

init() {
self.dateContainer = .init(.init(date: Date.now.formatted(.rfc1123)))
self.dateContainer = .init(.init(date: Date.now.httpHeaderDate))
}

public func run() async throws {
let timerSequence = AsyncTimerSequence(interval: .seconds(1), clock: .suspending)
.cancelOnGracefulShutdown()
for try await _ in timerSequence {
self.dateContainer.store(.init(date: Date.now.formatted(.rfc1123)), ordering: .releasing)
self.dateContainer.store(.init(date: Date.now.httpHeaderDate), ordering: .releasing)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,27 @@
//
//===----------------------------------------------------------------------===//

#if swift(>=6.0)

#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

struct RFC1123DateParsingError: Error {}
extension Date {
init?(httpHeaderDate: String) {
try? self.init(httpHeaderDate, strategy: .rfc9110)
}

var httpHeaderDate: String {
self.formatted(.rfc9110)
}
}

struct RFC9110DateParsingError: Error {}

struct RFC1123FormatStyle {
struct RFC9110FormatStyle {

let calendar: Calendar

Expand All @@ -31,13 +43,13 @@ struct RFC1123FormatStyle {
}
}

extension RFC1123FormatStyle: ParseStrategy {
extension RFC9110FormatStyle: ParseStrategy {
func parse(_ input: String) throws -> Date {
guard let components = self.components(from: input) else {
throw RFC1123DateParsingError()
throw RFC9110DateParsingError()
}
guard let date = components.date else {
throw RFC1123DateParsingError()
throw RFC9110DateParsingError()
}
return date
}
Expand Down Expand Up @@ -266,7 +278,7 @@ let timezoneOffsetMap: [[UInt8]: Int] = [
Array("PDT".utf8): -7 * 60,
]

extension RFC1123FormatStyle: FormatStyle {
extension RFC9110FormatStyle: FormatStyle {
//let calendar: Calendar

func format(_ value: Date) -> String {
Expand Down Expand Up @@ -310,10 +322,75 @@ extension RFC1123FormatStyle: FormatStyle {
]
}

extension FormatStyle where Self == RFC1123FormatStyle {
static var rfc1123: Self { .init() }
extension FormatStyle where Self == RFC9110FormatStyle {
static var rfc9110: Self { .init() }
}

extension ParseStrategy where Self == RFC1123FormatStyle {
static var rfc1123: Self { .init() }
extension ParseStrategy where Self == RFC9110FormatStyle {
static var rfc9110: Self { .init() }
}

#else

import Foundation

extension Date {
init?(httpHeaderDate: String) {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "EEE, dd MMM yyy HH:mm:ss z"
formatter.timeZone = TimeZone(secondsFromGMT: 0)
guard let date = formatter.date(from: httpHeaderDate) else { return nil }
self = date
}

var httpHeaderDate: String {
var epochTime = Int(self.timeIntervalSince1970)
var timeStruct = tm.init()
gmtime_r(&epochTime, &timeStruct)
let year = Int(timeStruct.tm_year + 1900)
let day = Self.dayNames[numericCast(timeStruct.tm_wday)]
let month = Self.monthNames[numericCast(timeStruct.tm_mon)]
var formatted = day
formatted.reserveCapacity(30)
formatted += ", "
formatted += Self.numberNames[numericCast(timeStruct.tm_mday)]
formatted += " "
formatted += month
formatted += " "
formatted += Self.numberNames[year / 100]
formatted += Self.numberNames[year % 100]
formatted += " "
formatted += Self.numberNames[numericCast(timeStruct.tm_hour)]
formatted += ":"
formatted += Self.numberNames[numericCast(timeStruct.tm_min)]
formatted += ":"
formatted += Self.numberNames[numericCast(timeStruct.tm_sec)]
formatted += " GMT"

return formatted
}

private static let dayNames = [
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
]

private static let monthNames = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
]

private static let numberNames = [
"00", "01", "02", "03", "04", "05", "06", "07", "08", "09",
"10", "11", "12", "13", "14", "15", "16", "17", "18", "19",
"20", "21", "22", "23", "24", "25", "26", "27", "28", "29",
"30", "31", "32", "33", "34", "35", "36", "37", "38", "39",
"40", "41", "42", "43", "44", "45", "46", "47", "48", "49",
"50", "51", "52", "53", "54", "55", "56", "57", "58", "59",
"60", "61", "62", "63", "64", "65", "66", "67", "68", "69",
"70", "71", "72", "73", "74", "75", "76", "77", "78", "79",
"80", "81", "82", "83", "84", "85", "86", "87", "88", "89",
"90", "91", "92", "93", "94", "95", "96", "97", "98", "99",
]
}

#endif // #if swift(>=6.0)
2 changes: 1 addition & 1 deletion Tests/HummingbirdTests/CookiesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ final class CookieTests: XCTestCase {

func testExpires() {
let cookie = Cookie(from: "name=value; Expires=Wed, 21 Oct 2015 07:28:00 GMT")
XCTAssertEqual(cookie?.expires, try? Date("Wed, 21 Oct 2015 07:28:00 GMT", strategy: .rfc1123))
XCTAssertEqual(cookie?.expires, Date(httpHeaderDate: "Wed, 21 Oct 2015 07:28:00 GMT"))
}

func testDomain() {
Expand Down
8 changes: 4 additions & 4 deletions Tests/HummingbirdTests/DateCacheTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import XCTest
@testable import Hummingbird

final class DateTests: XCTestCase {
func testRFC1123Renderer() {
func testHTTPHeaderDateRenderer() {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "EEE, dd MMM yyy HH:mm:ss z"
Expand All @@ -30,7 +30,7 @@ final class DateTests: XCTestCase {
let date = Date(timeIntervalSince1970: Double(time))
XCTAssertEqual(
formatter.string(from: date),
date.formatted(.rfc1123)
date.httpHeaderDate
)
}
}
Expand Down Expand Up @@ -58,8 +58,8 @@ final class DateTests: XCTestCase {
for _ in 0..<1000 {
let time = Int.random(in: 1...4 * Int(Int32.max))
let date = Date(timeIntervalSince1970: Double(time))
let string = date.formatted(.rfc1123)
let parsedDate = try Date(string, strategy: .rfc1123)
let string = date.httpHeaderDate
let parsedDate = Date(httpHeaderDate: string)
XCTAssertEqual(date, parsedDate)
}

Expand Down
6 changes: 3 additions & 3 deletions Tests/HummingbirdTests/FileMiddlewareTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ final class FileMiddlewareTests: XCTestCase {
return ByteBufferAllocator().buffer(bytes: data)
}

static var rfc1123Formatter: DateFormatter {
static var rfc9110Formatter: DateFormatter {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "EEE, d MMM yyy HH:mm:ss z"
Expand Down Expand Up @@ -185,7 +185,7 @@ final class FileMiddlewareTests: XCTestCase {
XCTAssertEqual(response.headers[.contentLength], text.utf8.count.description)
XCTAssertEqual(response.headers[.contentType], "text/plain")
let responseDateString = try XCTUnwrap(response.headers[.lastModified])
let responseDate = try XCTUnwrap(Self.rfc1123Formatter.date(from: responseDateString))
let responseDate = try XCTUnwrap(Self.rfc9110Formatter.date(from: responseDateString))
XCTAssert(date < responseDate + 2 && date > responseDate - 2)
}
}
Expand Down Expand Up @@ -264,7 +264,7 @@ final class FileMiddlewareTests: XCTestCase {
XCTAssertEqual(response.status, .notModified)
}
// one minute before current date
let date = try XCTUnwrap(Self.rfc1123Formatter.string(from: Date(timeIntervalSinceNow: -60)))
let date = try XCTUnwrap(Self.rfc9110Formatter.string(from: Date(timeIntervalSinceNow: -60)))
try await client.execute(uri: filename, method: .get, headers: [.ifModifiedSince: date]) { response in
XCTAssertEqual(response.status, .ok)
}
Expand Down

0 comments on commit 5761ec6

Please sign in to comment.