Skip to content

Commit 6220e78

Browse files
author
Jadian
committed
Add end-of-line Mix mode support, no mater \r\n\n or \n\r\n or \r\r
1 parent 1c7bc99 commit 6220e78

File tree

5 files changed

+115
-93
lines changed

5 files changed

+115
-93
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//
2+
// Data+Split.swift
3+
// EventSource
4+
//
5+
// Created by JadianZheng on 2025/7/24.
6+
//
7+
8+
import Foundation
9+
10+
extension Data {
11+
func split(separators: [[UInt8]]) -> (completeData: [Data], remainingData: Data) {
12+
var currentIndex = startIndex
13+
var messages = [Data]()
14+
15+
while currentIndex < endIndex {
16+
var foundSeparator: [UInt8]? = nil
17+
var foundRange: Range<Data.Index>? = nil
18+
19+
let remainingData = self[currentIndex..<endIndex]
20+
21+
for separator in separators {
22+
if let range = remainingData.firstRange(of: separator) {
23+
24+
if foundRange == nil || range.lowerBound < foundRange!.lowerBound {
25+
foundSeparator = separator
26+
foundRange = range
27+
}
28+
}
29+
}
30+
31+
if let separator = foundSeparator, let range = foundRange {
32+
let messageData = self[currentIndex..<range.lowerBound]
33+
34+
if !messageData.isEmpty {
35+
messages.append(Data(messageData))
36+
}
37+
38+
currentIndex = range.upperBound
39+
} else {
40+
break
41+
}
42+
}
43+
44+
let remainingData = currentIndex < endIndex ? self[currentIndex..<endIndex] : Data()
45+
return (messages, Data(remainingData))
46+
}
47+
}

Sources/EventSource/EventParser.swift

Lines changed: 2 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,10 @@ struct ServerEventParser: EventParser {
2121
self.mode = mode
2222
}
2323

24-
static let lf: UInt8 = 0x0A
25-
static let cr: UInt8 = 0x0D
26-
static let colon: UInt8 = 0x3A
2724

