Skip to content

[DRAFT] Migrate format parsers to UTF8Span #1370

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

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
150 changes: 88 additions & 62 deletions Sources/FoundationEssentials/Formatting/Date+HTTPFormatStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,32 +60,23 @@ extension Date {
}

fileprivate func parse(_ value: String, in range: Range<String.Index>) -> (String.Index, Date)? {
var v = value[range]
let v = value[range]
guard !v.isEmpty else {
return nil
}

let result = v.withUTF8 { buffer -> (Int, Date)? in
let view = BufferView(unsafeBufferPointer: buffer)!

guard let comps = try? componentsStyle.components(from: value, in: view) else {
return nil
}

// HTTP dates are always GMT
guard let date = Calendar(identifier: .gregorian).date(from: comps.components) else {
return nil
}

return (comps.consumed, date)
guard #available(FoundationSpan 6.2, *) else {
fatalError("Span unavailable")
}

guard let result else {

guard let comps = try? componentsStyle.components(in: v.utf8Span),
let date = Calendar(identifier: .gregorian).date(from: comps.components)
else {
return nil
}
let endIndex = value.utf8.index(v.startIndex, offsetBy: result.0)
return (endIndex, result.1)

let endIndex = value.utf8.index(v.startIndex, offsetBy: comps.consumed)
return (endIndex, date)
}
}
}
Expand Down Expand Up @@ -316,49 +307,51 @@ extension DateComponents {
}

private func parse(_ value: String, in range: Range<String.Index>) -> (String.Index, DateComponents)? {
var v = value[range]
let v = value[range]
guard !v.isEmpty else {
return nil
}

let result = v.withUTF8 { buffer -> (Int, DateComponents)? in
let view = BufferView(unsafeBufferPointer: buffer)!

guard let comps = try? components(from: value, in: view) else {
return nil
}

return (comps.consumed, comps.components)
guard #available(FoundationSpan 6.2, *) else {
fatalError("Span unavailable")
}
guard let result else {

guard let comps = try? components(in: v.utf8Span) else {
return nil
}
let endIndex = value.utf8.index(v.startIndex, offsetBy: result.0)
return (endIndex, result.1)

let endIndex = value.utf8.index(v.startIndex, offsetBy: comps.consumed)
return (endIndex, comps.components)
}

fileprivate func components(from inputString: String, in view: borrowing BufferView<UInt8>) throws -> ComponentsParseResult {

@available(FoundationSpan 6.2, *)
fileprivate func components(
in view: UTF8Span
) throws -> ComponentsParseResult {
// https://www.rfc-editor.org/rfc/rfc9110.html#http.date
// <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT

var it = view.makeIterator()
// Produce an error message to throw
func error(_ extendedDescription: String? = nil) -> CocoaError {
parseError(view, exampleFormattedString: Date.HTTPFormatStyle().format(Date.now), extendedDescription: extendedDescription)
}

var it = view.makeCursor()
var dc = DateComponents()

// Despite the spec, we allow the weekday name to be optional.
guard let maybeWeekday1 = it.peek() else {
throw parseError(inputString, exampleFormattedString: Date.HTTPFormatStyle().format(Date.now))
throw error()
}

if isASCIIDigit(maybeWeekday1) {
// This is the first digit of the day. Weekday is not present.
} else {
// Anything else must be a day-name (Mon, Tue, ... Sun)
guard let weekday1 = it.next(), let weekday2 = it.next(), let weekday3 = it.next() else {
throw parseError(inputString, exampleFormattedString: Date.HTTPFormatStyle().format(Date.now))
throw error()
}

dc.weekday = switch (weekday1, weekday2, weekday3) {
case (UInt8(ascii: "S"), UInt8(ascii: "u"), UInt8(ascii: "n")):
1
Expand All @@ -375,20 +368,30 @@ extension DateComponents {
case (UInt8(ascii: "S"), UInt8(ascii: "a"), UInt8(ascii: "t")):
7
default:
throw parseError(inputString, exampleFormattedString: Date.HTTPFormatStyle().format(Date.now), extendedDescription: "Malformed weekday name")
throw error("Malformed weekday name")
}

// Move past , and space to weekday
try it.expectCharacter(UInt8(ascii: ","), input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now), extendedDescription: "Missing , after weekday")
try it.expectCharacter(UInt8(ascii: " "), input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now), extendedDescription: "Missing space after weekday")
guard it.matchByte(UInt8(ascii: ",")) else {
throw error("Missing , after weekday")
}
guard it.matchByte(UInt8(ascii: " ")) else {
throw error("Missing space after weekday")
}
}

dc.day = try it.digits(minDigits: 2, maxDigits: 2, input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now), extendedDescription: "Missing or malformed day")
try it.expectCharacter(UInt8(ascii: " "), input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now))
guard let day = it.parseNumber(minDigits: 2, maxDigits: 2) else {
throw error("Missing or malformed day")
}
dc.day = day

