#if !BESTHTTP_DISABLE_SOCKETIO using System.Text; namespace BestHTTP.SocketIO { using System; using System.Collections.Generic; using BestHTTP.JSON; public sealed class Packet { private enum PayloadTypes : byte { Textual = 0, Binary = 1 } public const string Placeholder = "_placeholder"; #region Public properties /// /// Event type of this packet on the transport layer. /// public TransportEventTypes TransportEvent { get; private set; } /// /// The packet's type in the Socket.IO protocol. /// public SocketIOEventTypes SocketIOEvent { get; private set; } /// /// How many attachment should have this packet. /// public int AttachmentCount { get; private set; } /// /// The internal ack-id of this packet. /// public int Id { get; private set; } /// /// The sender namespace's name. /// public string Namespace { get; private set; } /// /// The payload as a Json string. /// public string Payload { get; private set; } /// /// The decoded event name from the payload string. /// public string EventName { get; private set; } /// /// All binary data attached to this event. /// public List Attachments { get { return attachments; } set { attachments = value; AttachmentCount = attachments != null ? attachments.Count : 0; } } private List attachments; /// /// Property to check whether all attachments are received to this packet. /// public bool HasAllAttachment { get { return Attachments != null && Attachments.Count == AttachmentCount; } } /// /// True if it's already decoded. The DecodedArgs still can be null after the Decode call. /// public bool IsDecoded { get; private set; } /// /// The decoded arguments from the result of a Json string -> c# object convert. /// public object[] DecodedArgs { get; private set; } #endregion #region Constructors /// /// Internal constructor. Don't use it directly! /// internal Packet() { this.TransportEvent = TransportEventTypes.Unknown; this.SocketIOEvent = SocketIOEventTypes.Unknown; this.Payload = string.Empty; } /// /// Internal constructor. Don't use it directly! /// internal Packet(string from) { this.Parse(from); } /// /// Internal constructor. Don't use it directly! /// public Packet(TransportEventTypes transportEvent, SocketIOEventTypes packetType, string nsp, string payload, int attachment = 0, int id = 0) { this.TransportEvent = transportEvent; this.SocketIOEvent = packetType; this.Namespace = nsp; this.Payload = payload; this.AttachmentCount = attachment; this.Id = id; } #endregion #region Public Functions public object[] Decode(BestHTTP.SocketIO.JsonEncoders.IJsonEncoder encoder) { if (IsDecoded || encoder == null) return DecodedArgs; IsDecoded = true; if (string.IsNullOrEmpty(Payload)) return DecodedArgs; List decoded = encoder.Decode(Payload); if (decoded != null && decoded.Count > 0) { if (this.SocketIOEvent == SocketIOEventTypes.Ack || this.SocketIOEvent == SocketIOEventTypes.BinaryAck) DecodedArgs = decoded.ToArray(); else { decoded.RemoveAt(0); DecodedArgs = decoded.ToArray(); } } return DecodedArgs; } /// /// Will set and return with the EventName from the packet's Payload string. /// public string DecodeEventName() { // Already decoded if (!string.IsNullOrEmpty(EventName)) return EventName; // No Payload to decode if (string.IsNullOrEmpty(Payload)) return string.Empty; // Not array encoded, we can't decode if (Payload[0] != '[') return string.Empty; int idx = 1; // Search for the string-begin mark( ' or " chars) while (Payload.Length > idx && Payload[idx] != '"' && Payload[idx] != '\'') idx++; // Reached the end of the string if (Payload.Length <= idx) return string.Empty; int startIdx = ++idx; // Search for the trailing mark of the string while (Payload.Length > idx && Payload[idx] != '"' && Payload[idx] != '\'') idx++; // Reached the end of the string if (Payload.Length <= idx) return string.Empty; return EventName = Payload.Substring(startIdx, idx - startIdx); } public string RemoveEventName(bool removeArrayMarks) { // No Payload to decode if (string.IsNullOrEmpty(Payload)) return string.Empty; // Not array encoded, we can't decode if (Payload[0] != '[') return string.Empty; int idx = 1; // Search for the string-begin mark( ' or " chars) while (Payload.Length > idx && Payload[idx] != '"' && Payload[idx] != '\'') idx++; // Reached the end of the string if (Payload.Length <= idx) return string.Empty; int startIdx = idx; // Search for end of first element, or end of the array marks while (Payload.Length > idx && Payload[idx] != ',' && Payload[idx] != ']') idx++; // Reached the end of the string if (Payload.Length <= ++idx) return string.Empty; string payload = Payload.Remove(startIdx, idx - startIdx); if (removeArrayMarks) payload = payload.Substring(1, payload.Length - 2); return payload; } /// /// Will switch the "{'_placeholder':true,'num':X}" to a the index num X. /// /// True if successfully reconstructed, false otherwise. public bool ReconstructAttachmentAsIndex() { //"452-["multiImage",{"image":true,"buffer1":{"_placeholder":true,"num":0},"buffer2":{"_placeholder":true,"num":1}}]" return PlaceholderReplacer((json, obj) => { int idx = Convert.ToInt32(obj["num"]); this.Payload = this.Payload.Replace(json, idx.ToString()); this.IsDecoded = false; }); } /// /// Will switch the "{'_placeholder':true,'num':X}" to a the data as a base64 encoded string. /// /// True if successfully reconstructed, false otherwise. public bool ReconstructAttachmentAsBase64() { //"452-["multiImage",{"image":true,"buffer1":{"_placeholder":true,"num":0},"buffer2":{"_placeholder":true,"num":1}}]" if (!HasAllAttachment) return false; return PlaceholderReplacer((json, obj) => { int idx = Convert.ToInt32(obj["num"]); this.Payload = this.Payload.Replace(json, string.Format("\"{0}\"", Convert.ToBase64String(this.Attachments[idx]))); this.IsDecoded = false; }); } #endregion #region Internal Functions /// /// Parse the packet from a server sent textual data. The Payload will be the raw json string. /// internal void Parse(string from) { int idx = 0; this.TransportEvent = (TransportEventTypes)ToInt(from[idx++]); if (from.Length > idx && ToInt(from[idx]) >= 0) this.SocketIOEvent = (SocketIOEventTypes)ToInt(from[idx++]); else this.SocketIOEvent = SocketIOEventTypes.Unknown; // Parse Attachment if (this.SocketIOEvent == SocketIOEventTypes.BinaryEvent || this.SocketIOEvent == SocketIOEventTypes.BinaryAck) { int endIdx = from.IndexOf('-', idx); if (endIdx == -1) endIdx = from.Length; int attachment = 0; int.TryParse(from.Substring(idx, endIdx - idx), out attachment); this.AttachmentCount = attachment; idx = endIdx + 1; } // Parse Namespace if (from.Length > idx && from[idx] == '/') { int endIdx = from.IndexOf(',', idx); if (endIdx == -1) endIdx = from.Length; this.Namespace = from.Substring(idx, endIdx - idx); idx = endIdx + 1; } else this.Namespace = "/"; // Parse Id if (from.Length > idx && ToInt(from[idx]) >= 0) { int startIdx = idx++; while (from.Length > idx && ToInt(from[idx]) >= 0) idx++; int id = 0; int.TryParse(from.Substring(startIdx, idx - startIdx), out id); this.Id = id; } // What left is the payload data if (from.Length > idx) this.Payload = from.Substring(idx); else this.Payload = string.Empty; } /// /// Custom function instead of char.GetNumericValue, as it throws an error under WebGL using the new 4.x runtime. /// It will return the value of the char if it's a numeric one, otherwise -1. /// private int ToInt(char ch) { int charValue = Convert.ToInt32(ch); int num = charValue - '0'; if (num < 0 || num > 9) return -1; return num; } /// /// Encodes this packet to a Socket.IO formatted string. /// internal string Encode() { StringBuilder builder = new StringBuilder(); // SaveLocal to Message if not set, and we are sending attachments if (this.TransportEvent == TransportEventTypes.Unknown && this.AttachmentCount > 0) this.TransportEvent = TransportEventTypes.Message; if (this.TransportEvent != TransportEventTypes.Unknown) builder.Append(((int)this.TransportEvent).ToString()); // SaveLocal to BinaryEvent if not set, and we are sending attachments if (this.SocketIOEvent == SocketIOEventTypes.Unknown && this.AttachmentCount > 0) this.SocketIOEvent = SocketIOEventTypes.BinaryEvent; if (this.SocketIOEvent != SocketIOEventTypes.Unknown) builder.Append(((int)this.SocketIOEvent).ToString()); if (this.SocketIOEvent == SocketIOEventTypes.BinaryEvent || this.SocketIOEvent == SocketIOEventTypes.BinaryAck) { builder.Append(this.AttachmentCount.ToString()); builder.Append("-"); } // Add the namespace. If there is any other then the root nsp ("/") // then we have to add a trailing "," if we have more data. bool nspAdded = false; if (this.Namespace != "/") { builder.Append(this.Namespace); nspAdded = true; } // ack id, if any if (this.Id != 0) { if (nspAdded) { builder.Append(","); nspAdded = false; } builder.Append(this.Id.ToString()); } // payload if (!string.IsNullOrEmpty(this.Payload)) { if (nspAdded) { builder.Append(","); nspAdded = false; } builder.Append(this.Payload); } return builder.ToString(); } /// /// Encodes this packet to a Socket.IO formatted byte array. /// internal byte[] EncodeBinary() { if (AttachmentCount != 0 || (Attachments != null && Attachments.Count != 0)) { if (Attachments == null) throw new ArgumentException("packet.Attachments are null!"); if (AttachmentCount != Attachments.Count) throw new ArgumentException("packet.AttachmentCount != packet.Attachments.Count. Use the packet.AddAttachment function to add data to a packet!"); } // Encode it as usual string encoded = Encode(); // Convert it to a byte[] byte[] payload = Encoding.UTF8.GetBytes(encoded); // Encode it to a message byte[] buffer = EncodeData(payload, PayloadTypes.Textual, null); // If there is any attachment, convert them too, and append them after each other if (AttachmentCount != 0) { int idx = buffer.Length; // List to temporarily hold the converted attachments List attachmentDatas = new List(AttachmentCount); // The sum size of the converted attachments to be able to resize our buffer only once. This way we can avoid some GC garbage int attachmentDataSize = 0; // Encode our attachments, and store them in our list for (int i = 0; i < AttachmentCount; i++) { byte[] tmpBuff = EncodeData(Attachments[i], PayloadTypes.Binary, new byte[] { 4 }); attachmentDatas.Add(tmpBuff); attachmentDataSize += tmpBuff.Length; } // Resize our buffer once Array.Resize(ref buffer, buffer.Length + attachmentDataSize); // And copy all data into it for (int i = 0; i < AttachmentCount; ++i) { byte[] data = attachmentDatas[i]; Array.Copy(data, 0, buffer, idx, data.Length); idx += data.Length; } } // Return the buffer return buffer; } /// /// Will add the byte[] that the server sent to the attachments list. /// internal void AddAttachmentFromServer(byte[] data, bool copyFull) { if (data == null || data.Length == 0) return; if (this.attachments == null) this.attachments = new List(this.AttachmentCount); if (copyFull) this.Attachments.Add(data); else { byte[] buff = new byte[data.Length - 1]; Array.Copy(data, 1, buff, 0, data.Length - 1); this.Attachments.Add(buff); } } #endregion #region Private Helper Functions /// /// Encodes a byte array to a Socket.IO binary encoded message /// private byte[] EncodeData(byte[] data, PayloadTypes type, byte[] afterHeaderData) { // Packet binary encoding: // [ 0|1 ][ length of data ][ FF ][data] // <1 = binary, 0 = string>[...] // Get the length of the payload. Socket.IO uses a wasteful encoding to send the length of the data. // If the data is 16 bytes we have to send the length as two bytes: byte value of the character '1' and byte value of the character '6'. // Instead of just one byte: 0xF. If the payload is 123 bytes, we can't send as 0x7B... int afterHeaderLength = (afterHeaderData != null ? afterHeaderData.Length : 0); string lenStr = (data.Length + afterHeaderLength).ToString(); byte[] len = new byte[lenStr.Length]; for (int cv = 0; cv < lenStr.Length; ++cv) len[cv] = (byte)char.GetNumericValue(lenStr[cv]); // We need another buffer to store the final data byte[] buffer = new byte[data.Length + len.Length + 2 + afterHeaderLength]; // The payload is textual -> 0 buffer[0] = (byte)type; // Copy the length of the data for (int cv = 0; cv < len.Length; ++cv) buffer[1 + cv] = len[cv]; int idx = 1 + len.Length; // End of the header data buffer[idx++] = 0xFF; if (afterHeaderData != null && afterHeaderData.Length > 0) { Array.Copy(afterHeaderData, 0, buffer, idx, afterHeaderData.Length); idx += afterHeaderData.Length; } // Copy our payload data to the buffer Array.Copy(data, 0, buffer, idx, data.Length); return buffer; } /// /// Searches for the "{'_placeholder':true,'num':X}" string, and will call the given action to modify the PayLoad /// private bool PlaceholderReplacer(Action> onFound) { if (string.IsNullOrEmpty(this.Payload)) return false; // Find the first index of the "_placeholder" str int placeholderIdx = this.Payload.IndexOf(Placeholder); while (placeholderIdx >= 0) { // Find the object-start token int startIdx = placeholderIdx; while (this.Payload[startIdx] != '{') startIdx--; // Find the object-end token int endIdx = placeholderIdx; while (this.Payload.Length > endIdx && this.Payload[endIdx] != '}') endIdx++; // We reached the end if (this.Payload.Length <= endIdx) return false; // Get the object, and decode it string placeholderJson = this.Payload.Substring(startIdx, endIdx - startIdx + 1); bool success = false; Dictionary obj = Json.Decode(placeholderJson, ref success) as Dictionary; if (!success) return false; // Check for presence and value of _placeholder object value; if (!obj.TryGetValue(Placeholder, out value) || !(bool)value) return false; // Check for presence of num if (!obj.TryGetValue("num", out value)) return false; // Let do, what we have to do onFound(placeholderJson, obj); // Find the next attachment if there is any placeholderIdx = this.Payload.IndexOf(Placeholder); } return true; } #endregion #region Overrides and Interface Implementations /// /// Returns with the Payload of this packet. /// public override string ToString() { return this.Payload; } /// /// Will clone this packet to an identical packet instance. /// internal Packet Clone() { Packet packet = new Packet(this.TransportEvent, this.SocketIOEvent, this.Namespace, this.Payload, 0, this.Id); packet.EventName = this.EventName; packet.AttachmentCount = this.AttachmentCount; packet.attachments = this.attachments; return packet; } #endregion } } #endif