Skip to content

Commit 1ac4497

Browse files
committed
Add validation to DateTime and Date.Components initializers
Makes DateTime.init(year:month:day:...) and Date.Components.init throwing to validate component ranges per RFC 5322 requirements: - month: 1-12 - day: 1-31 (validated for specific month/year, including leap years) - hour: 0-23 - minute: 0-59 - second: 0-60 (allowing leap second) - weekday: 0-6 New error cases in RFC_5322.Date.Error: - monthOutOfRange(Int) - dayOutOfRange(Int, month: Int, year: Int) - hourOutOfRange(Int) - minuteOutOfRange(Int) - secondOutOfRange(Int) - weekdayOutOfRange(Int) Updates: - All test functions calling component-based init now marked `throws` - Added 6 comprehensive validation tests - Parse function properly propagates validation errors - Internal components computed property uses try! (safe invariant) All 158 tests pass.
1 parent d686013 commit 1ac4497

23 files changed

+2823
-328
lines changed

Package.swift

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,70 @@
11
// swift-tools-version:6.0
22

3-
import Foundation
43
import PackageDescription
54

65
extension String {
76
static let rfc5322: Self = "RFC_5322"
7+
static let rfc5322Foundation: Self = "RFC_5322_Foundation"
88
}
99

10+
extension String { var tests: Self { self + " Tests" } }
11+
1012
extension Target.Dependency {
1113
static var rfc5322: Self { .target(name: .rfc5322) }
14+
static var rfc5322Foundation: Self { .target(name: .rfc5322Foundation) }
1215
static var rfc1123: Self { .product(name: "RFC_1123", package: "swift-rfc-1123") }
16+
static var standards: Self { .product(name: "Standards", package: "swift-standards") }
17+
static var incits_4_1986: Self { .product(name: "INCITS_4_1986", package: "swift-incits-4-1986") }
18+
static var standardsTestSupport: Self { .product(name: "StandardsTestSupport", package: "swift-standards") }
1319
}
1420

1521
let package = Package(
1622
name: "swift-rfc-5322",
1723
platforms: [
18-
.macOS(.v13),
19-
.iOS(.v16)
24+
.macOS(.v15),
25+
.iOS(.v18),
26+
.tvOS(.v18),
27+
.watchOS(.v11)
2028
],
2129
products: [
2230
.library(name: .rfc5322, targets: [.rfc5322]),
31+
.library(name: .rfc5322Foundation, targets: [.rfc5322Foundation]),
2332
],
2433
dependencies: [
2534
.package(url: "https://github.com/swift-standards/swift-rfc-1123.git", from: "0.0.1"),
35+
.package(url: "https://github.com/swift-standards/swift-standards.git", from: "0.1.0"),
36+
.package(url: "https://github.com/swift-standards/swift-incits-4-1986.git", from: "0.1.0")
2637
],
2738
targets: [
2839
.target(
2940
name: .rfc5322,
3041
dependencies: [
31-
.rfc1123
42+
.rfc1123,
43+
.standards,
44+
.incits_4_1986
45+
]
46+
),
47+
.target(
48+
name: .rfc5322Foundation,
49+
dependencies: [
50+
.rfc5322
3251
]
3352
),
3453
.testTarget(
3554
name: .rfc5322.tests,
3655
dependencies: [
37-
.rfc5322
56+
.rfc5322,
57+
.incits_4_1986,
58+
.standardsTestSupport
59+
]
60+
),
61+
.testTarget(
62+
name: .rfc5322Foundation.tests,
63+
dependencies: [
64+
.rfc5322,
65+
.rfc5322Foundation
3866
]
3967
),
4068
],
4169
swiftLanguageModes: [.v6]
4270
)
43-
44-
extension String { var tests: Self { self + " Tests" } }

README.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ let message = RFC_5322.Message(
8888
body: "Hello, World!".data(using: .utf8)!
8989
)
9090