guard it.matchByte(UInt8(ascii: " ")) else {
throw error()
}

// month-name (Jan, Feb, ... Dec)
guard let month1 = it.next(), let month2 = it.next(), let month3 = it.next() else {
throw parseError(inputString, exampleFormattedString: Date.HTTPFormatStyle().format(Date.now), extendedDescription: "Missing month")
throw error("Missing month")
}

dc.month = switch (month1, month2, month3) {
Expand Down Expand Up @@ -417,52 +420,75 @@ extension DateComponents {
case (UInt8(ascii: "D"), UInt8(ascii: "e"), UInt8(ascii: "c")):
12
default:
throw parseError(inputString, exampleFormattedString: Date.HTTPFormatStyle().format(Date.now), extendedDescription: "Month \(String(describing: dc.month)) is out of bounds")
throw error("Month \(String(describing: dc.month)) is out of bounds")
}

guard it.matchByte(UInt8(ascii: " ")) else {
throw error()
}

try it.expectCharacter(UInt8(ascii: " "), input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now))
guard let year = it.parseNumber(minDigits: 4, maxDigits: 4) else {
throw error()
}
dc.year = year

dc.year = try it.digits(minDigits: 4, maxDigits: 4, input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now))
try it.expectCharacter(UInt8(ascii: " "), input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now))
guard it.matchByte(UInt8(ascii: " ")) else {
throw error()
}

let hour = try it.digits(minDigits: 2, maxDigits: 2, input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now))
guard let hour = it.parseNumber(minDigits: 2, maxDigits: 2) else {
throw error()
}
if hour < 0 || hour > 23 {
throw parseError(inputString, exampleFormattedString: Date.HTTPFormatStyle().format(Date.now), extendedDescription: "Hour \(hour) is out of bounds")
throw error("Hour \(hour) is out of bounds")
}
dc.hour = hour

try it.expectCharacter(UInt8(ascii: ":"), input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now))
let minute = try it.digits(minDigits: 2, maxDigits: 2, input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now))
guard it.matchByte(UInt8(ascii: ":")) else {
throw error()
}
guard let minute = it.parseNumber(minDigits: 2, maxDigits: 2) else {
throw error()
}
if minute < 0 || minute > 59 {
throw parseError(inputString, exampleFormattedString: Date.HTTPFormatStyle().format(Date.now), extendedDescription: "Minute \(minute) is out of bounds")
throw error("Minute \(minute) is out of bounds")
}
dc.minute = minute

try it.expectCharacter(UInt8(ascii: ":"), input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now))
let second = try it.digits(minDigits: 2, maxDigits: 2, input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now))
guard it.matchByte(UInt8(ascii: ":")) else {
throw error()
}
guard let second = it.parseNumber(minDigits: 2, maxDigits: 2) else {
throw error()
}
// second '60' is supported in the spec for leap seconds, but Foundation does not support leap seconds. 60 is adjusted to 59.
if second < 0 || second > 60 {
throw parseError(inputString, exampleFormattedString: Date.HTTPFormatStyle().format(Date.now), extendedDescription: "Second \(second) is out of bounds")
throw error("Second \(second) is out of bounds")
}
// Foundation does not support leap seconds. We convert 60 seconds into 59 seconds.
if second == 60 {
dc.second = 59
} else {
dc.second = second
}
try it.expectCharacter(UInt8(ascii: " "), input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now))
guard it.matchByte(UInt8(ascii: " ")) else {
throw error()
}

// "GMT"
try it.expectCharacter(UInt8(ascii: "G"), input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now), extendedDescription: "Missing GMT time zone")
try it.expectCharacter(UInt8(ascii: "M"), input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now), extendedDescription: "Missing GMT time zone")
try it.expectCharacter(UInt8(ascii: "T"), input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now), extendedDescription: "Missing GMT time zone")
guard it.matchByte(UInt8(ascii: "G")),
it.matchByte(UInt8(ascii: "M")),
it.matchByte(UInt8(ascii: "T"))
else {
throw error("Missing GMT time zone")
}

// Time zone is always GMT, calendar is always Gregorian
dc.timeZone = .gmt
dc.calendar = Calendar(identifier: .gregorian)

// Would be nice to see this functionality on BufferView, but for now we calculate it ourselves.
let utf8CharactersRead = it.curPointer - view.startIndex._rawValue
let utf8CharactersRead = it.currentOffset

return ComponentsParseResult(consumed: utf8CharactersRead, components: dc)
}
Expand Down
Loading
Loading