/*
 _   _      _   ____
| \ | | ___| |_| __ )  _____  __
|  \| |/ _ \ __|  _ \ / _ \ \/ /
| |\  |  __/ |_| |_) | (_) >  <
|_| \_|\___|\__|____/ \___/_/\_\   v4.1.4 by @aloneguid

https://github.com/aloneguid/netbox
*/



// FILE: src/NetBox/WebUtility.cs

namespace NetBox
{
   using global::System;
   using global::System.Text;

   /// <summary>
   /// This class is ported from .NET 4.6 to support URL encoding/decoding functionality which is missing in .NET Standard
   /// </summary>
   static class WebUtility
   {
      public static string UrlEncode(string value)
      {
         if (value == null)
            return null;

         byte[] bytes = Encoding.UTF8.GetBytes(value);
         return Encoding.UTF8.GetString(UrlEncode(bytes, 0, bytes.Length, false /* alwaysCreateNewReturnValue */));
      }

      public static string UrlDecode(string encodedValue)
      {
         if (encodedValue == null)
            return null;

         return UrlDecodeInternal(encodedValue, Encoding.UTF8);
      }

      private static byte[] UrlEncode(byte[] bytes, int offset, int count, bool alwaysCreateNewReturnValue)
      {
         byte[] encoded = UrlEncode(bytes, offset, count);

         return (alwaysCreateNewReturnValue && (encoded != null) && (encoded == bytes))
             ? (byte[])encoded.Clone()
             : encoded;
      }

      private static byte[] UrlEncode(byte[] bytes, int offset, int count)
      {
         if (!ValidateUrlEncodingParameters(bytes, offset, count))
         {
            return null;
         }

         int cSpaces = 0;
         int cUnsafe = 0;

         // count them first
         for (int i = 0; i < count; i++)
         {
            char ch = (char)bytes[offset + i];

            if (ch == ' ')
               cSpaces++;
            else if (!IsUrlSafeChar(ch))
               cUnsafe++;
         }

         // nothing to expand?
         if (cSpaces == 0 && cUnsafe == 0)
         {
            // DevDiv 912606: respect "offset" and "count"
            if (0 == offset && bytes.Length == count)
            {
               return bytes;
            }
            else
            {
               byte[] subarray = new byte[count];
               Buffer.BlockCopy(bytes, offset, subarray, 0, count);
               return subarray;
            }
         }

         // expand not 'safe' characters into %XX, spaces to +s
         byte[] expandedBytes = new byte[count + cUnsafe * 2];
         int pos = 0;

         for (int i = 0; i < count; i++)
         {
            byte b = bytes[offset + i];
            char ch = (char)b;

            if (IsUrlSafeChar(ch))
            {
               expandedBytes[pos++] = b;
            }
            else if (ch == ' ')
            {
               expandedBytes[pos++] = (byte)'+';
            }
            else
            {
               expandedBytes[pos++] = (byte)'%';
               expandedBytes[pos++] = (byte)IntToHex((b >> 4) & 0xf);
               expandedBytes[pos++] = (byte)IntToHex(b & 0x0f);
            }
         }

         return expandedBytes;
      }

      private static bool ValidateUrlEncodingParameters(byte[] bytes, int offset, int count)
      {
         if (bytes == null && count == 0)
            return false;
         if (bytes == null)
         {
            throw new ArgumentNullException("bytes");
         }
         if (offset < 0 || offset > bytes.Length)
         {
            throw new ArgumentOutOfRangeException("offset");
         }
         if (count < 0 || offset + count > bytes.Length)
         {
            throw new ArgumentOutOfRangeException("count");
         }

         return true;
      }

      private static bool IsUrlSafeChar(char ch)
      {
         if (ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' || ch >= '0' && ch <= '9')
            return true;

         switch (ch)
         {
            case '-':
            case '_':
            case '.':
            case '!':
            case '*':
            case '(':
            case ')':
               return true;
         }

         return false;
      }

      private static char IntToHex(int n)
      {
         if (n <= 9)
            return (char)(n + (int)'0');
         else
            return (char)(n - 10 + (int)'A');
      }

      private static string UrlDecodeInternal(string value, Encoding encoding)
      {
         if (value == null)
         {
            return null;
         }

         int count = value.Length;
         UrlDecoder helper = new UrlDecoder(count, encoding);

         // go through the string's chars collapsing %XX and
         // appending each char as char, with exception of %XX constructs
         // that are appended as bytes

         for (int pos = 0; pos < count; pos++)
         {
            char ch = value[pos];

            if (ch == '+')
            {
               ch = ' ';
            }
            else if (ch == '%' && pos < count - 2)
            {
               int h1 = HexToInt(value[pos + 1]);
               int h2 = HexToInt(value[pos + 2]);

               if (h1 >= 0 && h2 >= 0)
               {     // valid 2 hex chars
                  byte b = (byte)((h1 << 4) | h2);
                  pos += 2;

                  // don't add as char
                  helper.AddByte(b);
                  continue;
               }
            }

            if ((ch & 0xFF80) == 0)
               helper.AddByte((byte)ch); // 7 bit have to go as bytes because of Unicode
            else
               helper.AddChar(ch);
         }

         return helper.GetString();
      }

      private class UrlDecoder
      {
         private int _bufferSize;

         // Accumulate characters in a special array
         private int _numChars;
         private char[] _charBuffer;

         // Accumulate bytes for decoding into characters in a special array
         private int _numBytes;
         private byte[] _byteBuffer;

         // Encoding to convert chars to bytes
         private Encoding _encoding;

         private void FlushBytes()
         {
            if (_numBytes > 0)
            {
               _numChars += _encoding.GetChars(_byteBuffer, 0, _numBytes, _charBuffer, _numChars);
               _numBytes = 0;
            }
         }

         internal UrlDecoder(int bufferSize, Encoding encoding)
         {
            _bufferSize = bufferSize;
            _encoding = encoding;

            _charBuffer = new char[bufferSize];
            // byte buffer created on demand
         }

         internal void AddChar(char ch)
         {
            if (_numBytes > 0)
               FlushBytes();

            _charBuffer[_numChars++] = ch;
         }

         internal void AddByte(byte b)
         {
            if (_byteBuffer == null)
               _byteBuffer = new byte[_bufferSize];

            _byteBuffer[_numBytes++] = b;
         }

         internal String GetString()
         {
            if (_numBytes > 0)
               FlushBytes();

            if (_numChars > 0)
               return new String(_charBuffer, 0, _numChars);
            else
               return String.Empty;
         }
      }

      private static int HexToInt(char h)
      {
         return (h >= '0' && h <= '9') ? h - '0' :
         (h >= 'a' && h <= 'f') ? h - 'a' + 10 :
         (h >= 'A' && h <= 'F') ? h - 'A' + 10 :
         -1;
      }
   }
}


// FILE: src/NetBox/ByteFormat.cs

namespace NetBox
{
   using global::System;

   static class ByteFormat
   {
      //http://en.wikipedia.org/wiki/Kibibyte

      private const long Kb = 1000;         //kilobyte
      private const long KiB = 1024;        //kikibyte
      private const long Mb = Kb * 1000;      //megabyte
      private const long MiB = KiB * 1024;    //memibyte
      private const long Gb = Mb * 1000;      //gigabyte
      private const long GiB = MiB * 1024;    //gigibyte
      private const long Tb = Gb * 1000;      //terabyte
      private const long TiB = GiB * 1024;    //tebibyte
      private const long Pb = Tb * 1024;      //petabyte
      private const long PiB = TiB * 1024;    //pepibyte

      public enum Standard
      {
         /// <summary>
         ///  International System of Units
         /// </summary>
         Si,

         /// <summary>
         /// International Electrotechnical Commission
         /// </summary>
         Iec
      }

      /// <summary>
      /// Returns the best formatted string representation of a byte value
      /// </summary>
      /// <param name="bytes">number of bytes</param>
      /// <param name="st"></param>
      /// <returns>formatted string</returns>
      private static string ToString(long bytes, Standard st = Standard.Iec)
      {
         return ToString(bytes, st, null);
      }

      /// <summary>
      /// Returns the best formatted string representation of a byte value
      /// </summary>
      /// <param name="bytes">number of bytes</param>
      /// <param name="st"></param>
      /// <param name="customFormat">Defines a custom numerical format for the conversion.
      /// If this parameters is null or empty the default format will be used 0.00</param>
      /// <returns>formatted string</returns>
      public static string ToString(long bytes, Standard st, string customFormat)
      {
         if (bytes == 0) return "0";

         if (string.IsNullOrEmpty(customFormat))
            customFormat = "0.00";

         string result;
         bool isNegative = bytes < 0;
         bytes = Math.Abs(bytes);

         if (st == Standard.Si)
         {
            if (bytes < Mb) result = BytesToKb(bytes, customFormat);

            else if (bytes < Gb) result = BytesToMb(bytes, customFormat);

            else if (bytes < Tb) result = BytesToGb(bytes, customFormat);

            else if (bytes < Pb) result = BytesToTb(bytes, customFormat);

            else result = BytesToPb(bytes, customFormat);
         }
         else
         {
            if (bytes < MiB) result = BytesToKib(bytes, customFormat);

            else if (bytes < GiB) result = BytesToMib(bytes, customFormat);

            else if (bytes < TiB) result = BytesToGib(bytes, customFormat);

            else if (bytes < PiB) result = BytesToTib(bytes, customFormat);

            else result = BytesToPib(bytes, customFormat);
         }

         return isNegative ? ("-" + result) : (result);
      }

