From c5fa6fcc37db2e08070557f55fd56be710dc92fa Mon Sep 17 00:00:00 2001
From: chylex <contact@chylex.com>
Date: Thu, 15 Nov 2018 11:49:31 +0100
Subject: [PATCH] Add BitStream w/ unit tests

---
 BrotliLib/IO/BitStream.cs     | 214 ++++++++++++++++++++++++++++++++++
 UnitTests/IO/TestBitStream.fs | 135 +++++++++++++++++++++
 UnitTests/UnitTests.fsproj    |   1 +
 3 files changed, 350 insertions(+)
 create mode 100644 BrotliLib/IO/BitStream.cs
 create mode 100644 UnitTests/IO/TestBitStream.fs

diff --git a/BrotliLib/IO/BitStream.cs b/BrotliLib/IO/BitStream.cs
new file mode 100644
index 0000000..27a8f19
--- /dev/null
+++ b/BrotliLib/IO/BitStream.cs
@@ -0,0 +1,214 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace BrotliLib.IO{
+    public class BitStream : IEnumerable<bool>{
+        private const char False = '0';
+        private const char True = '1';
+
+        private const int ByteSize = 8;
+        private const int BytesPerEntry = sizeof(ulong);
+        private const int BitEntrySize = ByteSize * BytesPerEntry;
+
+        // Instance
+
+        public int Length { get; private set; }
+        
+        private readonly LinkedList<ulong> bitCollection = new LinkedList<ulong>();
+        private LinkedListNode<ulong> lastNode;
+        private int lastNodeIndex;
+        
+        #region Construction
+
+        /// <summary>
+        /// Initializes an empty <see cref="BitStream"/>.
+        /// </summary>
+        public BitStream(){
+            this.lastNode = this.bitCollection.AddLast(0L);
+            this.lastNodeIndex = 0;
+        }
+
+        /// <summary>
+        /// Initializes a <see cref="BitStream"/> from a string consisting of 0s and 1s.
+        /// </summary>
+        /// <param name="bits">Input string. Must be either empty, or only contain the characters 0 and 1.</param>
+        /// <exception cref="ArgumentOutOfRangeException">Thrown when the input <paramref name="bits"/> string contains a character that is not 0 or 1.</exception>
+        public BitStream(string bits) : this(){
+            foreach(char chr in bits){
+                switch(chr){
+                    case False: this.Add(false); break;
+                    case True: this.Add(true); break;
+                    default: throw new ArgumentOutOfRangeException(nameof(bits), "Invalid character found in input string: "+chr);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Initializes a <see cref="BitStream"/> from a byte array.
+        /// </summary>
+        /// <param name="bytes">Input byte array segment.</param>
+        public BitStream(byte[] bytes) : this(){
+            int index = 0;
+
+            foreach(byte value in bytes){
+                int offset = index % BytesPerEntry;
+
+                if (offset == 0 && index > 0){
+                    this.lastNode = this.bitCollection.AddLast(0L);
+                    this.lastNodeIndex += BitEntrySize;
+                }
+                
+                this.lastNode.Value |= (ulong)value << (ByteSize * offset);
+                ++index;
+            }
+
+            this.Length = ByteSize * index;
+        }
+
+        /// <summary>
+        /// Initializes a new <see cref="BitStream"/> as a clone of <paramref name="source"/>.
+        /// </summary>
+        /// <param name="source">Source stream.</param>
+        private BitStream(BitStream source){
+            foreach(ulong bitEntry in source.bitCollection){
+                this.bitCollection.AddLast(bitEntry);
+            }
+
+            this.lastNode = this.bitCollection.Last;
+            this.lastNodeIndex = source.lastNodeIndex;
+            this.Length = source.Length;
+        }
+
+        /// <summary>
+        /// Returns a copy of the object, which can be modified without affecting the original object.
+        /// </summary>
+        public BitStream Clone(){
+            return new BitStream(this);
+        }
+
+        #endregion
+
+        #region Mutation
+
+        /// <summary>
+        /// Appends a bit to the end of the stream.
+        /// </summary>
+        /// <param name="bit">Input bit.</param>
+        public void Add(bool bit){
+            int offset = Length - lastNodeIndex;
+
+            if (offset >= BitEntrySize){
+                lastNode = bitCollection.AddLast(0L);
+                lastNodeIndex += BitEntrySize;
+                offset -= BitEntrySize;
+            }
+            
+            if (bit){
+                lastNode.Value |= 1UL << offset;
+            }
+            else{
+                lastNode.Value &= lastNode.Value & ~(1UL << offset);
+            }
+
+            ++Length;
+        }
+
+        #endregion
+
+        #region Enumeration
+
+        /// <summary>
+        /// Returns an enumerator that traverses the stream, converting 0s to false, and 1s to true.
+        /// </summary>
+        public IEnumerator<bool> GetEnumerator(){
+            int bitsLeft = Length;
+
+            foreach(ulong bitEntry in bitCollection){
+                for(int bitIndex = 0; bitIndex < BitEntrySize; bitIndex++){
+                    if (--bitsLeft < 0){
+                        yield break;
+                    }
+
+                    yield return (bitEntry & (1UL << bitIndex)) != 0;
+                }
+            }
+        }
+
+        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+        #endregion
+
+        #region Conversion
+
+        /// <summary>
+        /// Converts the stream into a byte array, with zero padding at the end if needed.
+        /// </summary>
+        public byte[] ToByteArray(){
+            const int ByteMask = (1 << ByteSize) - 1;
+
+            byte[] bytes = new byte[(Length + ByteSize - 1) / ByteSize];
+            int index = -1;
+
+            foreach(ulong bitEntry in bitCollection){
+                if (++index >= bytes.Length){
+                    break;
+                }
+
+                bytes[index] = (byte)(bitEntry & ByteMask);
+
+                for(int byteOffset = 1; byteOffset < BytesPerEntry; byteOffset++){
+                    if (++index >= bytes.Length){
+                        break;
+                    }
+
+                    bytes[index] = (byte)((bitEntry >> (ByteSize * byteOffset)) & ByteMask);
+                }
+            }
+
+            return bytes;
+        }
+
+        /// <summary>
+        /// Converts the stream into its text representation. The returned string is empty if the stream is also empty, or contains a sequence 0s and 1s.
+        /// </summary>
+        public override string ToString(){
+            StringBuilder build = new StringBuilder(Length);
+
+            foreach(bool bit in this){
+                build.Append(bit ? True : False);
+            }
+
+            return build.ToString();
+        }
+
+        #endregion
+
+        #region Equality
+
+        /// <summary>
+        /// Returns the hash code for this instance.
+        /// </summary>
+        public override int GetHashCode(){
+            int hash = Length * 17;
+
+            foreach(ulong bitEntry in bitCollection){
+                hash = unchecked((hash * 31) + (bitEntry.GetHashCode()));
+            }
+
+            return hash;
+        }
+
+        /// <summary>
+        /// Returns true if and only if the <paramref name="obj"/> parameter is a <see cref="BitStream"/> with the same <see cref="Length"/> and contents.
+        /// </summary>
+        /// <param name="obj"></param>
+        public override bool Equals(object obj){
+            return obj is BitStream other && Length == other.Length && bitCollection.SequenceEqual(other.bitCollection);
+        }
+
+        #endregion
+    }
+}
diff --git a/UnitTests/IO/TestBitStream.fs b/UnitTests/IO/TestBitStream.fs
new file mode 100644
index 0000000..e66ae4b
--- /dev/null
+++ b/UnitTests/IO/TestBitStream.fs
@@ -0,0 +1,135 @@
+namespace UnitTests.IO.TestBitStream
+
+open Xunit
+open System
+open BrotliLib.IO
+
+
+module Representations =
+    [<Theory>]
+    [<InlineData((*  0*)"")>]
+    [<InlineData((*  1*)"0")>]
+    [<InlineData((*  2*)"00")>]
+    [<InlineData((*  3*)"000")>]
+    [<InlineData((*  4*)"0000")>]
+    [<InlineData((*  8*)"11110000")>]
+    [<InlineData((* 16*)"1111000011110000")>]
+    [<InlineData((* 32*)"11110000111100001111000011110000")>]
+    [<InlineData((* 64*)"1111000011110000111100001111000011110000111100001111000011110000")>]
+    [<InlineData((*128*)"11110000111100001111000011110000111100001111000011110000111100001111000011110000111100001111000011110000111100001111000011110000")>]
+    [<InlineData((*129*)"111100001111000011110000111100001111000011110000111100001111000011110000111100001111000011110000111100001111000011110000111100001")>]
+    let ``constructing from string yields same length and string representation`` (bits: string) =
+        let stream = BitStream(bits)
+        Assert.Equal(bits.Length, stream.Length)
+        Assert.Equal(bits, stream.ToString())
+        
+    [<Theory>]
+    [<InlineData("1100 ")>]
+    [<InlineData("1111211")>]
+    [<InlineData("0000_1111")>]
+    let ``constructing from string with invalid characters throws exception`` (bits: string) =
+        Assert.Throws<ArgumentOutOfRangeException>(fun () -> BitStream(bits) |> ignore)
+        
+    [<Theory>]
+    [<InlineData()>]
+    [<InlineData(0b00000000uy)>]
+    [<InlineData(0b11100110uy)>]
+    [<InlineData(0b00000000uy, 0b11111111uy)>]
+    [<InlineData(0b11111111uy, 0b00000000uy, 0b11111111uy)>]
+    [<InlineData(0b00000000uy, 0b11111111uy, 0b00000000uy, 0b11111111uy)>]
+    [<InlineData(0b11111111uy, 0b00000000uy, 0b11111111uy, 0b00000000uy, 0b11111111uy)>]
+    [<InlineData(0b10101010uy, 0b01010101uy, 0b10101010uy, 0b01010101uy, 0b10101010uy, 0b01010101uy, 0b10101010uy, 0b01010101uy, 0b10101010uy)>]
+    let ``constructing from byte array yields same length and byte array representation`` ([<ParamArray>] bytes: byte array) =
+        let stream = BitStream(bytes)
+        Assert.Equal(bytes.Length * 8, stream.Length)
+        Assert.Equal<byte array>(bytes, stream.ToByteArray())
+        
+    [<Theory>]
+    [<InlineData("0", 0b00000000uy)>]
+    [<InlineData("1", 0b00000001uy)>]
+    [<InlineData("11", 0b00000011uy)>]
+    [<InlineData("1111111", 0b01111111uy)>]
+    [<InlineData("11111111", 0b11111111uy)>]
+    [<InlineData("01010101", 0b10101010uy)>]
+    [<InlineData("111111110", 0b11111111uy, 0b00000000uy)>]
+    [<InlineData("111111111", 0b11111111uy, 0b00000001uy)>]
+    [<InlineData("11110000111100001111001", 0b00001111uy, 0b00001111uy, 0b01001111uy)>]
+    let ``constructing from string yields correct byte array representation with zero padding`` (bits: string, [<ParamArray>] bytes: byte array) =
+        Assert.Equal<byte array>(bytes, BitStream(bits).ToByteArray())
+        
+
+module Equality =
+    let equal : obj array seq = seq {
+        yield [| "" |]
+        yield [| "0" |]
+        yield [| "1" |]
+        yield [| "10101010" |]
+        yield [| "000000000" |]
+    }
+
+    let notequal : obj array seq = seq {
+        yield [| ""; "0" |]
+        yield [| "0"; "1" |]
+        yield [| "00"; "000" |]
+        yield [| "10101010"; "01010101" |]
+        yield [| "00000000"; "0000000000" |]
+        yield [| "000000001"; "000000000" |]
+    }
+
+    [<Theory>]
+    [<MemberData("equal")>]
+    let ``two streams are equal if they have same contents`` (bits: string) =
+        Assert.True(BitStream(bits).Equals(BitStream(bits)))
+        
+    [<Theory>]
+    [<MemberData("notequal")>]
+    let ``two streams are not equal if they have different contents`` (first: string, second: string) =
+        Assert.False(BitStream(first).Equals(BitStream(second)))
+
+    [<Theory>]
+    [<MemberData("equal")>]
+    let ``two equal streams have the same hash code`` (bits: string) =
+        Assert.Equal(BitStream(bits).GetHashCode(), BitStream(bits).GetHashCode())
+        
+    [<Theory>]
+    [<MemberData("notequal")>]
+    let ``two different streams should generally have different hash codes`` (first: string, second: string) =
+        Assert.NotEqual(BitStream(first).GetHashCode(), BitStream(second).GetHashCode())
+        
+
+module Mutability =
+    [<Theory>]
+    [<InlineData("")>]
+    [<InlineData("1", true)>]
+    [<InlineData("0", false)>]
+    [<InlineData("10", true, false)>]
+    [<InlineData("11110000", true, true, true, true, false, false, false, false)>]
+    [<InlineData("11110000111100001", true, true, true, true, false, false, false, false, true, true, true, true, false, false, false, false, true)>]
+    let ``appending to empty stream yields correct text representation`` (expected: string, [<ParamArray>] values: bool array) =
+        let stream = BitStream()
+        Array.iter (stream.Add) <| values
+
+        Assert.Equal(expected, stream.ToString())
+
+
+module Cloning =
+    [<Theory>]
+    [<InlineData("")>]
+    [<InlineData("0")>]
+    [<InlineData("1")>]
+    [<InlineData("101010101")>]
+    let ``cloned stream is equal to original stream but not the same object`` (bits: string) =
+        let original = BitStream(bits)
+        let clone = original.Clone()
+
+        Assert.Equal<BitStream>(original, clone)
+        Assert.NotSame(original, clone)
+
+    [<Fact>]
+    let ``appending to cloned stream keeps the original stream intact`` () =
+        let original = BitStream("11100")
+        let clone = original.Clone()
+        clone.Add(true)
+
+        Assert.Equal("11100", original.ToString())
+        Assert.Equal("111001", clone.ToString())
diff --git a/UnitTests/UnitTests.fsproj b/UnitTests/UnitTests.fsproj
index 35d5079..f4d3a5e 100644
--- a/UnitTests/UnitTests.fsproj
+++ b/UnitTests/UnitTests.fsproj
@@ -56,6 +56,7 @@
   </Target>
   <Import Project="..\packages\xunit.core.2.4.1\build\xunit.core.targets" Condition="Exists('..\packages\xunit.core.2.4.1\build\xunit.core.targets')" />
   <ItemGroup>
+    <Compile Include="IO\TestBitStream.fs" />
     <Content Include="packages.config" />
   </ItemGroup>
   <ItemGroup>