Skip to content

Commit b0bf94f

Browse files
authored
[RFC 9651] Add support for Date type to RawStructuredFieldValues (#41)
Motivation: [RFC 9651](https://www.ietf.org/rfc/rfc9651.html) added the Date Structured Type. Modifications: - Implement the parser and serializer for Date in the RawStructuredFieldValues module. Result: The RawStructuredFieldValues module will support the Date type.
1 parent f069b10 commit b0bf94f

File tree

9 files changed

+103
-5
lines changed

9 files changed

+103
-5
lines changed

Sources/RawStructuredFieldValues/ASCII.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ let asciiSemicolon = UInt8(ascii: ";")
2828
let asciiBackslash = UInt8(ascii: "\\")
2929
let asciiQuestionMark = UInt8(ascii: "?")
3030
let asciiExclamationMark = UInt8(ascii: "!")
31+
let asciiAt = UInt8(ascii: "@")
3132
let asciiOctothorpe = UInt8(ascii: "#")
3233
let asciiDollar = UInt8(ascii: "$")
3334
let asciiPercent = UInt8(ascii: "%")

Sources/RawStructuredFieldValues/ComponentTypes.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ extension BareItem {
107107

108108
case .token(let t):
109109
self = .token(t)
110+
111+
case .date:
112+
throw StructuredHeaderError.invalidItem
110113
}
111114
}
112115
}
@@ -135,6 +138,9 @@ public enum RFC9651BareItem: Sendable {
135138

136139
/// A token item.
137140
case token(String)
141+
142+
/// A date item.
143+
case date(Int)
138144
}
139145