      private static string BytesToPb(long bytes, string customFormat)
      {
         double tb = bytes / ((double)Pb);
         return tb.ToString(customFormat) + " PB";
      }
      private static string BytesToPib(long bytes, string customFormat)
      {
         double tb = bytes / ((double)PiB);
         return tb.ToString(customFormat) + " PiB";
      }

      private static string BytesToTb(long bytes, string customFormat)
      {
         double tb = bytes / ((double)Tb);
         return tb.ToString(customFormat) + " TB";
      }
      private static string BytesToTib(long bytes, string customFormat)
      {
         double tb = bytes / ((double)TiB);
         return tb.ToString(customFormat) + " TiB";
      }

      private static string BytesToGb(long bytes, string customFormat)
      {
         double gb = bytes / ((double)Gb);
         return gb.ToString(customFormat) + " GB";
      }
      private static string BytesToGib(long bytes, string customFormat)
      {
         double gb = bytes / ((double)GiB);
         return gb.ToString(customFormat) + " GiB";
      }

      private static string BytesToMb(long bytes, string customFormat)
      {
         double mb = bytes / ((double)Mb);
         return mb.ToString(customFormat) + " MB";
      }
      private static string BytesToMib(long bytes, string customFormat)
      {
         double mb = bytes / ((double)MiB);
         return mb.ToString(customFormat) + " MiB";
      }

      private static string BytesToKb(long bytes, string customFormat)
      {
         double kb = bytes / ((double)Kb);
         return kb.ToString(customFormat) + " KB";
      }
      private static string BytesToKib(long bytes, string customFormat)
      {
         double kb = bytes / ((double)KiB);
         return kb.ToString(customFormat) + " KiB";
      }
   }
}


// FILE: src/NetBox/Ascii85.cs

namespace NetBox
{
   using global::System;
   using global::System.IO;
   using global::System.Text;

   /// <summary>
   /// C# implementation of ASCII85 encoding. 
   /// Based on C code from http://www.stillhq.com/cgi-bin/cvsweb/ascii85/
   /// </summary>
   /// <remarks>
   /// Jeff Atwood
   /// http://www.codinghorror.com/blog/archives/000410.html
   /// </remarks>
   class Ascii85
   {
      /// <summary>
      /// Prefix mark that identifies an encoded ASCII85 string
      /// </summary>
      public string PrefixMark = "<~";
      /// <summary>
      /// Suffix mark that identifies an encoded ASCII85 string
      /// </summary>
      public string SuffixMark = "~>";
      /// <summary>
      /// Maximum line length for encoded ASCII85 string; 
      /// set to zero for one unbroken line.
      /// </summary>
      public int LineLength = 75;

      private const int _asciiOffset = 33;
      private byte[] _encodedBlock = new byte[5];
      private byte[] _decodedBlock = new byte[4];
      private uint _tuple = 0;
      private int _linePos = 0;

      private uint[] pow85 = { 85 * 85 * 85 * 85, 85 * 85 * 85, 85 * 85, 85, 1 };

      private static Ascii85 instance = new Ascii85();

      public static Ascii85 Instance => instance;

      /// <summary>
      /// Decodes an ASCII85 encoded string into the original binary data
      /// </summary>
      /// <param name="s">ASCII85 encoded string</param>
      /// <param name="enforceMarks">enforce marks</param>
      /// <returns>byte array of decoded binary data</returns>
      public byte[] Decode(string s, bool enforceMarks)
      {
         if (enforceMarks)
         {
            if (!s.StartsWith(PrefixMark) | !s.EndsWith(SuffixMark))
            {
               throw new Exception("ASCII85 encoded data should begin with '" + PrefixMark +
                  "' and end with '" + SuffixMark + "'");
            }
         }

         // strip prefix and suffix if present
         if (s.StartsWith(PrefixMark))
         {
            s = s.Substring(PrefixMark.Length);
         }
         if (s.EndsWith(SuffixMark))
         {
            s = s.Substring(0, s.Length - SuffixMark.Length);
         }

         MemoryStream ms = new MemoryStream();
         int count = 0;
         bool processChar = false;

         foreach (char c in s)
         {
            switch (c)
            {
               case 'z':
                  if (count != 0)
                  {
                     throw new Exception("The character 'z' is invalid inside an ASCII85 block.");
                  }
                  _decodedBlock[0] = 0;
                  _decodedBlock[1] = 0;
                  _decodedBlock[2] = 0;
                  _decodedBlock[3] = 0;
                  ms.Write(_decodedBlock, 0, _decodedBlock.Length);
                  processChar = false;
                  break;
               case '\n':
               case '\r':
               case '\t':
               case '\0':
               case '\f':
               case '\b':
                  processChar = false;
                  break;
               default:
                  if (c < '!' || c > 'u')
                  {
                     throw new Exception("Bad character '" + c + "' found. ASCII85 only allows characters '!' to 'u'.");
                  }
                  processChar = true;
                  break;
            }

            if (processChar)
            {
               _tuple += ((uint)(c - _asciiOffset) * pow85[count]);
               count++;
               if (count == _encodedBlock.Length)
               {
                  DecodeBlock();
                  ms.Write(_decodedBlock, 0, _decodedBlock.Length);
                  _tuple = 0;
                  count = 0;
               }
            }
         }

         // if we have some bytes left over at the end..
         if (count != 0)
         {
            if (count == 1)
            {
               throw new Exception("The last block of ASCII85 data cannot be a single byte.");
            }
            count--;
            _tuple += pow85[count];
            DecodeBlock(count);
            for (int i = 0; i < count; i++)
            {
               ms.WriteByte(_decodedBlock[i]);
            }
         }

         return ms.ToArray();
      }

      /// <summary>
      /// Encodes binary data into a plaintext ASCII85 format string
      /// </summary>
      /// <param name="ba">binary data to encode</param>
      /// <param name="enforceMarks">enforce marks</param>
      /// <returns>ASCII85 encoded string</returns>
      public string Encode(byte[] ba, bool enforceMarks)
      {
         StringBuilder sb = new StringBuilder(ba.Length * (_encodedBlock.Length / _decodedBlock.Length));
         _linePos = 0;

         if (enforceMarks)
         {
            AppendString(sb, PrefixMark);
         }

         int count = 0;
         _tuple = 0;
         foreach (byte b in ba)
         {
            if (count >= _decodedBlock.Length - 1)
            {
               _tuple |= b;
               if (_tuple == 0)
               {
                  AppendChar(sb, 'z');
               }
               else
               {
                  EncodeBlock(sb);
               }
               _tuple = 0;
               count = 0;
            }
            else
            {
               _tuple |= (uint)(b << (24 - (count * 8)));
               count++;
            }
         }

         // if we have some bytes left over at the end..
         if (count > 0)
         {
            EncodeBlock(count + 1, sb);
         }

         if (enforceMarks)
         {
            AppendString(sb, SuffixMark);
         }
         return sb.ToString();
      }

      private void EncodeBlock(StringBuilder sb)
      {
         EncodeBlock(_encodedBlock.Length, sb);
      }

      private void EncodeBlock(int count, StringBuilder sb)
      {
         for (int i = _encodedBlock.Length - 1; i >= 0; i--)
         {
            _encodedBlock[i] = (byte)((_tuple % 85) + _asciiOffset);
            _tuple /= 85;
         }

         for (int i = 0; i < count; i++)
         {
            char c = (char)_encodedBlock[i];
            AppendChar(sb, c);
         }

      }

      private void DecodeBlock()
      {
         DecodeBlock(_decodedBlock.Length);
      }

      private void DecodeBlock(int bytes)
      {
         for (int i = 0; i < bytes; i++)
         {
            _decodedBlock[i] = (byte)(_tuple >> 24 - (i * 8));
         }
      }

      private void AppendString(StringBuilder sb, string s)
      {
         if (LineLength > 0 && (_linePos + s.Length > LineLength))
         {
            _linePos = 0;
            sb.Append('\n');
         }
         else
         {
            _linePos += s.Length;
         }
         sb.Append(s);
      }

      private void AppendChar(StringBuilder sb, char c)
      {
         sb.Append(c);
         _linePos++;
         if (LineLength > 0 && (_linePos >= LineLength))
         {
            _linePos = 0;
            sb.Append('\n');
         }
      }

   }
}


// FILE: src/NetBox/TempFile.cs

namespace NetBox
{
   using global::System;
   using global::System.IO;

   /// <summary>
   /// Represents a temporary file that is deleted on dispose. The files are created in user's temp directory.
   /// </summary>
   class TempFile : IDisposable
   {
      /// <summary>
      /// 
      /// </summary>
      /// <param name="ext">Optional extension, defaults to .tmp</param>
      public TempFile(string ext = null)
      {
         if (ext == null) ext = ".tmp";

         if (!ext.StartsWith(".")) ext = "." + ext;

         string name = Guid.NewGuid().ToString() + ext;

         FullPath = Path.Combine(Path.GetTempPath(), name);
      }

