diff --git a/Sources/EventSource/Data+Split.swift b/Sources/EventSource/Data+Split.swift new file mode 100644 index 0000000..d06e376 --- /dev/null +++ b/Sources/EventSource/Data+Split.swift @@ -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? = nil + + let remainingData = self[currentIndex.. [EVEvent] { - let (separatedMessages, remainingData) = splitBuffer(for: buffer + data) + let (separatedMessages, remainingData) = (buffer + data).split(separators: doubleSeparators) + buffer = remainingData return parseBuffer(for: separatedMessages) } @@ -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?) { - var chosenSeparator: [UInt8]? - var lastSeparatorRange: Range? - 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.. $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 } diff --git a/Sources/EventSource/ServerEvent.swift b/Sources/EventSource/ServerEvent.swift index 51e4893..d60783f 100644 --- a/Sources/EventSource/ServerEvent.swift +++ b/Sources/EventSource/ServerEvent.swift @@ -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 @@ -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] = "" diff --git a/Tests/EventSourceTests/EventParserTests.swift b/Tests/EventSourceTests/EventParserTests.swift index e737c38..59e2143 100644 --- a/Tests/EventSourceTests/EventParserTests.swift +++ b/Tests/EventSourceTests/EventParserTests.swift @@ -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"