140146
extension RFC9651BareItem: ExpressibleByBooleanLiteral {

Sources/RawStructuredFieldValues/Errors.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public struct StructuredHeaderError: Error, Sendable {
2626
case invalidByteSequence
2727
case invalidBoolean
2828
case invalidToken
29+
case invalidDate
2930
case invalidList
3031
case invalidDictionary
3132
case missingKey
@@ -51,6 +52,7 @@ extension StructuredHeaderError {
5152
public static let invalidByteSequence = StructuredHeaderError(.invalidByteSequence)
5253
public static let invalidBoolean = StructuredHeaderError(.invalidBoolean)
5354
public static let invalidToken = StructuredHeaderError(.invalidToken)
55+
public static let invalidDate = StructuredHeaderError(.invalidDate)
5456
public static let invalidList = StructuredHeaderError(.invalidList)
5557
public static let invalidDictionary = StructuredHeaderError(.invalidDictionary)
5658
public static let missingKey = StructuredHeaderError(.missingKey)

Sources/RawStructuredFieldValues/FieldParser.swift

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ extension StructuredFieldValueParser {
213213

214214
switch first {
215215
case asciiDash, asciiDigits:
216-
return try self._parseAnIntegerOrDecimal()
216+
return try self._parseAnIntegerOrDecimal(isDate: false)
217217
case asciiDquote:
218218
return try self._parseAString()
219219
case asciiColon:
@@ -222,12 +222,14 @@ extension StructuredFieldValueParser {
222222
return try self._parseABoolean()
223223
case asciiCapitals, asciiLowercases, asciiAsterisk:
224224
return try self._parseAToken()
225+
case asciiAt:
226+
return try self._parseADate()
225227
default:
226228
throw StructuredHeaderError.invalidItem
227229
}
228230
}
229231

230-
private mutating func _parseAnIntegerOrDecimal() throws -> RFC9651BareItem {
232+
private mutating func _parseAnIntegerOrDecimal(isDate: Bool) throws -> RFC9651BareItem {
231233
var sign = 1
232234
var type = IntegerOrDecimal.integer
233235

@@ -248,10 +250,19 @@ extension StructuredFieldValueParser {
248250
// Do nothing
249251
()
250252
case asciiPeriod where type == .integer:
253+
// If output_date is decimal, fail parsing.
254+
if isDate {
255+
throw StructuredHeaderError.invalidDate
256+
}
257+
251258
// If input_number contains more than 12 characters, fail parsing. Otherwise,
252259
// set type to decimal and consume.
253260
if self.underlyingData.distance(from: self.underlyingData.startIndex, to: index) > 12 {
254-
throw StructuredHeaderError.invalidIntegerOrDecimal
261+
if isDate {
262+
throw StructuredHeaderError.invalidDate
263+
} else {
264+
throw StructuredHeaderError.invalidIntegerOrDecimal
265+
}
255266
}
256267
type = .decimal
257268
default:
@@ -268,9 +279,15 @@ extension StructuredFieldValueParser {
268279
switch type {
269280
case .integer:
270281
if count > 15 {
271-
throw StructuredHeaderError.invalidIntegerOrDecimal
282+
if isDate {
283+
throw StructuredHeaderError.invalidDate
284+
} else {
285+
throw StructuredHeaderError.invalidIntegerOrDecimal
286+
}
272287
}
273288
case .decimal:
289+
assert(isDate == false)
290+
274291
if count > 16 {
275292
throw StructuredHeaderError.invalidIntegerOrDecimal
276293
}
@@ -286,7 +303,13 @@ extension StructuredFieldValueParser {
286303
// This intermediate string is sad, we should rewrite this manually to avoid it.
287304
// This force-unwrap is safe, as we have validated that all characters are ascii digits.
288305
let baseInt = Int(String(decoding: integerBytes, as: UTF8.self), radix: 10)!
289-
return .integer(baseInt * sign)
306+
let resultingInt = baseInt * sign
307+
308+
if isDate {
309+
return .date(resultingInt)
310+
} else {
311+
return .integer(resultingInt)
312+
}
290313
case .decimal:
291314
// This must be non-nil, otherwise we couldn't have flipped to the decimal type.
292315
let periodIndex = integerBytes.firstIndex(of: asciiPeriod)!
@@ -459,6 +482,12 @@ extension StructuredFieldValueParser {
459482
return .token(String(decoding: tokenSlice, as: UTF8.self))
460483
}
461484

485+
private mutating func _parseADate() throws -> RFC9651BareItem {
486+
assert(self.underlyingData.first == asciiAt)
487+
self.underlyingData.consumeFirst()
488+
return try self._parseAnIntegerOrDecimal(isDate: true)
489+
}
490+
462491
private mutating func _parseParameters() throws -> OrderedMap<Key, RFC9651BareItem> {
463492
var parameters = OrderedMap<Key, RFC9651BareItem>()
464493

Sources/RawStructuredFieldValues/FieldSerializer.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,15 @@ extension StructuredFieldValueSerializer {
204204
self.data.append(asciiQuestionMark)
205205
let character = bool ? asciiOne : asciiZero
206206
self.data.append(character)
207+
case .date(let date):
208+
self.data.append(asciiAt)
209+
210+
// Then, serialize as integer.
211+
guard let wideInt = Int64(exactly: date), validIntegerRange.contains(wideInt) else {
212+
throw StructuredHeaderError.invalidDate
213+
}
214+
215+
self.data.append(contentsOf: String(date, radix: 10).utf8)
207216
}
208217
}
209218
}

Sources/sh-parser/main.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ extension RFC9651BareItem {
166166
let d = Decimal(sign: decimal.mantissa > 0 ? .plus : .minus,
167167
exponent: Int(decimal.exponent), significand: Decimal(decimal.mantissa))
168168
return "decimal \(d)"
169+
case .date(let date):
170+
return "date \(date)"
169171
}
170172
}
171173
}

Tests/StructuredFieldValuesTests/StructuredFieldParserTests.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ final class StructuredFieldParserTests: XCTestCase {
6060
XCTAssertEqual(decodedValue, decodedExpected, "\(fixtureName): Got \(Array(decodedValue)), expected \(Array(decodedExpected))")
6161
case (.bool(let baseBool), .bool(let expectedBool)):
6262
XCTAssertEqual(baseBool, expectedBool, "\(fixtureName): Got \(baseBool), expected \(expectedBool)")
63+
case(.date(let baseDate), .dictionary(let typeDictionary)):
64+
guard typeDictionary.count == 2, case .string(let typeName) = typeDictionary["__type"], case .integer(let typeValue) = typeDictionary["value"] else {
65+
XCTFail("\(fixtureName): Unexpected type dict \(typeDictionary)")
66+
return
67+
}
68+
69+
XCTAssertEqual(typeName, "date", "\(fixtureName): Expected type date, got type \(typeName)")
70+
XCTAssertEqual(typeValue, baseDate, "\(fixtureName): Got \(baseDate), expected \(typeValue)")
6371
default:
6472
XCTFail("\(fixtureName): Got \(bareItem), expected \(schema)")
6573
}

Tests/StructuredFieldValuesTests/StructuredFieldSerializerTests.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,9 @@ extension RFC9651BareItem {
198198
let expectedBase64Bytes = Data(base32Encoded: Data(value.utf8)).base64EncodedString()
199199
self = .undecodedByteSequence(expectedBase64Bytes)
200200

201+
case (.some(.string("date")), .some(.integer(let value))):
202+
self = .date(value)
203+
201204
default:
202205
preconditionFailure("Unexpected type object \(typeObject)")
203206
}

Tests/TestFixtures/date.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
[
2+
{
3+
"name": "date - 1970-01-01 00:00:00",
4+
"raw": ["@0"],
5+
"header_type": "item",
6+
"expected": [{"__type": "date", "value": 0}, {}]
7+
},
8+
{
9+
"name": "date - 2022-08-04 01:57:13",
10+
"raw": ["@1659578233"],
11+
"header_type": "item",
12+
"expected": [{"__type": "date", "value": 1659578233}, {}]
13+
},
14+
{
15+
"name": "date - 1917-05-30 22:02:47",
16+
"raw": ["@-1659578233"],
17+
"header_type": "item",
18+
"expected": [{"__type": "date", "value": -1659578233}, {}]
19+
},
20+
{
21+
"name": "date - 2^31",
22+
"raw": ["@2147483648"],
23+
"header_type": "item",
24+
"expected": [{"__type": "date", "value": 2147483648}, {}]
25+
},
26+
{
27+
"name": "date - 2^32",
28+
"raw": ["@4294967296"],
29+
"header_type": "item",
30+
"expected": [{"__type": "date", "value": 4294967296}, {}]
31+
},
32+
{
33+
"name": "date - decimal",
34+
"raw": ["@1659578233.12"],
35+
"header_type": "item",
36+
"must_fail": true
37+
}
38+
]

0 commit comments

Comments
 (0)