      /// <summary>
      /// Full path to the temp file. It won't exist on disk.
      /// </summary>
      public string FullPath { get; }

      /// <summary>
      /// Implicit conversion to string (full path).
      /// </summary>
      /// <param name="tf"></param>
      public static implicit operator string(TempFile tf) => tf.FullPath;

      public override string ToString() => FullPath;

      public void Dispose()
      {
         if(File.Exists(FullPath))
         {
            File.Delete(FullPath);
         }
      }
   }
}


// FILE: src/NetBox/Performance/TimeMeasure.cs

namespace NetBox.Performance
{
   using global::System;
   using global::System.Diagnostics;

   /// <summary>
   /// Measures a time slice as precisely as possible
   /// </summary>
   class TimeMeasure : IDisposable
   {
      private readonly Stopwatch _sw = new Stopwatch();

      /// <summary>
      /// Creates the measure object
      /// </summary>
      public TimeMeasure()
      {
         _sw.Start();
      }

      /// <summary>
      /// Returns number of elapsed ticks since the start of measure.
      /// The measuring process will continue running.
      /// </summary>
      public long ElapsedTicks => _sw.ElapsedTicks;

      /// <summary>
      /// Returns number of elapsed milliseconds since the start of measure.
      /// The measuring process will continue running.
      /// </summary>
      public long ElapsedMilliseconds => _sw.ElapsedMilliseconds;


      /// <summary>
      /// Gets time elapsed from the time this measure was created
      /// </summary>
      public TimeSpan Elapsed => _sw.Elapsed;

      /// <summary>
      /// Stops measure object if still running
      /// </summary>
      public void Dispose()
      {
         if (_sw.IsRunning)
         {
            _sw.Stop();
         }
      }
   }
}


// FILE: src/NetBox/System/ByteArrayExtensions.cs

namespace System
{
   using Crypto = System.Security.Cryptography;

   /// <summary>
   /// Byte array extensions methods
   /// </summary>
   static class ByteArrayExtensions
   {
      private static readonly char[] LowerCaseHexAlphabet = "0123456789abcdef".ToCharArray();
      private static readonly char[] UpperCaseHexAlphabet = "0123456789ABCDEF".ToCharArray();
      private static readonly Crypto.MD5 _md5 = Crypto.MD5.Create();
      private static readonly Crypto.SHA256 _sha256 = Crypto.SHA256.Create();


      /// <summary>
      /// Converts byte array to hexadecimal string
      /// </summary>
      public static string ToHexString(this byte[] bytes)
      {
         return ToHexString(bytes, true);
      }

      private static string ToHexString(this byte[] bytes, bool lowerCase)
      {
         if (bytes == null) return null;

         char[] alphabet = lowerCase ? LowerCaseHexAlphabet : UpperCaseHexAlphabet;

         int len = bytes.Length;
         char[] result = new char[len * 2];

         int i = 0;
         int j = 0;

         while (i < len)
         {
            byte b = bytes[i++];
            result[j++] = alphabet[b >> 4];
            result[j++] = alphabet[b & 0xF];
         }

         return new string(result);
      }

      public static byte[] MD5(this byte[] bytes)
      {
         if (bytes == null) return null;

         return _md5.ComputeHash(bytes);
      }

      public static byte[] SHA256(this byte[] bytes)
      {
         if (bytes == null) return null;

         return _sha256.ComputeHash(bytes);
      }

      public static byte[] HMACSHA256(this byte[] data, byte[] key)
      {
         if (data == null) return null;

         var alg = Crypto.KeyedHashAlgorithm.Create("HmacSHA256");
         alg.Key = key;
         return alg.ComputeHash(data);
      }
   }
}


// FILE: src/NetBox/System/EnumerableExtensions.cs

namespace System
{
   using global::System.Collections.Generic;
   using global::System.Linq;

   /// <summary>
   /// <see cref="System.IEquatable{T}"/> extension methods
   /// </summary>
   static class EnumerableExtensions
   {
      /// <summary>
      /// Split sequence in batches of specified size
      /// </summary>
      /// <typeparam name="T">Element type</typeparam>
      /// <param name="source">Enumeration source</param>
      /// <param name="chunkSize">Size of the batch chunk</param>
      /// <returns></returns>
      public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, int chunkSize)
      {
         if (source == null) throw new ArgumentNullException(nameof(source));

         while (source.Any())
         {
            yield return source.Take(chunkSize);
            source = source.Skip(chunkSize);
         }
      }

      /// <summary>
      /// Performs a specific action on each element of the sequence
      /// </summary>
      public static IEnumerable<T> ForEach<T>(this IEnumerable<T> source, Action<T> action)
      {
         if (source == null) throw new ArgumentNullException(nameof(source));
         if (action == null) throw new ArgumentNullException(nameof(action));

         foreach (T element in source)
         {
            action(element);

            yield return element;
         }
      }
   }
}


// FILE: src/NetBox/System/TaskExtensions.cs

namespace System
{
   using global::System.Threading.Tasks;
   using System.Diagnostics.CodeAnalysis;

   /// <summary>
   /// Task utility methods
   /// </summary>
   static class TaskExtensions
   {
      /// <summary>
      /// Fire-and-forget without compiler warnings
      /// </summary>
      /// <param name="task"></param>
      [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "task")]
      public static void Forget(this Task task)
      {
      }
   }
}


// FILE: src/NetBox/System/GuidExtensions.cs


namespace System
{
   using NetBox;

   public static class GuidExtensions
   {
      public static string ToShortest(this Guid g)
      {
         return Ascii85.Instance.Encode(g.ToByteArray(), true);
      }
   }
}


// FILE: src/NetBox/System/StringExtensions.cs

namespace System
{
   using System.Net.Http;
   using global::System.Threading.Tasks;
   using NetBox;
   using global::System.Collections.Generic;
   using global::System.IO;
   using global::System.Linq;
   using global::System.Text;
   using global::System.Text.RegularExpressions;

   /// <summary>
   /// String extensions.
   /// </summary>
   static class StringExtensions
   {
      private const string HtmlStripPattern = @"<(.|\n)*?>";

      static readonly char[] Invalid = Path.GetInvalidFileNameChars();

