/// <summary> /// Attempts to parse a single non-empty line of SSE content that was read as UTF-8 bytes. Empty lines /// should not be not passed to this method. /// </summary> /// <param name="line">a line that was read from the stream, not including any trailing CR/LF</param> /// <returns>a <see cref="Result"/> containing the parsed field or comment; <c>ValueBytes</c> /// will be set rather than <c>ValueString</c></returns> public static Result ParseLineUtf8Bytes(Utf8ByteSpan line) { if (line.Length > 0 && line.Data[line.Offset] == ':') // comment { return(new Result { ValueBytes = line }); } int colonPos = 0; for (; colonPos < line.Length && line.Data[line.Offset + colonPos] != ':'; colonPos++) { } string fieldName = Encoding.UTF8.GetString(line.Data, line.Offset, colonPos); if (colonPos == line.Length) // field name without a value - assume empty value { return(new Result { FieldName = fieldName, ValueBytes = new Utf8ByteSpan() }); } int valuePos = colonPos + 1; if (valuePos < line.Length && line.Data[line.Offset + valuePos] == ' ') { valuePos++; // trim a single leading space from the value, if present } return(new Result { FieldName = fieldName, ValueBytes = new Utf8ByteSpan(line.Data, line.Offset + valuePos, line.Length - valuePos) }); }
/// <summary> /// Initializes a new instance of the <see cref="MessageEvent"/> class, /// providing the data as a UTF-8 byte span. /// </summary> /// <param name="name">the event name</param> /// <param name="dataUtf8Bytes">the data received in the server-sent event; /// the <c>MessageEvent</c> will store a reference to the byte array, rather than /// copying it, so it should not be modified afterward by the caller /// </param> /// <param name="lastEventId">the last event identifier, or null</param> /// <param name="origin">the origin URI of the stream</param> public MessageEvent(string name, Utf8ByteSpan dataUtf8Bytes, string lastEventId, Uri origin) { _name = name; _dataString = null; _dataUtf8Bytes = dataUtf8Bytes; _lastEventId = lastEventId; _origin = origin; }
/// <summary> /// Initializes a new instance of the <see cref="MessageEvent"/> class. /// </summary> /// <param name="name">the event name</param> /// <param name="data">the data received in the server-sent event</param> /// <param name="lastEventId">the last event identifier, or null</param> /// <param name="origin">the origin URI of the stream</param> public MessageEvent(string name, string data, string lastEventId, Uri origin) { _name = name; _dataString = data; _dataUtf8Bytes = new Utf8ByteSpan(); _lastEventId = lastEventId; _origin = origin; }
private void ProcessResponseLineUtf8(Utf8ByteSpan content) { if (content.Length == 0) { DispatchEvent(); } else { HandleParsedLine(EventParser.ParseLineUtf8Bytes(content)); } }
/// <summary> /// Tests whether the bytes in this span are the same as another span. /// </summary> /// <param name="other">Another <c>Utf8ByteSpan</c>.</param> /// <returns>True if the two spans have the same length and the same /// data, starting from each one's <c>Offset</c>.</returns> public bool Equals(Utf8ByteSpan other) { var len = Length; if (len != other.Length) { return(false); } int offset = Offset, otherOffset = other.Offset; byte[] data = Data, otherData = other.Data; for (int i = 0; i < len; i++) { if (data[i + offset] != otherData[i + otherOffset]) { return(false); } } return(true); }
private void DispatchEvent() { var name = _eventName ?? Constants.MessageField; _eventName = null; MessageEvent message; if (_eventDataStringBuffer != null) { if (_eventDataStringBuffer.Count == 0) { return; } // remove last item which is always a trailing newline _eventDataStringBuffer.RemoveAt(_eventDataStringBuffer.Count - 1); var dataString = string.Concat(_eventDataStringBuffer); message = new MessageEvent(name, dataString, _lastEventId, _configuration.Uri); _eventDataStringBuffer.Clear(); } else { if (_eventDataUtf8ByteBuffer is null || _eventDataUtf8ByteBuffer.Length == 0) { return; } var dataSpan = new Utf8ByteSpan(_eventDataUtf8ByteBuffer.GetBuffer(), 0, (int)_eventDataUtf8ByteBuffer.Length - 1); // remove trailing newline message = new MessageEvent(name, dataSpan, _lastEventId, _configuration.Uri); // We've now taken ownership of the original buffer; null out the previous // reference to it so a new one will be created next time _eventDataUtf8ByteBuffer = null; } _logger.Debug("Received event \"{0}\"", name); OnMessageReceived(new MessageReceivedEventArgs(message)); }
/// <summary> /// Searches for the next line ending and, if successful, provides the line data. /// </summary> /// <param name="lineOut">if successful, this is set to point to the bytes for the line /// <i>not</i> including any CR/LF; whenever possible this is a reference to the underlying /// buffer, not a copy, so the caller should read/copy it before doing anything else to the /// buffer</param> /// <returns>true if a full line was read, false if we need more data first</returns> public bool ScanToEndOfLine(out Utf8ByteSpan lineOut) { if (_startPos == _count) { _startPos = _count = 0; lineOut = new Utf8ByteSpan(); return(false); } if (_startPos == 0 && _partialLine != null && _partialLine.Position > 0 && _partialLine.GetBuffer()[_partialLine.Position - 1] == '\r') { // This is an edge case where the very last byte we previously saw was a CR, and we didn't know // whether the next byte would be LF or not, but we had to dump the buffer into _partialLine // because it was completely full. So, now we can return the line that's already in _partialLine, // but if the first byte in the buffer is LF we should skip past it. if (_buffer[_startPos] == '\n') { _startPos++; } lineOut = new Utf8ByteSpan(_partialLine.GetBuffer(), 0, (int)_partialLine.Position - 1); // don't include the CR _partialLine = null; return(true); } int startedAt = _startPos, pos = _startPos; while (pos < _count) { var b = _buffer[pos]; if (b == '\n') // LF by itself terminates a line { _startPos = pos + 1; // next line will start after the LF break; } if (b == '\r') { if (pos < (_count - 1)) { _startPos = pos + 1; // next line will start after the CR-- if (_buffer[pos + 1] == '\n') // --unless there was an LF right after that { _startPos++; } break; } else { // CR by itself and CR+LF are both valid line endings in SSE, so if the very // last character we saw was a CR, we can't know when the line is fully read // until we've gotten more data. So we'll need to treat this as an incomplete // line. pos++; break; } } pos++; } if (pos == _count) // we didn't find a line terminator { lineOut = new Utf8ByteSpan(); if (_count < _capacity) { // There's still room in the buffer, so we'll re-scan the line once they add more bytes return(false); } // We need to dump the incomplete line into _partialLine so we can make room in the buffer var partialCount = pos - _startPos; if (_partialLine is null) { _partialLine = new MemoryStream(partialCount); } _partialLine.Write(_buffer, _startPos, partialCount); // Clear the main buffer _startPos = _count = 0; return(false); } if (_partialLine != null && _partialLine.Position > 0) { _partialLine.Write(_buffer, startedAt, pos - startedAt); lineOut = new Utf8ByteSpan(_partialLine.GetBuffer(), 0, (int)_partialLine.Position); _partialLine = null; // If there are still bytes in the main buffer, move them over to make more room. It's // safe for us to do this before the caller has looked at lineOut, because lineOut is now // a reference to the separate _partialLine buffer, not to the main buffer. if (_startPos < _count) { System.Buffer.BlockCopy(_buffer, _startPos, _buffer, 0, _count - _startPos); } _count -= _startPos; _startPos = 0; } else { lineOut = new Utf8ByteSpan(_buffer, startedAt, pos - startedAt); if (_startPos == _count) { // If we've scanned all the data in the buffer, reset _startPos and _count to indicate // that the entire buffer is available for the next read. It's safe for us to do this // before the caller has looked at lineOut, because we're not actually modifying any // bytes in the buffer. It's the caller's responsibility not to modify the buffer // until it has already done whatever needs to be done with the lineOut data. _startPos = _count = 0; } } return(true); }