Skip to content
Closed
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
47 changes: 47 additions & 0 deletions Sources/EventSource/Data+Split.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// Data+Split.swift
// EventSource
//
// Created by JadianZheng on 2025/7/24.
//

import Foundation

extension Data {
func split(separators: [[UInt8]]) -> (completeData: [Data], remainingData: Data) {
var currentIndex = startIndex
var messages = [Data]()

while currentIndex < endIndex {
var foundSeparator: [UInt8]? = nil
var foundRange: Range<Data.Index>? = nil

let remainingData = self[currentIndex..<endIndex]

for separator in separators {
if let range = remainingData.firstRange(of: separator) {

if foundRange == nil || range.lowerBound < foundRange!.lowerBound {
foundSeparator = separator
foundRange = range
}
}
}

if let separator = foundSeparator, let range = foundRange {
let messageData = self[currentIndex..<range.lowerBound]

if !messageData.isEmpty {
messages.append(Data(messageData))
}

currentIndex = range.upperBound
} else {
break
}
}

let remainingData = currentIndex < endIndex ? self[currentIndex..<endIndex] : Data()
return (messages, Data(remainingData))
}
}
85 changes: 2 additions & 83 deletions Sources/EventSource/EventParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,10 @@ struct ServerEventParser: EventParser {
self.mode = mode
}

static let lf: UInt8 = 0x0A
static let cr: UInt8 = 0x0D
static let colon: UInt8 = 0x3A

mutating func parse(_ data: Data) -> [EVEvent] {
let (separatedMessages, remainingData) = splitBuffer(for: buffer + data)
let (separatedMessages, remainingData) = (buffer + data).split(separators: doubleSeparators)

buffer = remainingData
return parseBuffer(for: separatedMessages)
}
Expand All @@ -37,83 +35,4 @@ struct ServerEventParser: EventParser {

return messages
}

private func splitBuffer(for data: Data) -> (completeData: [Data], remainingData: Data) {
let separators: [[UInt8]] = [[Self.lf, Self.lf], [Self.cr, Self.lf, Self.cr, Self.lf]]

// find last range of our separator, most likely to be fast enough
let (chosenSeparator, lastSeparatorRange) = findLastSeparator(in: data, separators: separators)
guard let separator = chosenSeparator, let lastSeparator = lastSeparatorRange else {
return ([], data)
}

// chop everything before the last separator, going forward, O(n) complexity
let bufferRange = data.startIndex ..< lastSeparator.upperBound
let remainingRange = lastSeparator.upperBound ..< data.endIndex
let rawMessages: [Data] = if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, visionOS 1.0, *) {
data[bufferRange].split(separator: separator)
} else {
data[bufferRange].split(by: separator)
}

// now clean up the messages and return
let cleanedMessages = rawMessages.map { cleanMessageData($0) }
return (cleanedMessages, data[remainingRange])
}

private func findLastSeparator(in data: Data, separators: [[UInt8]]) -> ([UInt8]?, Range<Data.Index>?) {
var chosenSeparator: [UInt8]?
var lastSeparatorRange: Range<Data.Index>?
for separator in separators {
if let range = data.lastRange(of: separator) {
if lastSeparatorRange == nil || range.upperBound > lastSeparatorRange!.upperBound {
chosenSeparator = separator
lastSeparatorRange = range
}
}
}
return (chosenSeparator, lastSeparatorRange)
}

private func cleanMessageData(_ messageData: Data) -> Data {
var cleanData = messageData

// remove trailing CR/LF characters from the end
while !cleanData.isEmpty, cleanData.last == Self.cr || cleanData.last == Self.lf {
cleanData = cleanData.dropLast()
}

// also clean internal lines within each message to remove trailing \r
let cleanedLines = cleanData.split(separator: Self.lf)
.map { line in line.trimming(while: { $0 == Self.cr }) }
.joined(separator: [Self.lf])

return Data(cleanedLines)
}
}