      /// <summary>
      /// Converts hex string to byte array
      /// </summary>
      /// <param name="hex"></param>
      /// <returns></returns>
      public static byte[] FromHexToBytes(this string hex)
      {
         if (hex == null) return null;

         byte[] raw = new byte[hex.Length / 2];
         for (int i = 0; i < raw.Length; i++)
         {
            try
            {
               raw[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
            }
            catch (FormatException)
            {
               return null;
            }
         }
         return raw;
      }

      #region [ HTML Helpers ]

      /// <summary>
      /// Strips HTML string from any tags leaving text only.
      /// </summary>
      /// <param name="s"></param>
      /// <returns></returns>
      public static string StripHtml(this string s)
      {
         if (s == null) return null;

         //This pattern Matches everything found inside html tags;
         //(.|\n) - > Look for any character or a new line
         // *?  -> 0 or more occurences, and make a non-greedy search meaning
         //That the match will stop at the first available '>' it sees, and not at the last one
         //(if it stopped at the last one we could have overlooked
         //nested HTML tags inside a bigger HTML tag..)

         return Regex.Replace(s, HtmlStripPattern, string.Empty);
      }

      #endregion

      #region [ Encoding ]

      /// <summary>
      /// Encodes a string to BASE64 format
      /// </summary>
      public static string Base64Encode(this string s)
      {
         if (s == null) return null;

         return Convert.ToBase64String(Encoding.UTF8.GetBytes(s));
      }

      /// <summary>
      /// Decodes a BASE64 encoded string
      /// </summary>
      public static string Base64Decode(this string s)
      {
         if (s == null) return null;

         byte[] data = Convert.FromBase64String(s);

         return Encoding.UTF8.GetString(data, 0, data.Length);
      }


      /// <summary>
      /// Decodes a BASE64 encoded string to byte array
      /// </summary>
      /// <param name="s">String to decode</param>
      /// <returns>Byte array</returns>
      public static byte[] Base64DecodeAsBytes(this string s)
      {
         if (s == null) return null;

         return Convert.FromBase64String(s);
      }

      /// <summary>
      /// Converts shortest guid representation back to Guid. See <see cref="GuidExtensions.ToShortest(Guid)"/>
      /// on how to convert Guid to string.
      /// </summary>
      public static Guid FromShortestGuid(this string s)
      {
         byte[] guidBytes = Ascii85.Instance.Decode(s, false);
         return new Guid(guidBytes);
      }

      /// <summary>
      /// URL-encodes input string
      /// </summary>
      /// <param name="value">String to encode</param>
      /// <returns>Encoded string</returns>
      public static string UrlEncode(this string value)
      {
         return WebUtility.UrlEncode(value);
      }

      /// <summary>
      /// URL-decodes input string
      /// </summary>
      /// <param name="value">String to decode</param>
      /// <returns>Decoded string</returns>
      public static string UrlDecode(this string value)
      {
         return WebUtility.UrlDecode(value);
      }

      public static byte[] UTF8Bytes(this string s)
      {
         if (s == null) return null;

         return Encoding.UTF8.GetBytes(s);
      }

      #endregion

      #region [ Hashing ]

      public static string MD5(this string s)
      {
         if (s == null) return null;

         return Encoding.UTF8.GetBytes(s).MD5().ToHexString();
      }

      public static string SHA256(this string s)
      {
         if (s == null) return null;

         return Encoding.UTF8.GetBytes(s).SHA256().ToHexString();
      }

      public static byte[] HMACSHA256(this string s, byte[] key)
      {
         if (s == null) return null;

         return Encoding.UTF8.GetBytes(s).HMACSHA256(key);
      }


      #endregion

      #region [ Stream Conversion ]

      /// <summary>
      /// Converts to MemoryStream with a specific encoding
      /// </summary>
      public static MemoryStream ToMemoryStream(this string s, Encoding encoding)
      {
         if (s == null) return null;
         if (encoding == null) encoding = Encoding.UTF8;

         return new MemoryStream(encoding.GetBytes(s));
      }

      /// <summary>
      /// Converts to MemoryStream in UTF-8 encoding
      /// </summary>
      public static MemoryStream ToMemoryStream(this string s)
      {
         // ReSharper disable once IntroduceOptionalParameters.Global
         return ToMemoryStream(s, null);
      }

      #endregion

      #region [ String Manipulation ]

      private static bool FindTagged(ref string s, ref string startToken, ref string endToken, bool includeOuterTokens, out int startIdx, out int length)
      {
         int idx0 = s.IndexOf(startToken, StringComparison.Ordinal);

         if (idx0 != -1)
         {
            int idx1 = s.IndexOf(endToken, idx0 + startToken.Length, StringComparison.Ordinal);

            if (idx1 != -1)
            {
               startIdx = includeOuterTokens ? idx0 : idx0 + startToken.Length;
               length = includeOuterTokens
                            ? (idx1 - idx0 + endToken.Length)
                            : (idx1 - idx0 - startToken.Length);

               return true;
            }
         }

         startIdx = length = -1;
         return false;
      }

      /// <summary>
      /// Looks for <paramref name="startTag"/> and <paramref name="endTag"/> followed in sequence and when found returns the text between them.
      /// </summary>
      /// <param name="s">Input string</param>
      /// <param name="startTag">Start tag</param>
      /// <param name="endTag">End tag</param>
      /// <param name="includeOuterTags">When set to true, returns the complete phrase including start and end tag value,
      /// otherwise only inner text returned</param>
      /// <returns></returns>
      public static string FindTagged(this string s, string startTag, string endTag, bool includeOuterTags)
      {
         if (startTag == null) throw new ArgumentNullException(nameof(startTag));
         if (endTag == null) throw new ArgumentNullException(nameof(endTag));

         int start, length;
         if (FindTagged(ref s, ref startTag, ref endTag, includeOuterTags, out start, out length))
         {
            return s.Substring(start, length);
         }

         return null;
      }

      /// <summary>
      /// Looks for <paramref name="startTag"/> and <paramref name="endTag"/> followed in sequence, and if found
      /// performs a replacement of text inside them with <paramref name="replacementText"/>
      /// </summary>
      /// <param name="s">Input string</param>
      /// <param name="startTag">Start tag</param>
      /// <param name="endTag">End tag</param>
      /// <param name="replacementText">Replacement text</param>
      /// <param name="replaceOuterTokens">When set to true, not only the text between tags is replaced, but the whole
      /// phrase with <paramref name="startTag"/>, text between tags and <paramref name="endTag"/></param>
      /// <returns></returns>
      public static string ReplaceTagged(this string s, string startTag, string endTag, string replacementText, bool replaceOuterTokens)
      {
         if (startTag == null) throw new ArgumentNullException(nameof(startTag));
         if (endTag == null) throw new ArgumentNullException(nameof(endTag));
         //if (replacementText == null) throw new ArgumentNullException("replacementText");

         int start, length;

         if (FindTagged(ref s, ref startTag, ref endTag, replaceOuterTokens, out start, out length))
         {
            s = s.Remove(start, length);

            if (replacementText != null)
               s = s.Insert(start, replacementText);
         }

         return s;
      }

      /// <summary>
      /// Converts a string with spaces to a camel case version, for example
      /// "The camel string" is converted to "TheCamelString"
      /// </summary>
      public static string SpacedToCamelCase(this string s)
      {
         if (s == null) return null;

         string[] parts = s.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);

         var b = new StringBuilder();
         foreach (string part in parts)
         {
            string uc = part.Capitalize();
            b.Append(uc);
         }
         return b.ToString();
      }

      /// <summary>
      /// Transforms the string so that the first letter is uppercase and the rest of them are lowercase
      /// </summary>
      public static string Capitalize(this string s)
      {
         if (s == null) return null;
         var b = new StringBuilder();

         for (int i = 0; i < s.Length; i++)
         {
            b.Append(i == 0 ? char.ToUpper(s[i]) : char.ToLower(s[i]));
         }

         return b.ToString();
      }

      /// <summary>
      /// Pythonic approach to slicing strings
      /// </summary>
      /// <param name="s">Input string</param>
      /// <param name="start">Is the start index to slice from. It can be either positive or negative.
      /// Negative value indicates that the index is taken from the end of the string.</param>
      /// <param name="end">Is the index to slice to. It can be either positive or negative.
      /// Negative value indicates that the index is taken from the end of the string.</param>
      /// <returns>Sliced string</returns>
      public static string Slice(this string s, int? start, int? end)
      {
         if (s == null) return null;
         if (start == null && end == null) return s;

         int si = start ?? 0;
         int ei = end ?? s.Length;

         if (si < 0)
         {
            si = s.Length + si;
            if (si < 0) si = 0;
         }

         if (si > s.Length)
         {
            si = s.Length - 1;
         }

         if (ei < 0)
         {
            ei = s.Length + ei;
            if (ei < 0) ei = 0;
         }

         if (ei > s.Length)
         {
            ei = s.Length;
         }

         return s.Substring(si, ei - si);
      }

      /// <summary>
      /// Splits the string into key and value using the provided delimiter values. Both key and value are trimmed as well.
      /// </summary>
      /// <param name="s">Input string. When null returns null immediately.</param>
      /// <param name="delimiter">List of delmiters between key and value. This method searches for all the provided
      /// delimiters, and splits by the first left-most one.</param>
      /// <returns>A tuple of two values where the first one is the key and second is the value. If none of the delimiters
      /// are found the second value of the tuple is null and the first value is the input string</returns>
      public static Tuple<string, string> SplitByDelimiter(this string s, params string[] delimiter)
      {
         if (s == null) return null;

         string key, value;

         if (delimiter == null || delimiter.Length == 0)
         {
            key = s.Trim();
            value = null;
         }
         else
         {

            List<int> indexes = delimiter.Where(d => d != null).Select(d => s.IndexOf(d)).Where(d => d != -1).ToList();

            if (indexes.Count == 0)
            {
               key = s.Trim();
               value = null;
            }
            else
            {
               int idx = indexes.OrderBy(i => i).First();
               key = s.Substring(0, idx);
               value = s.Substring(idx + 1);
            }
         }

         return new Tuple<string, string>(key, value);
      }

      /// <summary>
      /// Splits text line by line and removes lines containing specific substring
      /// </summary>
      public static string RemoveLinesContaining(this string input, string substring, StringComparison stringComparison = StringComparison.CurrentCulture)
      {
         if (string.IsNullOrEmpty(input)) return input;

         var result = new StringBuilder();

         using (var sr = new StringReader(input))
         {
            string line;
            while ((line = sr.ReadLine()) != null)
            {
               if (line.IndexOf(substring, stringComparison) != -1) continue;

               result.AppendLine(line);
            }
         }

         return result.ToString();
      }


      #endregion

      #region [ JSON ]

      /// <summary>
      /// Escapes a string for JSON encoding
      /// </summary>
      /// <param name="s"></param>
      /// <returns></returns>
      public static string ToEscapedJsonValueString(this string s)
      {
         if (s == null) return null;

         string result = string.Empty;
         for (int i = 0; i < s.Length; i++)
         {
            char c = s[i];
            string ec;

            switch (c)
            {
               case '\t':
                  ec = "\\t";
                  break;
               case '\n':
                  ec = "\\n";
                  break;
               case '\r':
                  ec = "\\r";
                  break;
               case '\f':
                  ec = "\\f";
                  break;
               case '\b':
                  ec = "\\b";
                  break;
               case '\\':
                  ec = "\\\\";
                  break;
               case '\u0085': // Next Line
                  ec = "\\u0085";
                  break;
               case '\u2028': // Line Separator
                  ec = "\\u2028";
                  break;
               case '\u2029': // Paragraph Separator
                  ec = "\\u2029";
                  break;
               default:
                  ec = new string(c, 1);
                  break;
            }

            result += ec;
         }

         return result;
      }

      #endregion

      #region [ Networking ]