2825
mutating func parse(_ data: Data) -> [EVEvent] {
29-
let (separatedMessages, remainingData) = splitBuffer(for: buffer + data)
26+
let (separatedMessages, remainingData) = (buffer + data).split(separators: doubleSeparators)
27+
3028
buffer = remainingData
3129
return parseBuffer(for: separatedMessages)
3230
}
@@ -37,83 +35,4 @@ struct ServerEventParser: EventParser {
3735

3836
return messages
3937
}
40-
41-
private func splitBuffer(for data: Data) -> (completeData: [Data], remainingData: Data) {
42-
let separators: [[UInt8]] = [[Self.cr, Self.cr], [Self.lf, Self.lf], [Self.cr, Self.lf, Self.cr, Self.lf]]
43-
44-
// find last range of our separator, most likely to be fast enough
45-
let (chosenSeparator, lastSeparatorRange) = findLastSeparator(in: data, separators: separators)
46-
guard let separator = chosenSeparator, let lastSeparator = lastSeparatorRange else {
47-
return ([], data)
48-
}
49-
50-
// chop everything before the last separator, going forward, O(n) complexity
51-
let bufferRange = data.startIndex ..< lastSeparator.upperBound
52-
let remainingRange = lastSeparator.upperBound ..< data.endIndex
53-
let rawMessages: [Data] = if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, visionOS 1.0, *) {
54-
data[bufferRange].split(separator: separator)
55-
} else {
56-
data[bufferRange].split(by: separator)
57-
}
58-
59-
// now clean up the messages and return
60-
let cleanedMessages = rawMessages.map { cleanMessageData($0) }
61-
return (cleanedMessages, data[remainingRange])
62-
}
63-
64-
private func findLastSeparator(in data: Data, separators: [[UInt8]]) -> ([UInt8]?, Range<Data.Index>?) {
65-
var chosenSeparator: [UInt8]?
66-
var lastSeparatorRange: Range<Data.Index>?
67-
for separator in separators {
68-
if let range = data.lastRange(of: separator) {
69-
if lastSeparatorRange == nil || range.upperBound > lastSeparatorRange!.upperBound {
70-
chosenSeparator = separator
71-
lastSeparatorRange = range
72-
}
73-
}
74-
}
75-
return (chosenSeparator, lastSeparatorRange)
76-
}
77-
78-
private func cleanMessageData(_ messageData: Data) -> Data {
79-
var cleanData = messageData
80-
81-
// remove trailing CR/LF characters from the end
82-
while !cleanData.isEmpty, cleanData.last == Self.cr || cleanData.last == Self.lf {
83-
cleanData = cleanData.dropLast()
84-
}
85-
86-
// also clean internal lines within each message to remove trailing \r
87-
let cleanedLines = cleanData.split(separator: Self.lf)
88-
.map { line in line.trimming(while: { $0 == Self.cr }) }
89-
.joined(separator: [Self.lf])
90-
91-
return Data(cleanedLines)
92-
}
93-
}
94-
95-
fileprivate extension Data {
96-
@available(macOS, deprecated: 13.0, obsoleted: 13.0, message: "This method is not recommended on macOS 13.0+")
97-
@available(iOS, deprecated: 16.0, obsoleted: 16.0, message: "This method is not recommended on iOS 16.0+")
98-
@available(watchOS, deprecated: 9.0, obsoleted: 9.0, message: "This method is not recommended on watchOS 9.0+")
99-
@available(tvOS, deprecated: 16.0, obsoleted: 16.0, message: "This method is not recommended on tvOS 16.0+")
100-
@available(visionOS, deprecated: 1.0, obsoleted: 1.1, message: "This method is not recommended on visionOS 1.0+")
101-
func split(by separator: [UInt8]) -> [Data] {
102-
var chunks: [Data] = []
103-
var pos = startIndex
104-
// Find next occurrence of separator after current position
105-
while let r = self[pos...].range(of: Data(separator)) {
106-
// Append if non-empty
107-
if r.lowerBound > pos {
108-
chunks.append(self[pos..<r.lowerBound])
109-
}
110-
// Update current position
111-
pos = r.upperBound
112-
}
113-
// Append final chunk, if non-empty
114-
if pos < endIndex {
115-
chunks.append(self[pos..<endIndex])
116-
}
117-
return chunks
118-
}
11938
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//
2+
// EventSourceEOL.swift
3+
// EventSource
4+
//
5+
// Created by JadianZheng on 2025/7/24.
6+
//
7+
8+
import Foundation
9+
10+
/*
11+
* https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events
12+
*
13+
* Event Stream Format (ABNF):
14+
* stream = [ bom ] *event
15+
* event = *( comment / field ) end-of-line
16+
* comment = colon *any-char end-of-line
17+
* field = 1*name-char [ colon [ space ] *any-char ] end-of-line
18+
* end-of-line = ( cr lf / cr / lf )
19+
*
20+
* ; characters
21+
* lf = %x000A ; U+000A LINE FEED (LF)
22+
* cr = %x000D ; U+000D CARRIAGE RETURN (CR)
23+
* space = %x0020 ; U+0020 SPACE
24+
* colon = %x003A ; U+003A COLON (:)
25+
* bom = %xFEFF ; U+FEFF BYTE ORDER MARK
26+
* name-char = %x0000-0009 / %x000B-000C / %x000E-0039 / %x003B-10FFFF
27+
* ; a scalar value other than U+000A LINE FEED (LF), U+000D CARRIAGE RETURN (CR), or U+003A COLON (:)
28+
* any-char = %x0000-0009 / %x000B-000C / %x000E-10FFFF
29+
* ; a scalar value other than U+000A LINE FEED (LF) or U+000D CARRIAGE RETURN (CR)
30+
*/
31+
32+
let lf: UInt8 = 0x0A // \n
33+
let cr: UInt8 = 0x0D // \r
34+
let colon: UInt8 = 0x3A // :
35+
36+
let singleSeparators: [[UInt8]] = [
37+
[cr, lf], // \r\n
38+
[cr], // \r
39+
[lf] // \n
40+
].sorted { $0.count > $1.count }
41+
42+
let doubleSeparators: [[UInt8]] = [
43+
[cr, lf, cr, lf], // \r\n\r\n
44+
[lf, cr, lf], // \n\r\n
45+
[cr, cr, lf], // \r\r\n
46+
[cr, lf, lf], // \r\n\n
47+
[cr, lf, cr], // \r\n\r
48+
[cr, cr], // \r\r
49+
[lf, lf] // \n\n
50+
].sorted { $0.count > $1.count }

Sources/EventSource/ServerEvent.swift

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,22 +68,28 @@ public struct ServerEvent: EVEvent {
6868
}
6969

7070
public static func parse(from data: Data, mode: EventSource.Mode = .default) -> ServerEvent? {
71-
let rows: [Data] = switch mode {
72-
case .default:
73-
data.split(separator: ServerEventParser.lf) // Separate event fields
74-
case .dataOnly:
75-
[data] // Do not split data in data-only mode
76-
}
71+
let recivedStr = String(data: data, encoding: .utf8)
72+
73+
let rows: [Data] = {
74+
switch mode {
75+
case .default:
76+
let (separatedMessages, remainingData) = data.split(separators: singleSeparators)
77+
return separatedMessages + [remainingData]
78+
79+
case .dataOnly:
80+
return [data] // Do not split data in data-only mode
81+
}
82+
}()
7783

7884
var message = ServerEvent()
7985

8086
for row in rows {
8187
// Skip the line if it is empty or it starts with a colon character
82-
if row.isEmpty || row.first == ServerEventParser.colon {
88+
if row.isEmpty || row.first == colon {
8389
continue
8490
}
8591

86-
let keyValue = row.split(separator: ServerEventParser.colon, maxSplits: 1)
92+
let keyValue = row.split(separator: colon, maxSplits: 1)
8793
let key = keyValue[0].utf8String
8894

8995
// If value starts with a SPACE character, remove it from value
@@ -111,7 +117,7 @@ public struct ServerEvent: EVEvent {
111117
// If the line is not empty but does not contain a colon character
112118
// add it to the other fields using the whole line as the field name,
113119
// and the empty string as the field value.
114-
if row.contains(ServerEventParser.colon) == false {
120+
if row.contains(colon) == false {
115121
let string = row.utf8String
116122
if var other = message.other {
117123
other[string] = ""

Tests/EventSourceTests/EventParserTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ struct EventParserTests {
189189

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

0 commit comments

Comments
 (0)