PageRenderTime 28ms CodeModel.GetById 1ms RepoModel.GetById 1ms app.codeStats 0ms

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