      /// <summary>
      /// Treat this string as URL and download it as stream
      /// </summary>
      /// <param name="uri"></param>
      /// <returns></returns>
      public static async Task<TempFile> DownloadUrlToTempFile(this string uri)
      {
         if (string.IsNullOrEmpty(uri)) return null;

         var tf = new TempFile();

         using (Stream src = await new HttpClient().GetStreamAsync(uri))
         {
            using(Stream tgt = File.Create(tf))
            {
               await src.CopyToAsync(tgt);
            }
         }

         return tf;
      }

      #endregion
   }
}


// FILE: src/NetBox/System/NumericExtensions.cs

namespace System
{
   using NetBox;

   /// <summary>
   /// <see cref="long"/> extension methods
   /// </summary>
   static class LongExtensions
   {
      /// <summary>
      /// Converts number to readable size string in IEC format, i.e. 1024 converts to "1.02 KiB"
      /// </summary>
      public static string ToFileSizeString(this long number)
      {
         return ByteFormat.ToString(number, ByteFormat.Standard.Iec, null);
      }

      /// <summary>
      /// Converts number to readable size string in SI format, i.e. 1024 converts to "1.02 KB"
      /// </summary>
      public static string ToFileSizeUiString(this long number)
      {
         return ByteFormat.ToString(number, ByteFormat.Standard.Si, null);
      }
   }

   static class IntExtensions
   {
      /// <summary>
      /// Converts number to readable size string in IEC format, i.e. 1024 converts to "1.02 KiB"
      /// </summary>
      public static string ToFileSizeString(this int number)
      {
         return ByteFormat.ToString(number, ByteFormat.Standard.Iec, null);
      }

      /// <summary>
      /// Converts number to readable size string in SI format, i.e. 1024 converts to "1.02 KB"
      /// </summary>
      public static string ToFileSizeUiString(this int number)
      {
         return ByteFormat.ToString(number, ByteFormat.Standard.Si, null);
      }

      /// <summary>
      /// Converts number to seconds
      /// </summary>
      /// <param name="number">Number of seconds</param>
      /// <returns>Timespan values</returns>
      public static TimeSpan Seconds(this int number)
      {
         return TimeSpan.FromSeconds(number);
      }

      /// <summary>
      /// Converts number to minutes
      /// </summary>
      /// <param name="number">Number of minutes</param>
      /// <returns>Timespan value</returns>
      public static TimeSpan Minutes(this int number)
      {
         return TimeSpan.FromMinutes(number);
      }

      /// <summary>
      /// Converts number to hours 
      /// </summary>
      /// <param name="number">Number of hours</param>
      /// <returns>Timespan value</returns>
      public static TimeSpan Hours(this int number)
      {
         return TimeSpan.FromHours(number);
      }
   }
}


// FILE: src/NetBox/System/StreamExtensions.cs

namespace System
{
   using global::System.Collections.Generic;
   using global::System.IO;
   using global::System.Text;

   /// <summary>
   /// <see cref="Stream"/> extension
   /// </summary>
   public static class StreamExtensions
   {
      #region [ General ]

      /// <summary>
      /// Attemps to get the size of this stream by reading the Length property, otherwise returns 0.
      /// </summary>
      public static bool TryGetSize(this Stream s, out long size)
      {
         try
         {
            size = s.Length;
            return true;
         }
         catch (NotSupportedException)
         {

         }
         catch (ObjectDisposedException)
         {

         }

         size = 0;
         return false;
      }

      /// <summary>
      /// Attemps to get the size of this stream by reading the Length property, otherwise returns 0.
      /// </summary>
      public static long? TryGetSize(this Stream s)
      {
         long size;
         if (TryGetSize(s, out size))
         {
            return size;
         }

         return null;
      }

      #endregion

      #region [ Seek and Read ]

      /// <summary>
      /// Reads the stream until a specified sequence of bytes is reached.
      /// </summary>
      /// <returns>Bytes before the stop sequence</returns>
      public static byte[] ReadUntil(this Stream s, byte[] stopSequence)
      {
         byte[] buf = new byte[1];
         var result = new List<byte>(50);
         int charsMatched = 0;

         while (s.Read(buf, 0, 1) == 1)
         {
            byte b = buf[0];
            result.Add(b);

            if (b == stopSequence[charsMatched])
            {
               if (++charsMatched == stopSequence.Length)
               {
                  break;
               }
            }
            else
            {
               charsMatched = 0;
            }

         }
         return result.ToArray();
      }

      #endregion

      #region [ Stream Conversion ]

      /// <summary>
      /// Reads all stream in memory and returns as byte array
      /// </summary>
      public static byte[] ToByteArray(this Stream stream)
      {
         if (stream == null) return null;
         using (var ms = new MemoryStream())
         {
            stream.CopyTo(ms);
            return ms.ToArray();
         }
      }

      /// <summary>
      /// Converts the stream to string using specified encoding. This is done by reading the stream into
      /// byte array first, then applying specified encoding on top.
      /// </summary>
      public static string ToString(this Stream stream, Encoding encoding)
      {
         if (stream == null) return null;
         if (encoding == null) throw new ArgumentNullException(nameof(encoding));

         using (StreamReader reader = new StreamReader(stream, encoding))
         {
            return reader.ReadToEnd();
         }
      }

      #endregion

   }
}


// FILE: src/NetBox/System/DateTimeExtensions.cs

namespace System
{
   /// <summary>
   /// <see cref="DateTime"/> extension methods
   /// </summary>
   static class DateTimeExtensions
   {
      /// <summary>
      /// Strips time from the date structure
      /// </summary>
      public static DateTime RoundToDay(this DateTime time)
      {
         return new DateTime(time.Year, time.Month, time.Day);
      }

      /// <summary>
      /// Changes to the end of day time, i.e. hours, minutes and seconds are changed to 23:59:59
      /// </summary>
      /// <param name="time"></param>
      /// <returns></returns>
      public static DateTime EndOfDay(this DateTime time)
      {
         return new DateTime(time.Year, time.Month, time.Day, 23, 59, 59);
      }

      /// <summary>
      /// Rounds to the closest minute
      /// </summary>
      /// <param name="time">Input date</param>
      /// <param name="round">Closest minute i.e. 15, 30, 45 etc.</param>
      /// <param name="roundLeft">Whether to use minimum or maximum value. For example
      /// when time is 13:14 and rounding is to every 15 minutes, when this parameter is true
      /// the result it 13:00, otherwise 13:15</param>
      /// <returns></returns>
      public static DateTime RoundToMinute(this DateTime time, int round, bool roundLeft)
      {
         int minute = time.Minute;
         int leftover = minute % round;
         if (leftover == 0) return time;
         int addHours = 0;
         minute -= leftover;

         if (!roundLeft) minute += round;
         if (minute > 59)
         {
            minute = minute % 60;
            addHours = 1;
         }

         return new DateTime(time.Year, time.Month, time.Day, time.Hour + addHours, minute, 0);
      }

      /// <summary>
      /// Strips off details after seconds
      /// </summary>
      /// <param name="time"></param>
      /// <returns></returns>
      public static DateTime RoundToSecond(this DateTime time)
      {
         return new DateTime(time.Year, time.Month, time.Day, time.Hour, time.Minute, time.Second, time.Kind);
      }

      /// <summary>
      /// Returns true if the date is today's date.
      /// </summary>
      public static bool IsToday(this DateTime time)
      {
         DateTime now = DateTime.UtcNow;

         return now.Year == time.Year &&
            now.Month == time.Month &&
            now.Day == time.Day;
      }

      /// <summary>
      /// Returns true if the date is tomorrow's date.
      /// </summary>
      public static bool IsTomorrow(this DateTime time)
      {
         TimeSpan diff = DateTime.UtcNow - time;

         return diff.TotalDays >= 1 && diff.TotalDays < 2;
      }

      /// <summary>
      /// Returns date in "HH:mm" format
      /// </summary>
      public static string ToHourMinuteString(this DateTime time)
      {
         return time.ToString("HH:mm");
      }

      /// <summary>
      /// Formats date in ISO 8601 format
      /// </summary>
      public static string ToIso8601DateString(this DateTime time)
      {
         return time.ToString("o");
      }
   }
}


// FILE: src/NetBox/IO/NonCloseableStream.cs

namespace NetBox.IO
{
   using global::System;
   using global::System.IO;

   /// <summary>
   /// Represents a stream that ignores <see cref="IDisposable"/> operations i.e. cannot be closed by the client
   /// </summary>
   class NonCloseableStream : DelegatedStream
   {
      /// <summary>
      /// Creates an instance of this class
      /// </summary>
      /// <param name="master">Master stream to delegate operations to</param>
      public NonCloseableStream(Stream master) : base(master)
      {

      }

      /// <summary>
      /// Overrides this call to do nothing
      /// </summary>
      /// <param name="disposing"></param>
      protected override void Dispose(bool disposing)
      {
         //does nothing on purpose
      }
   }
}


// FILE: src/NetBox/IO/DelegatedStream.cs

namespace NetBox.IO
{
   using global::System;
   using global::System.IO;

