Skip to content

[RFC 9651] Add support for Date type to RawStructuredFieldValues #41

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Sources/RawStructuredFieldValues/ASCII.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ let asciiSemicolon = UInt8(ascii: ";")
let asciiBackslash = UInt8(ascii: "\\")
let asciiQuestionMark = UInt8(ascii: "?")
let asciiExclamationMark = UInt8(ascii: "!")
let asciiAt = UInt8(ascii: "@")
let asciiOctothorpe = UInt8(ascii: "#")
let asciiDollar = UInt8(ascii: "$")
let asciiPercent = UInt8(ascii: "%")
Expand Down
6 changes: 6 additions & 0 deletions Sources/RawStructuredFieldValues/ComponentTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ extension BareItem {

case .token(let t):
self = .token(t)

case .date:
throw StructuredHeaderError.invalidItem
}
}
}
Expand Down Expand Up @@ -135,6 +138,9 @@ public enum RFC9651BareItem: Sendable {

/// A token item.
case token(String)

/// A date item.
case date(Int)
}

extension RFC9651BareItem: ExpressibleByBooleanLiteral {
Expand Down
2 changes: 2 additions & 0 deletions Sources/RawStructuredFieldValues/Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public struct StructuredHeaderError: Error, Sendable {
case invalidByteSequence
case invalidBoolean
case invalidToken
case invalidDate
case invalidList
case invalidDictionary
case missingKey
Expand All @@ -51,6 +52,7 @@ extension StructuredHeaderError {
public static let invalidByteSequence = StructuredHeaderError(.invalidByteSequence)
public static let invalidBoolean = StructuredHeaderError(.invalidBoolean)
public static let invalidToken = StructuredHeaderError(.invalidToken)
public static let invalidDate = StructuredHeaderError(.invalidDate)
public static let invalidList = StructuredHeaderError(.invalidList)
public static let invalidDictionary = StructuredHeaderError(.invalidDictionary)
public static let missingKey = StructuredHeaderError(.missingKey)
Expand Down
39 changes: 34 additions & 5 deletions Sources/RawStructuredFieldValues/FieldParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ extension StructuredFieldValueParser {

switch first {
case asciiDash, asciiDigits:
return try self._parseAnIntegerOrDecimal()
return try self._parseAnIntegerOrDecimal(isDate: false)
case asciiDquote:
return try self._parseAString()
case asciiColon:
Expand All @@ -222,12 +222,14 @@ extension StructuredFieldValueParser {
return try self._parseABoolean()
case asciiCapitals, asciiLowercases, asciiAsterisk:
return try self._parseAToken()
case asciiAt:
return try self._parseADate()
default:
throw StructuredHeaderError.invalidItem
}
}

private mutating func _parseAnIntegerOrDecimal() throws -> RFC9651BareItem {
private mutating func _parseAnIntegerOrDecimal(isDate: Bool) throws -> RFC9651BareItem {
var sign = 1
var type = IntegerOrDecimal.integer

Expand All @@ -248,10 +250,19 @@ extension StructuredFieldValueParser {
// Do nothing
()
case asciiPeriod where type == .integer:
// If output_date is decimal, fail parsing.
if isDate {
throw StructuredHeaderError.invalidDate
}

// If input_number contains more than 12 characters, fail parsing. Otherwise,
// set type to decimal and consume.
if self.underlyingData.distance(from: self.underlyingData.startIndex, to: index) > 12 {
throw StructuredHeaderError.invalidIntegerOrDecimal
if isDate {
throw StructuredHeaderError.invalidDate
} else {
throw StructuredHeaderError.invalidIntegerOrDecimal
}
}
type = .decimal
default:
Expand All @@ -268,9 +279,15 @@ extension StructuredFieldValueParser {
switch type {
case .integer:
if count > 15 {
throw StructuredHeaderError.invalidIntegerOrDecimal
if isDate {
throw StructuredHeaderError.invalidDate
} else {
throw StructuredHeaderError.invalidIntegerOrDecimal
}
}
case .decimal:
assert(isDate == false)

if count > 16 {
throw StructuredHeaderError.invalidIntegerOrDecimal
}
Expand All @@ -286,7 +303,13 @@ extension StructuredFieldValueParser {
// This intermediate string is sad, we should rewrite this manually to avoid it.
// This force-unwrap is safe, as we have validated that all characters are ascii digits.
let baseInt = Int(String(decoding: integerBytes, as: UTF8.self), radix: 10)!
return .integer(baseInt * sign)
let resultingInt = baseInt * sign

if isDate {
return .date(resultingInt)
} else {
return .integer(resultingInt)
}
case .decimal:
// This must be non-nil, otherwise we couldn't have flipped to the decimal type.
let periodIndex = integerBytes.firstIndex(of: asciiPeriod)!
Expand Down Expand Up @@ -459,6 +482,12 @@ extension StructuredFieldValueParser {
return .token(String(decoding: tokenSlice, as: UTF8.self))
}

private mutating func _parseADate() throws -> RFC9651BareItem {
assert(self.underlyingData.first == asciiAt)
self.underlyingData.consumeFirst()
return try self._parseAnIntegerOrDecimal(isDate: true)
}

private mutating func _parseParameters() throws -> OrderedMap<Key, RFC9651BareItem> {
var parameters = OrderedMap<Key, RFC9651BareItem>()

Expand Down
9 changes: 9 additions & 0 deletions Sources/RawStructuredFieldValues/FieldSerializer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,15 @@ extension StructuredFieldValueSerializer {
self.data.append(asciiQuestionMark)
let character = bool ? asciiOne : asciiZero
self.data.append(character)
case .date(let date):
self.data.append(asciiAt)

// Then, serialize as integer.
guard let wideInt = Int64(exactly: date), validIntegerRange.contains(wideInt) else {
throw StructuredHeaderError.invalidDate
}

self.data.append(contentsOf: String(date, radix: 10).utf8)
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/sh-parser/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ extension RFC9651BareItem {
let d = Decimal(sign: decimal.mantissa > 0 ? .plus : .minus,
exponent: Int(decimal.exponent), significand: Decimal(decimal.mantissa))
return "decimal \(d)"
case .date(let date):
return "date \(date)"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ final class StructuredFieldParserTests: XCTestCase {
XCTAssertEqual(decodedValue, decodedExpected, "\(fixtureName): Got \(Array(decodedValue)), expected \(Array(decodedExpected))")
case (.bool(let baseBool), .bool(let expectedBool)):
XCTAssertEqual(baseBool, expectedBool, "\(fixtureName): Got \(baseBool), expected \(expectedBool)")
case(.date(let baseDate), .dictionary(let typeDictionary)):
guard typeDictionary.count == 2, case .string(let typeName) = typeDictionary["__type"], case .integer(let typeValue) = typeDictionary["value"] else {
XCTFail("\(fixtureName): Unexpected type dict \(typeDictionary)")
return
}

XCTAssertEqual(typeName, "date", "\(fixtureName): Expected type date, got type \(typeName)")
XCTAssertEqual(typeValue, baseDate, "\(fixtureName): Got \(baseDate), expected \(typeValue)")
default:
XCTFail("\(fixtureName): Got \(bareItem), expected \(schema)")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ extension RFC9651BareItem {
let expectedBase64Bytes = Data(base32Encoded: Data(value.utf8)).base64EncodedString()
self = .undecodedByteSequence(expectedBase64Bytes)

case (.some(.string("date")), .some(.integer(let value))):
self = .date(value)

default:
preconditionFailure("Unexpected type object \(typeObject)")
}
Expand Down
38 changes: 38 additions & 0 deletions Tests/TestFixtures/date.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
[
{
"name": "date - 1970-01-01 00:00:00",
"raw": ["@0"],
"header_type": "item",
"expected": [{"__type": "date", "value": 0}, {}]
},
{
"name": "date - 2022-08-04 01:57:13",
"raw": ["@1659578233"],
"header_type": "item",
"expected": [{"__type": "date", "value": 1659578233}, {}]
},
{
"name": "date - 1917-05-30 22:02:47",
"raw": ["@-1659578233"],
"header_type": "item",
"expected": [{"__type": "date", "value": -1659578233}, {}]
},
{
"name": "date - 2^31",
"raw": ["@2147483648"],
"header_type": "item",
"expected": [{"__type": "date", "value": 2147483648}, {}]
},
{
"name": "date - 2^32",
"raw": ["@4294967296"],
"header_type": "item",
"expected": [{"__type": "date", "value": 4294967296}, {}]
},
{
"name": "date - decimal",
"raw": ["@1659578233.12"],
"header_type": "item",
"must_fail": true
}
]