91-
// Render to RFC 5322 format
92-
let emlContent = message.render()
91+
// Convert to RFC 5322 format
92+
let emlContent = String(message)
9393
print(emlContent)
9494
// From: John Doe <[email protected]>
9595
@@ -177,9 +177,16 @@ public struct Message: Hashable, Sendable {
177177
public let additionalHeaders: [Header]
178178
public let mimeVersion: String
179179

180-
public func render() -> String
181180
public var bodyString: String?
182-
public static func generateMessageId(from: EmailAddress) -> String
181+
public static func generateMessageId(from: EmailAddress, uniqueId: String) -> String
182+
}
183+
184+
extension [UInt8] {
185+
public init(_ message: RFC_5322.Message)
186+
}
187+
188+
extension String {
189+
public init(_ message: RFC_5322.Message)
183190
}
184191
```
185192

Sources/RFC_5322/RFC 5322 Date.swift

Lines changed: 0 additions & 172 deletions
This file was deleted.

Sources/RFC_5322/RFC_5322.Date.Components.swift

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ extension RFC_5322.Date {
1717
public let second: Int // 0-60 (allowing leap second)
1818
public let weekday: Int // 0=Sunday, 6=Saturday
1919

20+
/// Creates date components with validation
21+
/// - Throws: `RFC_5322.Date.Error` if any component is out of valid range
2022
public init(
2123
year: Int,
2224
month: Int,
@@ -25,7 +27,38 @@ extension RFC_5322.Date {
2527
minute: Int,
2628
second: Int,
2729
weekday: Int
28-
) {
30+
) throws {
31+
// Validate month
32+
guard (1...12).contains(month) else {
33+
throw RFC_5322.Date.Error.monthOutOfRange(month)
34+
}
35+
36+
// Validate day for the given month and year
37+
let maxDay = Self.daysInMonth(month, year: year)
38+
guard (1...maxDay).contains(day) else {
39+
throw RFC_5322.Date.Error.dayOutOfRange(day, month: month, year: year)
40+
}
41+
42+
// Validate hour
43+
guard (0...23).contains(hour) else {
44+
throw RFC_5322.Date.Error.hourOutOfRange(hour)
45+
}
46+
47+
// Validate minute
48+
guard (0...59).contains(minute) else {
49+
throw RFC_5322.Date.Error.minuteOutOfRange(minute)
50+
}
51+
52+
// Validate second (allowing 60 for leap second)
53+
guard (0...60).contains(second) else {
54+
throw RFC_5322.Date.Error.secondOutOfRange(second)
55+
}
56+
57+
// Validate weekday
58+
guard (0...6).contains(weekday) else {
59+
throw RFC_5322.Date.Error.weekdayOutOfRange(weekday)
60+
}
61+
2962
self.year = year
3063
self.month = month
3164
self.day = day
@@ -34,5 +67,24 @@ extension RFC_5322.Date {
3467
self.second = second
3568
self.weekday = weekday
3669
}
70+
71+
/// Returns the number of days in the given month for the given year
72+
private static func daysInMonth(_ month: Int, year: Int) -> Int {
73+
switch month {
74+
case 1, 3, 5, 7, 8, 10, 12:
75+
return 31
76+
case 4, 6, 9, 11:
77+
return 30
78+
case 2:
79+
return isLeapYear(year) ? 29 : 28
80+
default:
81+
return 0
82+
}
83+
}
84+
85+
/// Returns true if the year is a leap year
86+
private static func isLeapYear(_ year: Int) -> Bool {
87+
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
88+
}
3789
}
3890
}

Sources/RFC_5322/RFC_5322.Date.Error.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
//
77

88
extension RFC_5322.Date {
9-
10-
/// Errors that can occur when parsing RFC 5322 date-time strings
9+
/// Errors that can occur when parsing RFC 5322 date-time strings or creating date components
1110
public enum Error: Swift.Error, Sendable, Equatable {
1211
case invalidFormat(String)
1312
case invalidDayName(String)
@@ -20,5 +19,13 @@ extension RFC_5322.Date {
2019
case invalidSecond(String)
2120
case invalidTimezone(String)
2221
case weekdayMismatch(expected: String, actual: String)
22+
23+
// Component validation errors
24+
case monthOutOfRange(Int) // Must be 1-12
25+
case dayOutOfRange(Int, month: Int, year: Int) // Must be valid for month/year
26+
case hourOutOfRange(Int) // Must be 0-23
27+
case minuteOutOfRange(Int) // Must be 0-59
28+
case secondOutOfRange(Int) // Must be 0-60 (allowing leap second)
29+
case weekdayOutOfRange(Int) // Must be 0-6
2330
}
2431
}

0 commit comments

Comments
 (0)