   /// <summary>
   /// Makes stream members virtual instead of abstract, allowing to override only specific behaviors.
   /// </summary>
   class DelegatedStream : Stream
   {
      private readonly Stream _master;

      /// <summary>
      /// Creates an instance of non-closeable stream
      /// </summary>
      /// <param name="master"></param>
      public DelegatedStream(Stream master)
      {
         _master = master ?? throw new ArgumentNullException(nameof(master));
      }

      /// <summary>
      /// Calls <see cref="GetCanRead"/>
      /// </summary>
      public override bool CanRead => GetCanRead();

      /// <summary>
      /// Delegates to master by default
      /// </summary>
      /// <returns></returns>
      protected virtual bool GetCanRead()
      {
         return _master.CanRead;
      }

      /// <summary>
      /// Calls <see cref="GetCanSeek"/>
      /// </summary>
      public override bool CanSeek => GetCanSeek();

      /// <summary>
      /// Delegates to master by default
      /// </summary>
      /// <returns></returns>
      protected virtual bool GetCanSeek()
      {
         return _master.CanSeek;
      }

      /// <summary>
      /// Calls <see cref="GetCanWrite"/>
      /// </summary>
      public override bool CanWrite => GetCanWrite();

      /// <summary>
      /// Delegates to master by default
      /// </summary>
      /// <returns></returns>
      protected virtual bool GetCanWrite()
      {
         return _master.CanWrite;
      }

      /// <summary>
      /// Calls <see cref="GetLength"/>
      /// </summary>
      public override long Length => _master.Length;

      /// <summary>
      /// Delegates to master by default
      /// </summary>
      /// <returns></returns>
      protected virtual long GetLength()
      {
         return _master.Length;
      }

      /// <summary>
      /// Delegates to master by default
      /// </summary>
      /// <returns></returns>
      public override long Position { get => _master.Position; set => _master.Position = value; }

      /// <summary>
      /// Delegates to master by default
      /// </summary>
      /// <returns></returns>
      public override void Flush()
      {
         _master.Flush();
      }

      /// <summary>
      /// Delegates to master by default
      /// </summary>
      /// <returns></returns>
      public override int Read(byte[] buffer, int offset, int count)
      {
         return _master.Read(buffer, offset, count);
      }

      /// <summary>
      /// Delegates to master by default
      /// </summary>
      /// <returns></returns>
      public override long Seek(long offset, SeekOrigin origin)
      {
         return _master.Seek(offset, origin);
      }

      /// <summary>
      /// Delegates to master by default
      /// </summary>
      /// <returns></returns>
      public override void SetLength(long value)
      {
         _master.SetLength(value);
      }

      /// <summary>
      /// Delegates to master by default
      /// </summary>
      /// <returns></returns>
      public override void Write(byte[] buffer, int offset, int count)
      {
         _master.Write(buffer, offset, count);
      }
   }
}


// FILE: src/NetBox/FileFormats/Ini/IniKeyValue.cs

namespace NetBox.FileFormats.Ini
{
   using global::System;

   internal class IniKeyValue : IniEntity
   {
      public const string KeyValueSeparator = "=";

      public IniKeyValue(string key, string value, string comment)
      {
         if(key == null) throw new ArgumentNullException(nameof(key));
         Key = key;
         Value = value;
         Comment = comment == null ? null : new IniComment(comment);
      }

      public string Key { get; }

      public string Value { get; set; }

      public IniComment Comment { get; }

      public static IniKeyValue FromLine(string line, bool parseInlineComments)
      {
         int idx = line.IndexOf(KeyValueSeparator, StringComparison.CurrentCulture);
         if(idx == -1) return null;

         string key = line.Substring(0, idx).Trim();
         string value = line.Substring(idx + 1).Trim();
         string comment = null;

         if (parseInlineComments)
         {
            idx = value.LastIndexOf(IniComment.CommentSeparator, StringComparison.CurrentCulture);
            if (idx != -1)
            {
               comment = value.Substring(idx + 1).Trim();
               value = value.Substring(0, idx).Trim();
            }
         }

         return new IniKeyValue(key, value, comment);
      }

      public override string ToString()
      {
         return $"{Value}";
      }
   }
}


// FILE: src/NetBox/FileFormats/Ini/IniEntity.cs

namespace NetBox.FileFormats.Ini
{
   abstract class IniEntity
   {
   }
}


// FILE: src/NetBox/FileFormats/Ini/IniSection.cs

namespace NetBox.FileFormats.Ini
{
   using global::System;
   using global::System.Collections.Generic;
   using global::System.IO;

   class IniSection
   {
      public const string SectionKeySeparator = ".";

      private readonly List<IniEntity> _entities = new List<IniEntity>();
      private readonly Dictionary<string, IniKeyValue> _keyToValue = new Dictionary<string, IniKeyValue>();

      /// <summary>
      /// Section name
      /// </summary>
      public string Name { get; set; }

      /// <summary>
      /// 
      /// </summary>
      /// <param name="name">Pass null to work with global section</param>
      public IniSection(string name)
      {
         if(name != null)
         {
            if (name.StartsWith("[")) name = name.Substring(1);
            if (name.EndsWith("]")) name = name.Substring(0, name.Length - 1);
         }

         Name = name;
      }

      public void Add(IniEntity entity)
      {
         _entities.Add(entity);

         IniKeyValue ikv = entity as IniKeyValue;
         if(ikv != null)
         {
            _keyToValue[ikv.Key] = ikv;
         }
      }

      public IniKeyValue Set(string key, string value)
      {
         if(value == null)
         {
            IniKeyValue ikv;
            if(_keyToValue.TryGetValue(key, out ikv))
            {
               _keyToValue.Remove(key);
               return ikv;
            }
            return null;
         }
         else
         {
            IniKeyValue ikv;
            if(_keyToValue.TryGetValue(key, out ikv))
            {
               ikv.Value = value;
            }
            else
            {
               ikv = new IniKeyValue(key, value, null);
               Add(ikv);
            }
            return ikv;
         }
      }

      public static void SplitKey(string fullKey, out string sectionName, out string keyName)
      {
         int idx = fullKey.IndexOf(SectionKeySeparator, StringComparison.CurrentCulture);

         if(idx == -1)
         {
            sectionName = null;
            keyName = fullKey;
         }
         else
         {
            sectionName = fullKey.Substring(0, idx);
            keyName = fullKey.Substring(idx + 1);
         }
      }

      public void WriteTo(StreamWriter writer)
      {
         foreach(IniEntity entity in _entities)
         {
            IniKeyValue ikv = entity as IniKeyValue;
            if(ikv != null)
            {
               writer.Write($"{ikv.Key}{IniKeyValue.KeyValueSeparator}{ikv.Value}");
               if(ikv.Comment != null)
               {
                  writer.Write(" ");
                  writer.Write(IniComment.CommentSeparator);
                  writer.Write(ikv.Comment.Value);
               }
               writer.WriteLine();
               continue;
            }

            IniComment comment = entity as IniComment;
            if(comment != null)
            {
               writer.Write(IniComment.CommentSeparator);
               writer.WriteLine(comment.Value);
            }
         }
      }

      public override string ToString()
      {
         return Name;
      }
   }
}


// FILE: src/NetBox/FileFormats/Ini/StructuredIniFile.cs

namespace NetBox.FileFormats.Ini
{
   using global::System;
   using global::System.Collections.Generic;
   using global::System.IO;
   using global::System.Linq;
   using global::System.Text;

   class StructuredIniFile
   {
      private const string _sectionBegin = "[";
      private const string _sectionEnd = "]";
      private static readonly char[] _sectionTrims = {'[', ']'};

      private readonly IniSection _globalSection;
      private readonly List<IniSection> _sections = new List<IniSection>();
      private readonly Dictionary<string, IniKeyValue> _fullKeyNameToValue = new Dictionary<string, IniKeyValue>(StringComparer.InvariantCultureIgnoreCase);

      public StructuredIniFile()
      {
         _globalSection = new IniSection(null);
         _sections.Add(_globalSection);
      }

      public string this[string key]
      {
         get
         {
            if(key == null) return null;

            IniKeyValue value;
            return !_fullKeyNameToValue.TryGetValue(key, out value) ? null : value.Value;
         }
         set
         {
            if(key == null) return;

            string sectionName;
            string keyName;
            IniSection.SplitKey(key, out sectionName, out keyName);
            IniSection section = sectionName == null
               ? _globalSection
               : _sections.FirstOrDefault(s => s.Name == sectionName);
            if(section == null)
            {
               section = new IniSection(sectionName);
               _sections.Add(section);
            }
            IniKeyValue ikv = section.Set(keyName, value);

            //update the local cache
            if(ikv != null)
            {
               if(value == null)
               {
                  _fullKeyNameToValue.Remove(key);
               }
               else
               {
                  _fullKeyNameToValue[key] = ikv;
               }
            }
         }
      }

      public static StructuredIniFile FromString(string content, bool parseInlineComments = true)
      {
         using (Stream input = new MemoryStream(Encoding.UTF8.GetBytes(content)))
         {
            return FromStream(input, parseInlineComments);
         }
      }