fileprivate extension Data {
@available(macOS, deprecated: 13.0, obsoleted: 13.0, message: "This method is not recommended on macOS 13.0+")
@available(iOS, deprecated: 16.0, obsoleted: 16.0, message: "This method is not recommended on iOS 16.0+")
@available(watchOS, deprecated: 9.0, obsoleted: 9.0, message: "This method is not recommended on watchOS 9.0+")
@available(tvOS, deprecated: 16.0, obsoleted: 16.0, message: "This method is not recommended on tvOS 16.0+")
@available(visionOS, deprecated: 1.0, obsoleted: 1.1, message: "This method is not recommended on visionOS 1.0+")
func split(by separator: [UInt8]) -> [Data] {
var chunks: [Data] = []
var pos = startIndex
// Find next occurrence of separator after current position
while let r = self[pos...].range(of: Data(separator)) {
// Append if non-empty
if r.lowerBound > pos {
chunks.append(self[pos..<r.lowerBound])
}
// Update current position
pos = r.upperBound
}
// Append final chunk, if non-empty
if pos < endIndex {
chunks.append(self[pos..<endIndex])
}
return chunks
}
}
50 changes: 50 additions & 0 deletions Sources/EventSource/EventSourceABNF.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// EventSourceEOL.swift
// EventSource
//
// Created by JadianZheng on 2025/7/24.
//

import Foundation

/*
* https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events
*
* Event Stream Format (ABNF):
* stream = [ bom ] *event
* event = *( comment / field ) end-of-line
* comment = colon *any-char end-of-line
* field = 1*name-char [ colon [ space ] *any-char ] end-of-line
* end-of-line = ( cr lf / cr / lf )
*
* ; characters
* lf = %x000A ; U+000A LINE FEED (LF)
* cr = %x000D ; U+000D CARRIAGE RETURN (CR)
* space = %x0020 ; U+0020 SPACE
* colon = %x003A ; U+003A COLON (:)
* bom = %xFEFF ; U+FEFF BYTE ORDER MARK
* name-char = %x0000-0009 / %x000B-000C / %x000E-0039 / %x003B-10FFFF
* ; a scalar value other than U+000A LINE FEED (LF), U+000D CARRIAGE RETURN (CR), or U+003A COLON (:)
* any-char = %x0000-0009 / %x000B-000C / %x000E-10FFFF
* ; a scalar value other than U+000A LINE FEED (LF) or U+000D CARRIAGE RETURN (CR)
*/

let lf: UInt8 = 0x0A // \n
let cr: UInt8 = 0x0D // \r
let colon: UInt8 = 0x3A // :

let singleSeparators: [[UInt8]] = [
[cr, lf], // \r\n
[cr], // \r
[lf] // \n
].sorted { $0.count > $1.count }

let doubleSeparators: [[UInt8]] = [
[cr, lf, cr, lf], // \r\n\r\n
[lf, cr, lf], // \n\r\n
[cr, cr, lf], // \r\r\n
[cr, lf, lf], // \r\n\n
[cr, lf, cr], // \r\n\r
[cr, cr], // \r\r
[lf, lf] // \n\n
].sorted { $0.count > $1.count }
24 changes: 15 additions & 9 deletions Sources/EventSource/ServerEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,22 +68,28 @@ public struct ServerEvent: EVEvent {
}

public static func parse(from data: Data, mode: EventSource.Mode = .default) -> ServerEvent? {
let rows: [Data] = switch mode {
case .default:
data.split(separator: ServerEventParser.lf) // Separate event fields
case .dataOnly:
[data] // Do not split data in data-only mode
}
let recivedStr = String(data: data, encoding: .utf8)

let rows: [Data] = {
switch mode {
case .default:
let (separatedMessages, remainingData) = data.split(separators: singleSeparators)
return separatedMessages + [remainingData]

case .dataOnly:
return [data] // Do not split data in data-only mode
}
}()

var message = ServerEvent()

for row in rows {
// Skip the line if it is empty or it starts with a colon character
if row.isEmpty || row.first == ServerEventParser.colon {
if row.isEmpty || row.first == colon {
continue
}

let keyValue = row.split(separator: ServerEventParser.colon, maxSplits: 1)
let keyValue = row.split(separator: colon, maxSplits: 1)
let key = keyValue[0].utf8String

// If value starts with a SPACE character, remove it from value
Expand Down Expand Up @@ -111,7 +117,7 @@ public struct ServerEvent: EVEvent {
// If the line is not empty but does not contain a colon character
// add it to the other fields using the whole line as the field name,
// and the empty string as the field value.
if row.contains(ServerEventParser.colon) == false {
if row.contains(colon) == false {
let string = row.utf8String
if var other = message.other {
other[string] = ""
Expand Down
2 changes: 1 addition & 1 deletion Tests/EventSourceTests/EventParserTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ struct EventParserTests {

// Test with mixed LF (\n) and CR+LF (\r\n) - using separate events
let textMixed = "data: test mixedline1\n\n" +
"data: mixedline2\r\n\n" +
"data: mixedline2\n\r\n" +
"event: update\r\ndata: mixedtest\n\n" +
"id: 4\nevent: pong\r\ndata: mixedpong\r\n\n"

Expand Down