/Utilities/Helpers/StreamHelper.cs
C# | 575 lines | 327 code | 31 blank | 217 comment | 32 complexity | 7cee112d200dfe644dec0181d4e5fc80 MD5 | raw file
Possible License(s): Apache-2.0
- using System;
- using System.IO;
- using NUnit.Framework;
-
- namespace Delta.Utilities.Helpers
- {
- /// <summary>
- /// StreamHelper: Adding cool functions for stream stuff (writing/reading).
- /// For example to send small numbers, which are most likely not bigger
- /// than 255 or 65536 as bytes or shorts and not as full 4 byte (Int32).
- /// </summary>
- public static class StreamHelper
- {
- #region WriteNumberMostlySmallerThan254 (Static)
- /// <summary>
- /// The first byte is used for the type of number we save:
- /// smaller than 254: Just use this number as positive number 0-253.
- /// - 254: +2 bytes for any ushort number (positive number)
- /// - 255: +4 bytes for any int number (negative or positive number)
- /// This function should be used if you are pretty sure this number
- /// is a byte, else this function will probably not be the optimal choice.
- /// Very good for enums and small values, flags, etc.
- /// Used all over the place for optimized network package code!
- /// </summary>
- /// <param name="smallNumber">Small number that should be less than 254
- /// (and not negative) if possible, but it can be any number.</param>
- /// <param name="writer">Writer to write compressed number into</param>
- public static void WriteNumberMostlySmallerThan254(
- BinaryWriter writer, int smallNumber)
- {
- if (smallNumber < 0 ||
- smallNumber >= 254)
- {
- // Does fit into ushort?
- if (smallNumber >= ushort.MinValue &&
- smallNumber <= ushort.MaxValue)
- {
- writer.Write((byte)254);
- writer.Write((ushort)smallNumber);
- }
- else
- {
- // We have to use a full int
- writer.Write((byte)255);
- writer.Write(smallNumber);
- }
- }
- else
- {
- // This is what should happen most of the time, we got a small number!
- writer.Write((byte)smallNumber);
- }
- }
- #endregion
-
- #region WriteNumberMostlySmallerThan16Bit (Static)
- /// <summary>
- /// Similar to WriteNumberMostlySmallerThan255, but will write
- /// at least 2 bytes for shorts, except short.MinValue, which is
- /// used to mark that this value does not fit into a short,
- /// we will write an 4 byte int instead (total length is 6 bytes then).
- /// Use this function to save a byte more if you know your number
- /// is most likely in the range -32768 - 32768 (good for counters,
- /// statistic values, points, etc.).
- /// Of course this function will always use 2 bytes, even if
- /// we store something smaller than 255 (in this case use other function)
- /// </summary>
- /// <param name="smallNumber">Small Number</param>
- /// <param name="writer">Writer</param>
- public static void WriteNumberMostlySmallerThan16Bit(
- BinaryWriter writer, int smallNumber)
- {
- if (smallNumber > short.MinValue &&
- smallNumber <= short.MaxValue)
- {
- // Just store value, fits into 16 Bit
- writer.Write((short)smallNumber);
- }
- else
- {
- // Write short.MinValue to mark we have to use an int here
- writer.Write(short.MinValue);
- writer.Write(smallNumber);
- }
- }
- #endregion
-
- #region WriteNumberMostlySmallerThan32Bit (Static)
- /// <summary>
- /// Similar to WriteNumberMostlySmallerThan16Bit, but will
- /// write 4 bytes for everything in the range of an int. However,
- /// maybe we use the full capacity of a long, then int.MinValue is
- /// saved and then 8 bytes for the long value.MaxValue, else
- /// we got a long value and then 8 bytes for the number.
- /// Use this function to save a long if you know your number
- /// is most likely in the range -2bil - +2bil (good for long counters).
- /// </summary>
- /// <param name="number">Number</param>
- /// <param name="writer">Writer</param>
- public static void WriteNumberMostlySmallerThan32Bit(
- BinaryWriter writer, long number)
- {
- if (number > int.MinValue &&
- number <= int.MaxValue)
- {
- writer.Write((int)number);
- }
- else
- {
- // Write int.MinValue to mark we have to use a long here
- writer.Write(int.MinValue);
- writer.Write(number);
- }
- }
- #endregion
-
- #region ReadNumberMostlySmallerThan254 (Static)
- /// <summary>
- /// Read number written with WriteNumberMostlySmallerThan254,
- /// see that function for an explanation, returns number we submitted.
- /// Could read 1 byte (if value is between 0 and 253) or 3 (1 byte for
- /// the type=254 and 2 bytes for the ushort value) or even 5 bytes
- /// (in case an int was used).
- /// </summary>
- /// <param name="reader">Reader to extract the data from</param>
- /// <returns>Number that was stored in the stream with help of the
- /// WriteNumberMostlySmallerThan254 method above.</returns>
- public static int ReadNumberMostlySmallerThan254(BinaryReader reader)
- {
- int num = reader.ReadByte();
- if (num == 254)
- {
- // A ushort number was used
- num = reader.ReadUInt16();
- }
- else if (num == 255)
- {
- // An int number was used
- num = reader.ReadInt32();
- }
- return num;
- }
- #endregion
-
- #region ReadNumberMostlySmallerThan16Bit (Static)
- /// <summary>
- /// Read number written with WriteNumberMostlySmallerThan16Bit,
- /// see that function for an explanation, returns number we submitted.
- /// Could read 2 byte (if value is a short) or 6 bytes for integers.
- /// </summary>
- /// <param name="reader">Reader to extract the data from</param>
- /// <returns>Number that was stored in the stream with help of the
- /// WriteNumberMostlySmallerThan16Bit method above.</returns>
- public static int ReadNumberMostlySmallerThan16Bit(BinaryReader reader)
- {
- int num = reader.ReadInt16();
- if (num == short.MinValue)
- {
- num = reader.ReadInt32();
- }
- return num;
- }
- #endregion
-
- #region ReadNumberMostlySmallerThan32Bit (Static)
- /// <summary>
- /// Read number written with WriteNumberMostlySmallerThan32Bit,
- /// see that function for an explanation,
- /// returns number we submitted there!
- /// We read 4 or 12 bytes (obviously we return a long, because if
- /// we got more than 32 bits, it won't fit!)
- /// </summary>
- /// <param name="reader">Reader to extract the data from</param>
- /// <returns>Number that was stored in the stream with help of the
- /// WriteNumberMostlySmallerThan32Bit method above.</returns>
- public static long ReadNumberMostlySmallerThan32Bit(BinaryReader reader)
- {
- long num = reader.ReadInt32();
- if (num == int.MinValue)
- {
- num = reader.ReadInt64();
- }
- return num;
- }
- #endregion
-
- #region GetDataLengthOfNumberMostlySmallerThan254 (Static)
- /// <summary>
- /// Helper method to figure out how many bytes we need to store the given
- /// small number (which should be mostly 1 byte because we expect values
- /// below 254, but it can go up to 3 or 5 bytes for big numbers).
- /// </summary>
- /// <param name="smallNumber">Small number that should be less than 254
- /// (and not negative) if possible, but it can be any number.</param>
- /// <returns>Number of bytes that we would need to save this number.
- /// Does not actually store anything, use
- /// <see cref="WriteNumberMostlySmallerThan254"/> for that.</returns>
- public static int GetDataLengthOfNumberMostlySmallerThan254(
- int smallNumber)
- {
- if (smallNumber < 0 ||
- smallNumber >= 254)
- {
- // Does fit into ushort?
- if (smallNumber >= ushort.MinValue &&
- smallNumber <= ushort.MaxValue)
- {
- return 3;
- }
- else
- {
- // We have to use a full int
- return 5;
- }
- }
- else
- {
- // This is what should happen most of the time, we got a small number!
- return 1;
- }
- }
- #endregion
-
- #region TryToGetNumberMostlySmallerThan254 (Static)
- /// <summary>
- /// Helper function for network stream reading, we check if
- /// current byte array (we have no guarantee whole message has arrived,
- /// we have to check how much we got and if this is enough)
- /// contains full number in WriteNumberMostlySmallerThan255 style.
- /// Note: Will not restore the stream position because the caller usually
- /// does it anyway when we have to wait for more data.
- /// </summary>
- /// <returns>True if we successfully could read the number, false if
- /// there was not enough data and we have to wait for more data</returns>
- /// <param name="number">Number</param>
- /// <param name="reader">Reader</param>
- public static bool TryToGetNumberMostlySmallerThan254(
- BinaryReader reader, out int number)
- {
- if (reader.BaseStream.Length - reader.BaseStream.Position >= 1)
- {
- byte numberAsByte = reader.ReadByte();
- if (numberAsByte < 254)
- {
- // Just a byte, return it
- number = numberAsByte;
- return true;
- }
- else if (numberAsByte == 254)
- {
- // Do we have enough data for the next 2 bytes?
- if (reader.BaseStream.Length - reader.BaseStream.Position >= 2)
- {
- // A short was used
- number = reader.ReadUInt16();
- return true;
- }
- }
- else // This is an int value
- {
- // Do we have enough data for the next 4 bytes?
- if (reader.BaseStream.Length - reader.BaseStream.Position >= 4)
- {
- number = reader.ReadInt32();
- return true;
- }
- }
- }
-
- // Insufficient data, we need more data, cannot return number yet!
- number = 0;
- return false;
- }
- #endregion
-
- #region LoadWithLength (Static)
- /// <summary>
- /// Helper method to load the length of data plus the data itself. Very
- /// useful for byte arrays that obviously need the size saved out too.
- /// </summary>
- /// <param name="reader">Reader to read the byte data from</param>
- /// <returns>
- /// Returns the byte array with the length specified in the stream.
- /// </returns>
- public static byte[] LoadWithLength(BinaryReader reader)
- {
- // Grab the length first (as an int)
- int dataLength = reader.ReadInt32();
- // Create a byte array and load all the byte data into it.
- byte[] data = new byte[dataLength];
- reader.Read(data, 0, dataLength);
-
- return data;
- }
-
- /// <summary>
- /// Generic helper method to load an ISaveLoadBinary object back in. Use
- /// SaveWithLength to save such an object out into a binary stream. This
- /// method is most useful if called from the Factory.Load method, which
- /// will also create the type for you and load all data in for you.
- /// This method will not crash, but report a warning in the log instead,
- /// when the ISaveLoadBinary loading fails (e.g. not enough data).
- /// </summary>
- /// <param name="reader">Reader to read the object data (T) from</param>
- /// <param name="objectToLoad">
- /// ISaveLoadBinary object for the data to be loaded into.
- /// </param>
- public static void LoadWithLength<T>(BinaryReader reader, T objectToLoad)
- where T : ISaveLoadBinary
- {
- // Grab the length first (as an int)
- int dataLength = reader.ReadInt32();
- // Create a byte array and load all the byte data into it.
- byte[] data = new byte[dataLength];
- reader.Read(data, 0, dataLength);
-
- // Create an extra memory stream just for this data (loading might fail)
- try
- {
- using (MemoryStream tempStream = new MemoryStream(data))
- {
- BinaryReader tempReader = new BinaryReader(tempStream);
- // Load the data from this temporary stream
- objectToLoad.Load(tempReader);
- } // using
- } // try
- catch (Exception ex)
- {
- Log.Warning(
- "Failed to load object " + objectToLoad + " of type (" + typeof(T) +
- ") from the byte data (dataLength=" + dataLength + ", data=" +
- data.Write() + "): " + ex);
- } // catch
- }
- #endregion
-
- #region SaveWithLength (Static)
- /// <summary>
- /// Helper method to not just save an array out, but also keep track of
- /// how many bytes were saved and store those first in case anything goes
- /// wrong when loading the data later (e.g. the format has changed and
- /// more or less data is required). Use the LoadWithLength method to load
- /// the data again.
- /// </summary>
- /// <param name="writer">Writer to save the length and the data itself
- /// into.</param>
- /// <param name="dataToSave">Data to save as an byte array.</param>
- public static void SaveWithLength(BinaryWriter writer,
- byte[] dataToSave)
- {
- // Save the length first (as an int)
- writer.Write(dataToSave.Length);
- // And finally save the byte data itself
- writer.Write(dataToSave);
- }
-
- /// <summary>
- /// Helper method to not just save an array out, but also keep track of
- /// how many bytes were saved and store those first in case anything goes
- /// wrong when loading the data later (e.g. the format has changed and
- /// more or less data is required). Use the LoadWithLength method to load
- /// the data again.
- /// </summary>
- /// <param name="writer">Writer to save the length and the data itself
- /// into.</param>
- /// <param name="dataToSave">Data to save as a MemoryStream</param>
- public static void SaveWithLength(BinaryWriter writer,
- MemoryStream dataToSave)
- {
- SaveWithLength(writer, dataToSave.ToArray());
- }
-
- /// <summary>
- /// Helper method to not just save an object out, but also keep track of
- /// how many bytes were saved and store those first in case anything goes
- /// wrong when loading the data later (e.g. the format has changed and
- /// more or less data is required). Use the LoadWithLength method to load
- /// the data again.
- /// </summary>
- /// <param name="writer">Writer to save the length and the data itself
- /// into.</param>
- /// <param name="dataToSave">Data we want to save via the ISaveLoadBinary
- /// interface, which does not know its own length.</param>
- public static void SaveWithLength(BinaryWriter writer,
- ISaveLoadBinary dataToSave)
- {
- // Create an extra memory stream just for this data
- using (MemoryStream tempStream = new MemoryStream())
- {
- BinaryWriter tempWriter = new BinaryWriter(tempStream);
-
- // Save the data into this temporary stream
- dataToSave.Save(tempWriter);
-
- // Now grab the data
- byte[] data = tempStream.ToArray();
-
- // And save it out together with the data length into dataWriter.
- writer.Write(data.Length);
- writer.Write(data, 0, data.Length);
- } // using
- }
- #endregion
-
- #region GetSavedData (Static)
- /// <summary>
- /// Gets all saved pure data of the given object without any information
- /// about the data length at the beginning.
- /// </summary>
- /// <param name="dataToSave">
- /// Data we want to save via the ISaveLoadBinary interface, which does not
- /// know its own length.
- /// </param>
- public static byte[] GetSavedData(ISaveLoadBinary dataToSave)
- {
- // Create an extra memory stream just for this data
- using (MemoryStream tempStream = new MemoryStream())
- {
- BinaryWriter tempWriter = new BinaryWriter(tempStream);
-
- // Save the data into this temporary stream
- dataToSave.Save(tempWriter);
-
- // Now grab the data
- return tempStream.ToArray();
- } // using
- }
- #endregion
-
- #region GetSavedDataWithLength (Static)
- /// <summary>
- /// Gets all saved data of the given object with the number of the saved
- /// bytes at the beginning.
- /// </summary>
- /// <param name="dataToSave">
- /// Data we want to save via the ISaveLoadBinary interface, which does not
- /// know its own length.
- /// </param>
- public static byte[] GetSavedDataWithLength(ISaveLoadBinary dataToSave)
- {
- // Create an extra memory stream just for this data
- using (MemoryStream tempStream = new MemoryStream())
- {
- BinaryWriter tempWriter = new BinaryWriter(tempStream);
-
- // Save the data into this temporary stream
- SaveWithLength(tempWriter, dataToSave);
-
- // Now grab the data
- return tempStream.ToArray();
- } // using
- }
- #endregion
-
- #region Compare (Static)
- /// <summary>
- /// Helper method to compare two memory streams. Will return true if
- /// both streams are null or empty. If the streams have data, they must
- /// match exactly byte for byte to return true here. In all other cases
- /// false is returned.
- /// </summary>
- /// <param name="memoryStream1">First memory stream to check</param>
- /// <param name="memoryStream2">Second memory stream to check</param>
- /// <returns>True if both memory streams match (either both empty or null
- /// or both have exactly the same data), otherwise false.</returns>
- public static bool Compare(MemoryStream memoryStream1,
- MemoryStream memoryStream2)
- {
- // If both streams are empty, then return true, they are equal.
- if ((memoryStream1 == null ||
- memoryStream1.Length == 0) &&
- (memoryStream2 == null ||
- memoryStream2.Length == 0))
- {
- return true;
- }
-
- // Otherwise return false if only one of the stream is empty or if
- // the lengths are different.
- if (memoryStream1 == null ||
- memoryStream2 == null ||
- memoryStream1.Length != memoryStream2.Length)
- {
- return false;
- }
-
- // And finally check if both streams have the same data
- return ArrayHelper.Compare(memoryStream1.ToArray(),
- memoryStream2.ToArray());
- }
- #endregion
-
- /// <summary>
- /// Test class for the StreamHelper
- /// </summary>
- internal class StreamHelperTests
- {
- #region TestWritingAndReadingNumbers (Static)
- /// <summary>
- /// Test writing and reading numbers
- /// </summary>
- [Test]
- public static void TestWritingAndReadingNumbers()
- {
- using (MemoryStream testStream = new MemoryStream())
- {
- BinaryWriter writer = new BinaryWriter(testStream);
- // Check if we can really put any number in there!
- WriteNumberMostlySmallerThan254(writer, 0);
- WriteNumberMostlySmallerThan254(writer, -1);
- WriteNumberMostlySmallerThan254(writer, 253);
- WriteNumberMostlySmallerThan254(writer, 255);
- WriteNumberMostlySmallerThan254(writer, 234210);
- WriteNumberMostlySmallerThan16Bit(writer, -1340);
- WriteNumberMostlySmallerThan16Bit(writer, +32140);
- WriteNumberMostlySmallerThan16Bit(writer, +32940);
- WriteNumberMostlySmallerThan16Bit(writer, -32940);
- WriteNumberMostlySmallerThan32Bit(writer, +33422940);
- WriteNumberMostlySmallerThan32Bit(writer,
- +334229349840);
- WriteNumberMostlySmallerThan32Bit(writer,
- -39028159023482);
-
- testStream.Position = 0;
- BinaryReader reader = new BinaryReader(testStream);
- Assert.Equal(0,
- ReadNumberMostlySmallerThan254(reader));
- Assert.Equal(-1,
- ReadNumberMostlySmallerThan254(reader));
- Assert.Equal(253,
- ReadNumberMostlySmallerThan254(reader));
- Assert.Equal(255,
- ReadNumberMostlySmallerThan254(reader));
- Assert.Equal(234210,
- ReadNumberMostlySmallerThan254(reader));
- Assert.Equal(-1340,
- ReadNumberMostlySmallerThan16Bit(reader));
- Assert.Equal(+32140,
- ReadNumberMostlySmallerThan16Bit(reader));
- Assert.Equal(+32940,
- ReadNumberMostlySmallerThan16Bit(reader));
- Assert.Equal(-32940,
- ReadNumberMostlySmallerThan16Bit(reader));
- Assert.Equal(+33422940,
- ReadNumberMostlySmallerThan32Bit(reader));
- Assert.Equal(+334229349840,
- ReadNumberMostlySmallerThan32Bit(reader));
- Assert.Equal(-39028159023482,
- ReadNumberMostlySmallerThan32Bit(reader));
- }
- }
- #endregion
-
- #region GetDataLengthOfNumberMostlySmallerThan254
- /// <summary>
- /// Test the GetDataLengthOfNumberMostlySmallerThan254 method.
- /// </summary>
- [Test]
- public void GetDataLengthOfNumberMostlySmallerThan254()
- {
- Assert.Equal(1,
- StreamHelper.GetDataLengthOfNumberMostlySmallerThan254(1));
- Assert.Equal(1,
- StreamHelper.GetDataLengthOfNumberMostlySmallerThan254(100));
- Assert.Equal(1,
- StreamHelper.GetDataLengthOfNumberMostlySmallerThan254(253));
- Assert.Equal(3,
- StreamHelper.GetDataLengthOfNumberMostlySmallerThan254(254));
- Assert.Equal(5,
- StreamHelper.GetDataLengthOfNumberMostlySmallerThan254(139592532));
- }
- #endregion
- }
- }
- }