Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@

[assembly: Guid("29445467-fd04-458f-9bd4-fcd842b0468d")]

[assembly: InternalsVisibleTo("HuffmanCompressorTests")]
[assembly: InternalsVisibleTo("HuffmanCompressor.Tests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\HuffmanCompressor\HuffmanCompressorLib.csproj" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.8" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\HuffmanCompressor.Lib\HuffmanCompressor.Lib.csproj" />
</ItemGroup>

</Project>
80 changes: 80 additions & 0 deletions HuffmanCompressor.Cmd/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
namespace HuffmanCompressor.Cmd;

using System;
using Microsoft.Extensions.DependencyInjection;
using HuffmanCompressor.Lib;

/// <summary>
/// Main entry point for the HuffmanCompressorCmd console application.
/// </summary>
public class Program
{
private IFileCompressor _compressor;

/// <summary>
/// Constructor of the Program class.
/// </summary>
public Program(IFileCompressor compressor)
{
_compressor = compressor;
}

/// <summary>
/// The main entry point (Main method) for the application.
/// </summary>
/// <param name="args">Program arguments.</param>
public static void Main(string[] args)
{
var serviceProvider = ConfigureServices();
var program = new Program(serviceProvider.GetRequiredService<IFileCompressor>());
program.Run(args);
}

/// <summary>
/// The main entry point (Main method) for the application always needs to be declared as static.
/// That means that it cannot access non-static members of the class, such as _compressor.
/// Declaring the _compressor member as static would create a single instance of the compressor for all instances of the Program class.
/// This would be a problem if the Program class was used in a multi-threaded environment, such as when unit tests are run, where we
/// may run parallel compression jobs or even want to inject a mock compressor for testing purposes.
/// To avoid this, we create the Run wrapper as non-static and have Main create an instance of the Program class to invoke it.
/// </summary>
/// <param name="args">Program args</param>
/// <exception cref="ArgumentException"></exception>
public void Run(string[] args)
{
const string usageString = "Usage: [compress|inflate] [input file path] [output file path]";

if (args.Length != 3)
{
Console.WriteLine(usageString);
throw new ArgumentException(usageString);
}

switch (args[0].ToLower())
{
case "compress":
_compressor.Compress(args[1], args[2]);
break;
case "inflate":
_compressor.Inflate(args[1], args[2]);
break;
default:
Console.WriteLine(usageString);
throw new ArgumentException(usageString);
}
}

/// <summary>
/// Configures dependency injection container.
/// </summary>
/// <returns>Service provider with registered services.</returns>
private static IServiceProvider ConfigureServices()
{
var services = new ServiceCollection();

// Register IFileCompressor with HuffmanCompressor
services.AddTransient<IFileCompressor, HuffmanCompressor>();

return services.BuildServiceProvider();
}
}
49 changes: 49 additions & 0 deletions HuffmanCompressor.Lib/BitReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
namespace HuffmanCompressor.Lib;

/// <summary>
/// Provides functionality to read individual bits from an input file stream.
/// </summary>
internal class BitReader
{
private int bitIndex;
private byte currentByte;
private readonly FileStream fileHandle;

/// <summary>
/// Creates an instance of <see cref="BitReader"/> class.
/// </summary>
/// <param name="fileHandle">File stream to which the reader will read bits from.</param>
public BitReader(FileStream fileHandle)
{
ArgumentNullException.ThrowIfNull(fileHandle);
this.bitIndex = 8;
this.currentByte = 0x00;
this.fileHandle = fileHandle;
}

/// <summary>
/// Reads a bit from the file.
/// </summary>
/// <returns>Bit represented as a '0' or '1' character.</returns>
/// <exception cref="EndOfStreamException">If the end of the stream is reached while attempting to read.</exception>
public char ReadNextBit()
{
// Read 1 byte from the input file stream at a time, and use an index to return each bit from the byte.
// When we get to the last bit, read the next byte and continue until end of file
if (this.bitIndex >= 8)
{
var byteValue = this.fileHandle.ReadByte();
if (byteValue == -1)
{
throw new EndOfStreamException("Attempting to read past the end of the stream!");
}
this.currentByte = (byte)byteValue;
this.bitIndex = 0;
}

byte mask = (byte)(0x80 >> this.bitIndex++);
return (this.currentByte & mask) == 0x00
? '0'
: '1';
}
}
76 changes: 76 additions & 0 deletions HuffmanCompressor.Lib/BitWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
namespace HuffmanCompressor.Lib;

/// <summary>
/// Provides functionality to write individual bits to an output file stream.
/// </summary>
internal class BitWriter
{
private int bitIndex;
private byte currentByte;
private readonly FileStream fileHandle;

/// <summary>
/// Creates an instance of <see cref="BitWriter"/> class.
/// </summary>
/// <param name="fileHandle">File stream to which the writer will write bits to.</param>
public BitWriter(FileStream fileHandle)
{
ArgumentNullException.ThrowIfNull(fileHandle);
this.bitIndex = 0;
this.currentByte = 0x00;
this.fileHandle = fileHandle;
}

/// <summary>
/// Writes bits to the output file.
/// </summary>
/// <param name="bitString">String of bits, only 0 and 1 are supported.</param>
/// <exception cref="ArgumentException">In case the bit string contains unsupported characters.</exception>
public void WriteBits(string bitString)
{
ArgumentNullException.ThrowIfNull(bitString);

// Fill in currentByte from most significant bit to least significant bit by reading from input bitString
// until the input bitString is exhausted.
// As we fill out all available bits in currentByte, we flush it out to the file and start over with a fresh byte
for (int i = 0; i < bitString.Length; i++)
{
var bit = bitString[i];
switch(bit)
{
case '0':
break;
case '1':
// mask a '1' on the current byte
byte mask = 0x80;
mask = (byte)(mask >> bitIndex);
currentByte |= mask;
break;
default:
throw new ArgumentException($"Invalid bit detected {bit}");
}

if (bitIndex == 7)
{
// currentByte has been exhausted for all of its 8 available bits
// commit to disk and mint a new byte to continue the process.
fileHandle.WriteByte(currentByte);
bitIndex = 0;
currentByte = 0x00;
}
else
{
bitIndex++;
}
}
}

/// <summary>
/// Write the outstanding byte to disk, prior to closing the file handle.
/// The remaining bits will be padding.
/// </summary>
public void Flush()
{
fileHandle.WriteByte(currentByte);
}
}
126 changes: 126 additions & 0 deletions HuffmanCompressor.Lib/FrequencyCounter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
namespace HuffmanCompressor.Lib;

/// <summary>
/// Class to count the relative frequency of each byte in a stream of data.
/// There is no upper bound on the number of bytes that can be counted.
/// If the counter reaches the maximum value, the class will rebase all of the frequencies to avoid overflows.
/// </summary>
internal class FrequencyCounter
{
private readonly IDictionary<byte, UInt32> moduloCounter;
private readonly IDictionary<byte, UInt32> frequencies;
private int multiplier;

/// <summary>
/// Initializes a new instance of the <see cref="FrequencyCounter"/> class.
/// </summary>
public FrequencyCounter()
{
this.moduloCounter = new Dictionary<byte, UInt32>();
this.frequencies = new Dictionary<byte, UInt32>();
multiplier = 1;

// Initialize the dictionaries to make the code more straightforward in the rest of the class.
// Avoid overflow of counter that would lead to an infinite loop using an int
// https://stackoverflow.com/questions/43800147/iterating-from-minvalue-to-maxvalue-with-overflow
for (int i = byte.MinValue; i <= byte.MaxValue; i++)
{
this.moduloCounter.Add((byte)i, 0);
this.frequencies.Add((byte)i, 0);
}
}

/// <summary>
/// Increment the frequency of a byte by 1.
/// </summary>
/// <param name="value">The value to increment the frequency of.</param>
public void Increment(byte value)
{
// Check to see if we have reached the rare theoretic limit for frequency counting.
// In that case, the best we can do is stop counting to avoid an overlfow back to 0.
// Note: this would still mean having read 16 million TB of a single character!
if (this.moduloCounter[value] == UInt32.MaxValue &&
this.frequencies[value] == UInt32.MaxValue)
{
return;
}

this.moduloCounter[value]++;
if (this.moduloCounter[value] == multiplier)
{
// If the counter is about to overflow, we call the Rebase function
if (this.frequencies[value] == UInt32.MaxValue)
{
this.Rebase();
}
else
{
this.frequencies[value]++;
}

this.moduloCounter[value] = 0;
}
}

/// <summary>
/// Set the frequency of a byte to a specific value.
/// </summary>
/// <param name="value">The value to set.</param>
/// <param name="frequency">The frequency to set.</param>
public void SetFrequency(byte value, UInt32 frequency)
{
this.frequencies[(byte)value] = frequency;
this.moduloCounter[(byte)value] = 0;
}

/// <summary>
/// Get the frequency of a byte.
/// </summary>
/// <param name="value">The value to query the frequency of</param>
/// <returns>Frequency of the value.</returns>
public UInt32 GetFrequency(byte value)
{
// First check for the case that the frequency counter has not yet been
// bumped up, in that case, if we ever counted the value in the modulo counter, we must return
// the smallest value possible, which is 1.
if (this.frequencies[value] == 0)
{
if (this.moduloCounter[value] > 0)
{
return 1;
}

return 0;
}

return this.frequencies[value];
}

/// <summary>
/// Enumerator to allow caller to cycle through frequencies.
/// </summary>
/// <returns>Key value pair enumaration of non-zero frequencies</returns>
public IEnumerable<KeyValuePair<byte, UInt32>> GetEnumerator()
{
for (int b = byte.MinValue; b <= byte.MaxValue; b++)
{
var frequency = this.GetFrequency((byte)b);
if (frequency > 0)
{
yield return new KeyValuePair<byte, UInt32>((byte)b, frequency);
}
}
}

/// <summary>
/// Rebases all of the frequencies uniformly by dividing them by 2
/// This allows us to handle very high counts without risk of overflowing and still
/// preserving statistical accuracy.
/// </summary>
private void Rebase()
{
this.frequencies.ToList().ForEach( kvp => this.frequencies[kvp.Key] /= 2 + 1);
this.multiplier *= 2;
}

}
Loading
Loading