      public static StructuredIniFile FromStream(Stream inputStream, bool parseInlineComments = true)
      {
         if(inputStream == null) throw new ArgumentNullException(nameof(inputStream));

         var file = new StructuredIniFile();

         using(var reader = new StreamReader(inputStream))
         {
            IniSection section = file._globalSection;

            string line;
            while((line = reader.ReadLine()) != null)
            {
               line = line.Trim();

               if(line.StartsWith(_sectionBegin))
               {
                  //start new section
                  line = line.Trim();
                  section = new IniSection(line);
                  file._sections.Add(section);
               }
               else if(line.StartsWith(IniComment.CommentSeparator))
               {
                  //whole line is a comment
                  string comment = line.Substring(1).Trim();
                  section.Add(new IniComment(comment));
               }
               else
               {
                  IniKeyValue ikv = IniKeyValue.FromLine(line, parseInlineComments);
                  if(ikv == null) continue;

                  section.Add(ikv);
                  string fullKey = section.Name == null
                     ? ikv.Key
                     : $"{section.Name}{IniSection.SectionKeySeparator}{ikv.Key}";
                  file._fullKeyNameToValue[fullKey] = ikv;

               }
            }
         }

         return file;
      }

      public void WriteTo(Stream outputStream)
      {
         if(outputStream == null) throw new ArgumentNullException(nameof(outputStream));

         using(var writer = new StreamWriter(outputStream))
         {
            foreach(IniSection section in _sections)
            {
               if(section.Name != null)
               {
                  writer.WriteLine();
                  writer.WriteLine($"{_sectionBegin}{section.Name}{_sectionEnd}");
               }

               section.WriteTo(writer);
            }
         }
      }

      //private static 
   }
}


// FILE: src/NetBox/FileFormats/Ini/IniComment.cs

namespace NetBox.FileFormats.Ini
{
   class IniComment : IniEntity
   {
      public const string CommentSeparator = ";";

      public IniComment(string value)
      {
         Value = value;
      }

      public string Value { get; set; }

      public override string ToString() => Value;
   }
}


// FILE: src/NetBox/FileFormats/Csv/CsvReader.cs

namespace NetBox.FileFormats.Csv
{
   using global::System.Collections.Generic;
   using global::System.IO;
   using global::System.Linq;
   using global::System.Runtime.CompilerServices;
   using global::System.Text;

   /// <summary>
   /// Reads data from a CSV file. Fast and reliable, supports:
   /// - newline characters
   /// - double quotes
   /// - commas
   /// </summary>
   class CsvReader
   {
      private readonly StreamReader _reader;
      private char[] _buffer;
      private const int BufferSize = 1024 * 10; //10k buffer
      private int _pos;
      private int _size = -1;
      private readonly List<char> _chars = new List<char>();
      private readonly List<string> _row = new List<string>();
      private ValueState _lastState = ValueState.None;

      private enum ValueState
      {
         None,
         HasMore,
         EndOfLine,
         EndOfFile
      }

      /// <summary>
      /// Creates an instance from an open stream and encoding
      /// </summary>
      public CsvReader(Stream stream, Encoding encoding)
      {
         _reader = new StreamReader(stream, encoding);
         _buffer = new char[BufferSize];
      }

      /// <summary>
      /// Reads all file as a dictionary of column name to list of values
      /// </summary>
      /// <param name="content">File content</param>
      /// <param name="hasColumns">When true, the first line of the file includes columns</param>
      /// <returns>Dictionary mapping the column name to the list of values</returns>
      public static Dictionary<string, List<string>> ReadAllFromContent(string content, bool hasColumns = true)
      {
         var result = new Dictionary<string, List<string>>();

         using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(content)))
         {
            var reader = new CsvReader(ms, Encoding.UTF8);

            string[] columnNames = hasColumns ? reader.ReadNextRow() : null;

            string[] values;
            while ((values = reader.ReadNextRow()) != null)
            {
               if (columnNames == null)
               {
                  columnNames = Enumerable.Range(1, values.Length).Select(v => v.ToString()).ToArray();
               }


               for (int i = 0; i < values.Length; i++)
               {
                  if(!result.TryGetValue(columnNames[i], out List<string> list))
                  {
                     list = new List<string>();
                     result[columnNames[i]] = list;
                  }

                  list.Add(values[i]);
               }

            }
         }

         return result;
      }

      /// <summary>
      /// Reads next row of data if available.
      /// </summary>
      /// <returns>Null when end of file is reached, or array of strings for each column.</returns>
      public string[] ReadNextRow()
      {
         if (ValueState.EndOfFile == _lastState) return null;

         _row.Clear();
         _chars.Clear();

         while (ValueState.HasMore == (_lastState = ReadNextValue()))
         {
            _row.Add(Str());
            _chars.Clear();
         }

         _row.Add(Str());

         return _row.ToArray();
      }

      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      private string Str()
      {
         return new string(_chars.ToArray());
      }

      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      private ValueState ReadNextValue()
      {
         int curr, next;
         bool quoted = false;
         short state = 0;
         while (NextChars(out curr, out next))
         {
            switch (state)
            {
               case 0:  //value start
                  if (curr == CsvFormat.ValueQuote)
                  {
                     //if the value starts with quote it:
                     // - ends with quote
                     // - double quote must be transformed into single quote
                     // - column separator (usuallly ',') can be contained within the value
                     // - line separator '\r' can be inside the value and must be transforted to a proper line feed
                     quoted = true;
                     state = 1;
                  }
                  else if (IsLineEndChar(curr))
                  {
                     while (IsLineEndChar(next))
                     {
                        NextChars(out curr, out next);
                     }

                     return next == -1 ? ValueState.EndOfFile : ValueState.EndOfLine;
                  }
                  else if (CsvFormat.ValueSeparator == curr)
                  {
                     //start from value separator, meaning it's an empty value
                     return next == -1 ? ValueState.EndOfFile : ValueState.HasMore;
                  }
                  else
                  {
                     //if the value doesn't start with quote:
                     // - it can't contain column separator or quote characters inside
                     // - it can't contain line separators
                     _chars.Add((char)curr);

                     if (CsvFormat.ValueSeparator == next)
                     {
                        state = 2;
                     }
                     else
                     {
                        state = 1;
                     }
                  }
                  break;

               case 1:  //reading value
                  if (quoted)
                  {
                     switch (curr)
                     {
                        case CsvFormat.ValueQuote:
                           if (next == CsvFormat.ValueQuote)
                           {
                              //escaped quote, make a single one
                              _chars.Add(CsvFormat.ValueQuote);

                              //fast-forward to the next character
                              _pos++;
                           }
                           else if (next == CsvFormat.ValueSeparator || next == '\r' || next == '\n')
                           {
                              //this is the end of value
                              state = 2;
                           }
                           else
                           {
                              throw new IOException($"unexpected character {next} after {curr} at position {_pos}");
                           }
                           break;
                        case '\r':
                           _chars.Add('\r');
                           _chars.Add('\n');
                           break;
                        default:
                           _chars.Add((char)curr);
                           break;
                     }
                  }
                  else
                  {
                     _chars.Add((char)curr);

                     //simple and most common case
                     if (next == CsvFormat.ValueSeparator || next == '\r' || next == '\n')
                     {
                        state = 2;
                     }
                  }
                  break;

               case 2:  //end of value
                  //if the character after end of value (curr) is a value separator it's not the end of line
                  bool hasMore = (curr == CsvFormat.ValueSeparator);

                  if (!hasMore)
                  {
                     while (IsLineEndChar(next))
                     {
                        NextChars(out curr, out next);
                     }
                  }

                  return hasMore
                     ? ValueState.HasMore
                     : (next == -1 ? ValueState.EndOfFile : ValueState.EndOfLine);
            }

         }

         return ValueState.EndOfFile;
      }

      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      private bool NextChars(out int curr, out int next)
      {
         if (_pos >= _size)
         {
            NextBlock();

            if (_size == 0)
            {
               curr = next = -1;
               return false;
            }
         }
         curr = _buffer[_pos++];


         if (_pos >= _size)
         {
            NextBlock();

            if (_size == 0)
            {
               next = -1;
               return true;
            }
         }
         next = _buffer[_pos];
         return true;
      }

      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      private bool NextBlock()
      {
         _size = _reader.ReadBlock(_buffer, 0, BufferSize);
         _pos = 0;
         return _size > 0;
      }

      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      private static bool IsLineEndChar(int ch)
      {
         return ch == '\r' || ch == '\n';
      }
   }
}


// FILE: src/NetBox/FileFormats/Csv/CsvWriter.cs

namespace NetBox.FileFormats.Csv
{
   using global::System;
   using global::System.Collections.Generic;
   using global::System.IO;
   using global::System.Text;

   /// <summary>
   /// Writes data to a CSV file. Fast and reliable, supports:
   /// - newline characters
   /// - double quotes
   /// - commas
   /// </summary>
   class CsvWriter
   {
      private readonly Stream _destination;
      private readonly Encoding _encoding;
      private readonly byte[] _newLine;
      private readonly byte[] _separator;
      private bool _firstRowWritten;

      /// <summary>
      /// Creates a new instance of CsvWriter which uses UTF8 encoding
      /// </summary>
      /// <param name="destination"></param>
      public CsvWriter(Stream destination)
         : this(destination, Encoding.UTF8)
      {

      }

