PageRenderTime 155ms CodeModel.GetById 127ms app.highlight 20ms RepoModel.GetById 2ms app.codeStats 0ms

/Utilities/Helpers/StreamHelper.cs

#
C# | 575 lines | 327 code | 31 blank | 217 comment | 32 complexity | 7cee112d200dfe644dec0181d4e5fc80 MD5 | raw file
  1using System;
  2using System.IO;
  3using NUnit.Framework;
  4
  5namespace Delta.Utilities.Helpers
  6{
  7	/// <summary>
  8	/// StreamHelper: Adding cool functions for stream stuff (writing/reading).
  9	/// For example to send small numbers, which are most likely not bigger
 10	/// than 255 or 65536 as bytes or shorts and not as full 4 byte (Int32).
 11	/// </summary>
 12	public static class StreamHelper
 13	{
 14		#region WriteNumberMostlySmallerThan254 (Static)
 15		/// <summary>
 16		/// The first byte is used for the type of number we save:
 17		/// smaller than 254: Just use this number as positive number 0-253.
 18		/// - 254: +2 bytes for any ushort number (positive number)
 19		/// - 255: +4 bytes for any int number (negative or positive number)
 20		/// This function should be used if you are pretty sure this number
 21		/// is a byte, else this function will probably not be the optimal choice.
 22		/// Very good for enums and small values, flags, etc.
 23		/// Used all over the place for optimized network package code!
 24		/// </summary>
 25		/// <param name="smallNumber">Small number that should be less than 254
 26		/// (and not negative) if possible, but it can be any number.</param>
 27		/// <param name="writer">Writer to write compressed number into</param>
 28		public static void WriteNumberMostlySmallerThan254(
 29			BinaryWriter writer, int smallNumber)
 30		{
 31			if (smallNumber < 0 ||
 32			    smallNumber >= 254)
 33			{
 34				// Does fit into ushort?
 35				if (smallNumber >= ushort.MinValue &&
 36				    smallNumber <= ushort.MaxValue)
 37				{
 38					writer.Write((byte)254);
 39					writer.Write((ushort)smallNumber);
 40				}
 41				else
 42				{
 43					// We have to use a full int
 44					writer.Write((byte)255);
 45					writer.Write(smallNumber);
 46				}
 47			}
 48			else
 49			{
 50				// This is what should happen most of the time, we got a small number!
 51				writer.Write((byte)smallNumber);
 52			}
 53		}
 54		#endregion
 55
 56		#region WriteNumberMostlySmallerThan16Bit (Static)
 57		/// <summary>
 58		/// Similar to WriteNumberMostlySmallerThan255, but will write 
 59		/// at least 2 bytes for shorts, except short.MinValue, which is
 60		/// used to mark that this value does not fit into a short,
 61		/// we will write an 4 byte int instead (total length is 6 bytes then).
 62		/// Use this function to save a byte more if you know your number
 63		/// is most likely in the range -32768 - 32768 (good for counters,
 64		/// statistic values, points, etc.).
 65		/// Of course this function will always use 2 bytes, even if
 66		/// we store something smaller than 255 (in this case use other function)
 67		/// </summary>
 68		/// <param name="smallNumber">Small Number</param>
 69		/// <param name="writer">Writer</param>
 70		public static void WriteNumberMostlySmallerThan16Bit(
 71			BinaryWriter writer, int smallNumber)
 72		{
 73			if (smallNumber > short.MinValue &&
 74			    smallNumber <= short.MaxValue)
 75			{
 76				// Just store value, fits into 16 Bit
 77				writer.Write((short)smallNumber);
 78			}
 79			else
 80			{
 81				// Write short.MinValue to mark we have to use an int here
 82				writer.Write(short.MinValue);
 83				writer.Write(smallNumber);
 84			}
 85		}
 86		#endregion
 87
 88		#region WriteNumberMostlySmallerThan32Bit (Static)
 89		/// <summary>
 90		/// Similar to WriteNumberMostlySmallerThan16Bit, but will
 91		/// write 4 bytes for everything in the range of an int. However,
 92		/// maybe we use the full capacity of a long, then int.MinValue is
 93		/// saved and then 8 bytes for the long value.MaxValue, else
 94		/// we got a long value and then 8 bytes for the number.
 95		/// Use this function to save a long if you know your number
 96		/// is most likely in the range -2bil - +2bil (good for long counters).
 97		/// </summary>
 98		/// <param name="number">Number</param>
 99		/// <param name="writer">Writer</param>
100		public static void WriteNumberMostlySmallerThan32Bit(
101			BinaryWriter writer, long number)
102		{
103			if (number > int.MinValue &&
104			    number <= int.MaxValue)
105			{
106				writer.Write((int)number);
107			}
108			else
109			{
110				// Write int.MinValue to mark we have to use a long here
111				writer.Write(int.MinValue);
112				writer.Write(number);
113			}
114		}
115		#endregion
116
117		#region ReadNumberMostlySmallerThan254 (Static)
118		/// <summary>
119		/// Read number written with WriteNumberMostlySmallerThan254,
120		/// see that function for an explanation, returns number we submitted.
121		/// Could read 1 byte (if value is between 0 and 253) or 3 (1 byte for
122		/// the type=254 and 2 bytes for the ushort value) or even 5 bytes
123		/// (in case an int was used).
124		/// </summary>
125		/// <param name="reader">Reader to extract the data from</param>
126		/// <returns>Number that was stored in the stream with help of the
127		/// WriteNumberMostlySmallerThan254 method above.</returns>
128		public static int ReadNumberMostlySmallerThan254(BinaryReader reader)
129		{
130			int num = reader.ReadByte();
131			if (num == 254)
132			{
133				// A ushort number was used
134				num = reader.ReadUInt16();
135			}
136			else if (num == 255)
137			{
138				// An int number was used
139				num = reader.ReadInt32();
140			}
141			return num;
142		}
143		#endregion
144
145		#region ReadNumberMostlySmallerThan16Bit (Static)
146		/// <summary>
147		/// Read number written with WriteNumberMostlySmallerThan16Bit,
148		/// see that function for an explanation, returns number we submitted.
149		/// Could read 2 byte (if value is a short) or 6 bytes for integers.
150		/// </summary>
151		/// <param name="reader">Reader to extract the data from</param>
152		/// <returns>Number that was stored in the stream with help of the
153		/// WriteNumberMostlySmallerThan16Bit method above.</returns>
154		public static int ReadNumberMostlySmallerThan16Bit(BinaryReader reader)
155		{
156			int num = reader.ReadInt16();
157			if (num == short.MinValue)
158			{
159				num = reader.ReadInt32();
160			}
161			return num;
162		}
163		#endregion
164
165		#region ReadNumberMostlySmallerThan32Bit (Static)
166		/// <summary>
167		/// Read number written with WriteNumberMostlySmallerThan32Bit,
168		/// see that function for an explanation,
169		/// returns number we submitted there!
170		/// We read 4 or 12 bytes (obviously we return a long, because if
171		/// we got more than 32 bits, it won't fit!)
172		/// </summary>
173		/// <param name="reader">Reader to extract the data from</param>
174		/// <returns>Number that was stored in the stream with help of the
175		/// WriteNumberMostlySmallerThan32Bit method above.</returns>
176		public static long ReadNumberMostlySmallerThan32Bit(BinaryReader reader)
177		{
178			long num = reader.ReadInt32();
179			if (num == int.MinValue)
180			{
181				num = reader.ReadInt64();
182			}
183			return num;
184		}
185		#endregion
186
187		#region GetDataLengthOfNumberMostlySmallerThan254 (Static)
188		/// <summary>
189		/// Helper method to figure out how many bytes we need to store the given
190		/// small number (which should be mostly 1 byte because we expect values
191		/// below 254, but it can go up to 3 or 5 bytes for big numbers).
192		/// </summary>
193		/// <param name="smallNumber">Small number that should be less than 254
194		/// (and not negative) if possible, but it can be any number.</param>
195		/// <returns>Number of bytes that we would need to save this number.
196		/// Does not actually store anything, use
197		/// <see cref="WriteNumberMostlySmallerThan254"/> for that.</returns>
198		public static int GetDataLengthOfNumberMostlySmallerThan254(
199			int smallNumber)
200		{
201			if (smallNumber < 0 ||
202			    smallNumber >= 254)
203			{
204				// Does fit into ushort?
205				if (smallNumber >= ushort.MinValue &&
206				    smallNumber <= ushort.MaxValue)
207				{
208					return 3;
209				}
210				else
211				{
212					// We have to use a full int
213					return 5;
214				}
215			}
216			else
217			{
218				// This is what should happen most of the time, we got a small number!
219				return 1;
220			}
221		}
222		#endregion
223
224		#region TryToGetNumberMostlySmallerThan254 (Static)
225		/// <summary>
226		/// Helper function for network stream reading, we check if
227		/// current byte array (we have no guarantee whole message has arrived,
228		/// we have to check how much we got and if this is enough)
229		/// contains full number in WriteNumberMostlySmallerThan255 style.
230		/// Note: Will not restore the stream position because the caller usually
231		/// does it anyway when we have to wait for more data.
232		/// </summary>
233		/// <returns>True if we successfully could read the number, false if
234		/// there was not enough data and we have to wait for more data</returns>
235		/// <param name="number">Number</param>
236		/// <param name="reader">Reader</param>
237		public static bool TryToGetNumberMostlySmallerThan254(
238			BinaryReader reader, out int number)
239		{
240			if (reader.BaseStream.Length - reader.BaseStream.Position >= 1)
241			{
242				byte numberAsByte = reader.ReadByte();
243				if (numberAsByte < 254)
244				{
245					// Just a byte, return it
246					number = numberAsByte;
247					return true;
248				}
249				else if (numberAsByte == 254)
250				{
251					// Do we have enough data for the next 2 bytes?
252					if (reader.BaseStream.Length - reader.BaseStream.Position >= 2)
253					{
254						// A short was used
255						number = reader.ReadUInt16();
256						return true;
257					}
258				}
259				else // This is an int value
260				{
261					// Do we have enough data for the next 4 bytes?
262					if (reader.BaseStream.Length - reader.BaseStream.Position >= 4)
263					{
264						number = reader.ReadInt32();
265						return true;
266					}
267				}
268			}
269
270			// Insufficient data, we need more data, cannot return number yet!
271			number = 0;
272			return false;
273		}
274		#endregion
275
276		#region LoadWithLength (Static)
277		/// <summary>
278		/// Helper method to load the length of data plus the data itself. Very
279		/// useful for byte arrays that obviously need the size saved out too.
280		/// </summary>
281		/// <param name="reader">Reader to read the byte data from</param>
282		/// <returns>
283		/// Returns the byte array with the length specified in the stream.
284		/// </returns>
285		public static byte[] LoadWithLength(BinaryReader reader)
286		{
287			// Grab the length first (as an int)
288			int dataLength = reader.ReadInt32();
289			// Create a byte array and load all the byte data into it.
290			byte[] data = new byte[dataLength];
291			reader.Read(data, 0, dataLength);
292
293			return data;
294		}
295
296		/// <summary>
297		/// Generic helper method to load an ISaveLoadBinary object back in. Use
298		/// SaveWithLength to save such an object out into a binary stream. This
299		/// method is most useful if called from the Factory.Load method, which
300		/// will also create the type for you and load all data in for you.
301		/// This method will not crash, but report a warning in the log instead,
302		/// when the ISaveLoadBinary loading fails (e.g. not enough data).
303		/// </summary>
304		/// <param name="reader">Reader to read the object data (T) from</param>
305		/// <param name="objectToLoad">
306		/// ISaveLoadBinary object for the data to be loaded into.
307		/// </param>
308		public static void LoadWithLength<T>(BinaryReader reader, T objectToLoad)
309			where T : ISaveLoadBinary
310		{
311			// Grab the length first (as an int)
312			int dataLength = reader.ReadInt32();
313			// Create a byte array and load all the byte data into it.
314			byte[] data = new byte[dataLength];
315			reader.Read(data, 0, dataLength);
316
317			// Create an extra memory stream just for this data (loading might fail)
318			try
319			{
320				using (MemoryStream tempStream = new MemoryStream(data))
321				{
322					BinaryReader tempReader = new BinaryReader(tempStream);
323					// Load the data from this temporary stream
324					objectToLoad.Load(tempReader);
325				} // using
326			} // try
327			catch (Exception ex)
328			{
329				Log.Warning(
330					"Failed to load object " + objectToLoad + " of type (" + typeof(T) +
331					") from the byte data (dataLength=" + dataLength + ", data=" +
332					data.Write() + "): " + ex);
333			} // catch
334		}
335		#endregion
336
337		#region SaveWithLength (Static)
338		/// <summary>
339		/// Helper method to not just save an array out, but also keep track of
340		/// how many bytes were saved and store those first in case anything goes
341		/// wrong when loading the data later (e.g. the format has changed and
342		/// more or less data is required). Use the LoadWithLength method to load
343		/// the data again.
344		/// </summary>
345		/// <param name="writer">Writer to save the length and the data itself
346		/// into.</param>
347		/// <param name="dataToSave">Data to save as an byte array.</param>
348		public static void SaveWithLength(BinaryWriter writer,
349			byte[] dataToSave)
350		{
351			// Save the length first (as an int)
352			writer.Write(dataToSave.Length);
353			// And finally save the byte data itself
354			writer.Write(dataToSave);
355		}
356
357		/// <summary>
358		/// Helper method to not just save an array out, but also keep track of
359		/// how many bytes were saved and store those first in case anything goes
360		/// wrong when loading the data later (e.g. the format has changed and
361		/// more or less data is required). Use the LoadWithLength method to load
362		/// the data again.
363		/// </summary>
364		/// <param name="writer">Writer to save the length and the data itself
365		/// into.</param>
366		/// <param name="dataToSave">Data to save as a MemoryStream</param>
367		public static void SaveWithLength(BinaryWriter writer,
368			MemoryStream dataToSave)
369		{
370			SaveWithLength(writer, dataToSave.ToArray());
371		}
372
373		/// <summary>
374		/// Helper method to not just save an object out, but also keep track of
375		/// how many bytes were saved and store those first in case anything goes
376		/// wrong when loading the data later (e.g. the format has changed and
377		/// more or less data is required). Use the LoadWithLength method to load
378		/// the data again.
379		/// </summary>
380		/// <param name="writer">Writer to save the length and the data itself
381		/// into.</param>
382		/// <param name="dataToSave">Data we want to save via the ISaveLoadBinary
383		/// interface, which does not know its own length.</param>
384		public static void SaveWithLength(BinaryWriter writer,
385			ISaveLoadBinary dataToSave)
386		{
387			// Create an extra memory stream just for this data
388			using (MemoryStream tempStream = new MemoryStream())
389			{
390				BinaryWriter tempWriter = new BinaryWriter(tempStream);
391
392				// Save the data into this temporary stream
393				dataToSave.Save(tempWriter);
394
395				// Now grab the data
396				byte[] data = tempStream.ToArray();
397
398				// And save it out together with the data length into dataWriter.
399				writer.Write(data.Length);
400				writer.Write(data, 0, data.Length);
401			} // using
402		}
403		#endregion
404
405		#region GetSavedData (Static)
406		/// <summary>
407		/// Gets all saved pure data of the given object without any information
408		/// about the data length at the beginning.
409		/// </summary>
410		/// <param name="dataToSave">
411		/// Data we want to save via the ISaveLoadBinary interface, which does not
412		/// know its own length.
413		/// </param>
414		public static byte[] GetSavedData(ISaveLoadBinary dataToSave)
415		{
416			// Create an extra memory stream just for this data
417			using (MemoryStream tempStream = new MemoryStream())
418			{
419				BinaryWriter tempWriter = new BinaryWriter(tempStream);
420
421				// Save the data into this temporary stream
422				dataToSave.Save(tempWriter);
423
424				// Now grab the data
425				return tempStream.ToArray();
426			} // using
427		}
428		#endregion
429
430		#region GetSavedDataWithLength (Static)
431		/// <summary>
432		/// Gets all saved data of the given object with the number of the saved
433		/// bytes at the beginning.
434		/// </summary>
435		/// <param name="dataToSave">
436		/// Data we want to save via the ISaveLoadBinary interface, which does not
437		/// know its own length.
438		/// </param>
439		public static byte[] GetSavedDataWithLength(ISaveLoadBinary dataToSave)
440		{
441			// Create an extra memory stream just for this data
442			using (MemoryStream tempStream = new MemoryStream())
443			{
444				BinaryWriter tempWriter = new BinaryWriter(tempStream);
445
446				// Save the data into this temporary stream
447				SaveWithLength(tempWriter, dataToSave);
448
449				// Now grab the data
450				return tempStream.ToArray();
451			} // using
452		}
453		#endregion
454
455		#region Compare (Static)
456		/// <summary>
457		/// Helper method to compare two memory streams. Will return true if
458		/// both streams are null or empty. If the streams have data, they must
459		/// match exactly byte for byte to return true here. In all other cases
460		/// false is returned.
461		/// </summary>
462		/// <param name="memoryStream1">First memory stream to check</param>
463		/// <param name="memoryStream2">Second memory stream to check</param>
464		/// <returns>True if both memory streams match (either both empty or null
465		/// or both have exactly the same data), otherwise false.</returns>
466		public static bool Compare(MemoryStream memoryStream1,
467			MemoryStream memoryStream2)
468		{
469			// If both streams are empty, then return true, they are equal.
470			if ((memoryStream1 == null ||
471				memoryStream1.Length == 0) &&
472				(memoryStream2 == null ||
473				memoryStream2.Length == 0))
474			{
475				return true;
476			}
477
478			// Otherwise return false if only one of the stream is empty or if
479			// the lengths are different.
480			if (memoryStream1 == null ||
481				memoryStream2 == null ||
482				memoryStream1.Length != memoryStream2.Length)
483			{
484				return false;
485			}
486
487			// And finally check if both streams have the same data
488			return ArrayHelper.Compare(memoryStream1.ToArray(),
489				memoryStream2.ToArray());
490		}
491		#endregion
492
493		/// <summary>
494		/// Test class for the StreamHelper
495		/// </summary>
496		internal class StreamHelperTests
497		{
498			#region TestWritingAndReadingNumbers (Static)
499			/// <summary>
500			/// Test writing and reading numbers
501			/// </summary>
502			[Test]
503			public static void TestWritingAndReadingNumbers()
504			{
505				using (MemoryStream testStream = new MemoryStream())
506				{
507					BinaryWriter writer = new BinaryWriter(testStream);
508					// Check if we can really put any number in there!
509					WriteNumberMostlySmallerThan254(writer, 0);
510					WriteNumberMostlySmallerThan254(writer, -1);
511					WriteNumberMostlySmallerThan254(writer, 253);
512					WriteNumberMostlySmallerThan254(writer, 255);
513					WriteNumberMostlySmallerThan254(writer, 234210);
514					WriteNumberMostlySmallerThan16Bit(writer, -1340);
515					WriteNumberMostlySmallerThan16Bit(writer, +32140);
516					WriteNumberMostlySmallerThan16Bit(writer, +32940);
517					WriteNumberMostlySmallerThan16Bit(writer, -32940);
518					WriteNumberMostlySmallerThan32Bit(writer, +33422940);
519					WriteNumberMostlySmallerThan32Bit(writer,
520						+334229349840);
521					WriteNumberMostlySmallerThan32Bit(writer,
522						-39028159023482);
523
524					testStream.Position = 0;
525					BinaryReader reader = new BinaryReader(testStream);
526					Assert.Equal(0,
527						ReadNumberMostlySmallerThan254(reader));
528					Assert.Equal(-1,
529						ReadNumberMostlySmallerThan254(reader));
530					Assert.Equal(253,
531						ReadNumberMostlySmallerThan254(reader));
532					Assert.Equal(255,
533						ReadNumberMostlySmallerThan254(reader));
534					Assert.Equal(234210,
535						ReadNumberMostlySmallerThan254(reader));
536					Assert.Equal(-1340,
537						ReadNumberMostlySmallerThan16Bit(reader));
538					Assert.Equal(+32140,
539						ReadNumberMostlySmallerThan16Bit(reader));
540					Assert.Equal(+32940,
541						ReadNumberMostlySmallerThan16Bit(reader));
542					Assert.Equal(-32940,
543						ReadNumberMostlySmallerThan16Bit(reader));
544					Assert.Equal(+33422940,
545						ReadNumberMostlySmallerThan32Bit(reader));
546					Assert.Equal(+334229349840,
547						ReadNumberMostlySmallerThan32Bit(reader));
548					Assert.Equal(-39028159023482,
549						ReadNumberMostlySmallerThan32Bit(reader));
550				}
551			}
552			#endregion
553
554			#region GetDataLengthOfNumberMostlySmallerThan254
555			/// <summary>
556			/// Test the GetDataLengthOfNumberMostlySmallerThan254 method.
557			/// </summary>
558			[Test]
559			public void GetDataLengthOfNumberMostlySmallerThan254()
560			{
561				Assert.Equal(1,
562					StreamHelper.GetDataLengthOfNumberMostlySmallerThan254(1));
563				Assert.Equal(1,
564					StreamHelper.GetDataLengthOfNumberMostlySmallerThan254(100));
565				Assert.Equal(1,
566					StreamHelper.GetDataLengthOfNumberMostlySmallerThan254(253));
567				Assert.Equal(3,
568					StreamHelper.GetDataLengthOfNumberMostlySmallerThan254(254));
569				Assert.Equal(5,
570					StreamHelper.GetDataLengthOfNumberMostlySmallerThan254(139592532));
571			}
572			#endregion
573		}
574	}
575}