      /// <summary>
      /// Creates a new instance of CsvWriter on disk with UTF8 encoding
      /// </summary>
      /// <param name="fileName">File name or path</param>
      public CsvWriter(string fileName)
         : this(File.Create(fileName), Encoding.UTF8)
      {

      }

      /// <summary>
      /// Creates a new instance of CsvWriter and allows to specify the writer encoding
      /// </summary>
      /// <param name="destination"></param>
      /// <param name="encoding"></param>
      public CsvWriter(Stream destination, Encoding encoding)
      {
         if (destination == null) throw new ArgumentNullException("destination");
         if (encoding == null) throw new ArgumentNullException("encoding");
         if (!destination.CanWrite) throw new ArgumentException("must be writeable", "destination");

         _destination = destination;
         _encoding = encoding;
         _separator = new byte[] { (byte)CsvFormat.ValueSeparator };
         _newLine = _encoding.GetBytes(CsvFormat.NewLine);
      }

      /// <summary>
      /// Writes a row of data
      /// </summary>
      public void Write(params string[] values)
      {
         Write((IEnumerable<string>)values);
      }

      /// <summary>
      /// Writes a row of data
      /// </summary>
      public void Write(IEnumerable<string> values)
      {
         if (values == null) return;

         if (_firstRowWritten) _destination.Write(_newLine, 0, _newLine.Length);

         int i = 0;
         foreach (string column in values)
         {
            if (i != 0) _destination.Write(_separator, 0, _separator.Length);

            byte[] escaped = _encoding.GetBytes(CsvFormat.EscapeValue(column));
            _destination.Write(escaped, 0, escaped.Length);
            i++;
         }

         _firstRowWritten = true;
      }
   }
}


// FILE: src/NetBox/FileFormats/Csv/CsvFormat.cs

namespace NetBox.FileFormats.Csv
{
   static class CsvFormat
   {
      public const char ValueSeparator = ',';
      public const char ValueQuote = '"';
      public static readonly string ValueQuoteStr = "\"";
      public static readonly string ValueQuoteStrStr = "\"\"";
      private static readonly char[] QuoteMark = new[] { ValueSeparator, ValueQuote, '\r', '\n' };

      public static readonly char[] NewLine = { '\r', '\n' };

      private const string ValueLeftBracket = "\"";
      private const string ValueRightBracket = "\"";

      private const string ValueEscapeFind = "\"";
      private const string ValueEscapeValue = "\"\"";

      /// <summary>
      /// Implemented according to RFC4180 http://tools.ietf.org/html/rfc4180
      /// </summary>
      /// <param name="value"></param>
      /// <returns></returns>
      public static string EscapeValue(string value)
      {
         if (string.IsNullOrEmpty(value))
         {
            return string.Empty;
         }

         //the values have to be quoted if they contain either quotes themselves,
         //value separators, or newline characters
         if (value.IndexOfAny(QuoteMark) == -1)
         {
            return value;
         }

         return ValueQuoteStr +
            value
               .Replace(ValueQuoteStr, ValueQuoteStrStr)
               .Replace("\r\n", "\r")
               .Replace("\n", "\r") +
            ValueQuoteStr;
      }

      public static string UnescapeValue(string value)
      {
         if (value == null) return null;

         return value;
      }
   }
}


// FILE: src/NetBox/Generator/RandomGenerator.cs

namespace NetBox.Generator
{
   using global::System;
   using global::System.IO;
   using global::System.Security.Cryptography;
   using global::System.Text;

   /// <summary>
   /// Generates random data using <see cref="RandomNumberGenerator"/> for increased security
   /// </summary>
   public static class RandomGenerator
   {
      private static readonly RandomNumberGenerator Rnd = RandomNumberGenerator.Create();

      //get a cryptographically strong double between 0 and 1
      private static double NextCryptoDouble()
      {
         //fill-in array with 8 random  bytes
         byte[] b = new byte[sizeof(double)];
         Rnd.GetBytes(b);

         //i don't understand this
         ulong ul = BitConverter.ToUInt64(b, 0) / (1 << 11);
         double d = ul / (double)(1UL << 53);
         return d;
      }

      private static int NextCryptoInt()
      {
         byte[] b = new byte[sizeof(int)];
         Rnd.GetBytes(b);
         return BitConverter.ToInt32(b, 0);
      }

      /// <summary>
      /// Generates a random boolean
      /// </summary>
      public static bool RandomBool
      {
         get
         {
            return NextCryptoDouble() >= 0.5d;
         }
      }

      /// <summary>
      /// Generates a random long number between 0 and max
      /// </summary>
      public static long RandomLong => GetRandomLong(0, long.MaxValue);

      /// <summary>
      /// Generates a random integer between 0 and max
      /// </summary>
      public static int RandomInt
      {
         get
         {
            return NextCryptoInt();
         }
      }

      /// <summary>
      /// Returns random double
      /// </summary>
      public static double RandomDouble
      {
         get
         {
            return NextCryptoDouble();
         }
      }

      /// <summary>
      /// Generates a random integer until max parameter
      /// </summary>
      /// <param name="max">Maximum integer value, excluding</param>
      /// <returns></returns>
      public static int GetRandomInt(int max)
      {
         return GetRandomInt(0, max);
      }

      /// <summary>
      /// Generates a random integer number in range
      /// </summary>
      /// <param name="min">Minimum value, including</param>
      /// <param name="max">Maximum value, excluding</param>
      public static int GetRandomInt(int min, int max)
      {
         return (int)Math.Round(NextCryptoDouble() * (max - min - 1)) + min;
      }

      /// <summary>
      /// Generates a random long number in range
      /// </summary>
      /// <param name="min">Minimum value, including</param>
      /// <param name="max">Maximum value, excluding</param>
      public static long GetRandomLong(long min, long max)
      {
         double d = NextCryptoDouble();
         return (long)Math.Round(d * (max - min - 1)) + min;
      }

      /// <summary>
      /// Generates a random enum value by type
      /// </summary>
      public static Enum RandomEnum(Type t)
      {
         Array values = Enum.GetValues(t);

         object value = values.GetValue(GetRandomInt(values.Length));

         return (Enum)value;
      }

#if !NETSTANDARD16
      /// <summary>
      /// Generates a random enum value
      /// </summary>
      /// <typeparam name="T">Enumeration type</typeparam>
      public static T GetRandomEnum<T>() where T : struct
      {
         //can't limit generics to enum http://connect.microsoft.com/VisualStudio/feedback/details/386194/allow-enum-as-generic-constraint-in-c

         if (!typeof(T).IsEnum) throw new ArgumentException("T must be an enum");

         return (T)(object)RandomEnum(typeof(T));
      }
#endif

      /// <summary>
      /// Generates a random date in range
      /// </summary>
      /// <param name="minValue">Minimum date, including</param>
      /// <param name="maxValue">Maximum date, excluding</param>
      public static DateTime GetRandomDate(DateTime minValue, DateTime maxValue)
      {
         long randomTicks = GetRandomLong(minValue.Ticks, maxValue.Ticks);

         return new DateTime(randomTicks);
      }

      /// <summary>
      /// Generates a random date value
      /// </summary>
      public static DateTime RandomDate
      {
         get { return GetRandomDate(DateTime.MinValue, DateTime.MaxValue); }
      }

      /// <summary>
      /// Generates a random string. Never returns null.
      /// </summary>
      public static string RandomString
      {
         get
         {
            string path = Path.GetRandomFileName();
            path = path.Replace(".", "");
            return path;
         }
      }

      /// <summary>
      /// Generates a random string
      /// </summary>
      /// <param name="length">string length</param>
      /// <param name="allowNulls">Whether to allow to return null values</param>
      public static string GetRandomString(int length, bool allowNulls)
      {
         if (allowNulls && RandomLong % 2 == 0) return null;

         var builder = new StringBuilder();
         char ch;
         for (int i = 0; i < length; i++)
         {
            ch = Convert.ToChar(Convert.ToInt32(Math.Floor(26 * RandomDouble + 65)));
            builder.Append(ch);
         }

         return builder.ToString();
      }

      /// <summary>
      /// Generates a random URL in format "http://random.com/random.random
      /// </summary>
      /// <param name="allowNulls">Whether to allow to return nulls</param>
      public static Uri GetRandomUri(bool allowNulls)
      {
         if (allowNulls && RandomLong % 2 == 0) return null;

         return new Uri($"http://{RandomString}.com/{RandomString}.{GetRandomString(3, false)}");
      }

      /// <summary>
      /// Generates a random URL in format "http://random.com/random.random. Never returns null values.
      /// </summary>
      public static Uri RandomUri
      {
         get { return GetRandomUri(false); }
      }

      /// <summary>
      /// Generates a random sequence of bytes of a specified size
      /// </summary>
      public static byte[] GetRandomBytes(int minSize, int maxSize)
      {
         int size = minSize == maxSize ? minSize : GetRandomInt(minSize, maxSize);
         byte[] data = new byte[size];
         Rnd.GetBytes(data);
         return data;
      }

   }
}
