Skip to content

Commit

Permalink
🎉 Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Marthijn van den Heuvel committed Oct 3, 2024
1 parent f166739 commit 0dfd0ac
Show file tree
Hide file tree
Showing 9 changed files with 415 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -396,3 +396,4 @@ FodyWeavers.xsd

# JetBrains Rider
*.sln.iml
.idea/
38 changes: 38 additions & 0 deletions Sidio.Text.Base32.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sidio.Text.Base32", "src\Sidio.Text.Base32\Sidio.Text.Base32.csproj", "{E66C81D0-1720-4A06-91F0-AAC98FCB3330}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sidio.Text.Base32.Tests", "src\Sidio.Text.Base32.Tests\Sidio.Text.Base32.Tests.csproj", "{75D71CC9-4477-46C1-B453-BA87B05D81AD}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Src", "Src", "{8EEBD6D5-76C3-4E98-98FF-D9D722F9EC94}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{11912237-0E48-4977-81D6-66673625E4AE}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{E1E76740-2BA0-44D5-A6AA-7F5FE072EDEB}"
ProjectSection(SolutionItems) = preProject
.gitignore = .gitignore
LICENSE = LICENSE
README.md = README.md
icon.png = icon.png
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{E66C81D0-1720-4A06-91F0-AAC98FCB3330}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E66C81D0-1720-4A06-91F0-AAC98FCB3330}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E66C81D0-1720-4A06-91F0-AAC98FCB3330}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E66C81D0-1720-4A06-91F0-AAC98FCB3330}.Release|Any CPU.Build.0 = Release|Any CPU
{75D71CC9-4477-46C1-B453-BA87B05D81AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{75D71CC9-4477-46C1-B453-BA87B05D81AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{75D71CC9-4477-46C1-B453-BA87B05D81AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{75D71CC9-4477-46C1-B453-BA87B05D81AD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{E66C81D0-1720-4A06-91F0-AAC98FCB3330} = {8EEBD6D5-76C3-4E98-98FF-D9D722F9EC94}
{75D71CC9-4477-46C1-B453-BA87B05D81AD} = {11912237-0E48-4977-81D6-66673625E4AE}
EndGlobalSection
EndGlobal
Binary file added icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
57 changes: 57 additions & 0 deletions src/Sidio.Text.Base32.Tests/Base32EncodingTests.Hex.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System.Collections;
using System.Text;
using FluentAssertions;

namespace Sidio.Text.Base32.Tests;

public partial class Base32EncodingTests
{
[Theory]
[ClassData(typeof(Base32HexTestVectors))]
public void FromHexString_ReturnsByteArray(string input, string base32)
{
// act
var result = Base32Encoding.FromHexString(base32);

// assert
result.Should().NotBeNull();
var stringResult = Encoding.UTF8.GetString(result);
stringResult.Should().BeEquivalentTo(input);
}

[Theory]
[ClassData(typeof(Base32HexTestVectors))]
public void ToHexString_ReturnsBase32HexString(string input, string base32)
{
// arrange
var bytes = Encoding.UTF8.GetBytes(input);

// act
var result = Base32Encoding.ToHexString(bytes);

// assert
result.Should().NotBeNull();
result.Should().Be(base32);
}

private sealed class Base32HexTestVectors : IEnumerable<object[]>
{
private readonly List<object?[]> _data = [];

public Base32HexTestVectors()
{
// RFC test vectors https://datatracker.ietf.org/doc/html/rfc4648#page-8
_data.Add(["", ""]);
_data.Add(["f", "CO======"]);
_data.Add(["fo", "CPNG===="]);
_data.Add(["foo", "CPNMU==="]);
_data.Add(["foob", "CPNMUOG="]);
_data.Add(["fooba", "CPNMUOJ1"]);
_data.Add(["foobar", "CPNMUOJ1E8======"]);
}

public IEnumerator<object[]> GetEnumerator() => _data.GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
60 changes: 60 additions & 0 deletions src/Sidio.Text.Base32.Tests/Base32EncodingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System.Collections;
using System.Text;
using FluentAssertions;

namespace Sidio.Text.Base32.Tests;

public partial class Base32EncodingTests
{
[Theory]
[ClassData(typeof(Base32TestVectors))]
public void FromString_ReturnsByteArray(string input, string base32)
{
// act
var result = Base32Encoding.FromString(base32);

// assert
result.Should().NotBeNull();
var stringResult = Encoding.UTF8.GetString(result);
stringResult.Should().BeEquivalentTo(input);
}

[Theory]
[ClassData(typeof(Base32TestVectors))]
public void ToString_ReturnsBase32String(string input, string base32)
{
// arrange
var bytes = Encoding.UTF8.GetBytes(input);

// act
var result = Base32Encoding.ToString(bytes);

// assert
result.Should().NotBeNull();
result.Should().Be(base32);
}

private sealed class Base32TestVectors : IEnumerable<object[]>
{
private readonly List<object?[]> _data = [];

public Base32TestVectors()
{
// RFC test vectors https://datatracker.ietf.org/doc/html/rfc4648#page-8
_data.Add(["", ""]);
_data.Add(["f", "MY======"]);
_data.Add(["fo", "MZXQ===="]);
_data.Add(["foo", "MZXW6==="]);
_data.Add(["foob", "MZXW6YQ="]);
_data.Add(["fooba", "MZXW6YTB"]);
_data.Add(["foobar", "MZXW6YTBOI======"]);

// additional test vectors
_data.Add(["hello, world!", "NBSWY3DPFQQHO33SNRSCC==="]);
}

public IEnumerator<object[]> GetEnumerator() => _data.GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
28 changes: 28 additions & 0 deletions src/Sidio.Text.Base32.Tests/Sidio.Text.Base32.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
<PackageReference Include="FluentAssertions" Version="6.12.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
<PackageReference Include="xunit" Version="2.5.3"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3"/>
</ItemGroup>

<ItemGroup>
<Using Include="Xunit"/>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Sidio.Text.Base32\Sidio.Text.Base32.csproj" />
</ItemGroup>

</Project>
36 changes: 36 additions & 0 deletions src/Sidio.Text.Base32/Base32Encoding.Hex.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
namespace Sidio.Text.Base32;

public static partial class Base32Encoding
{
private static readonly char[] Base32HexTable =
[
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V'
];

private static readonly int[] Base32HexDecodeMap = CreateDecodeMap(Base32HexTable);

/// <summary>
/// Converts a base32 hex string to a byte array.
/// </summary>
/// <param name="input">The input string.</param>
/// <returns>A <see cref="byte"/> array.</returns>
public static byte[] FromHexString(string input)
{
ArgumentNullException.ThrowIfNull(input);

// remove padding '=' characters from the end of the input
return input.Length == 0 ? [] : FromString(input.AsSpan().TrimEnd('='), Base32HexDecodeMap);
}

/// <summary>
/// Converts a byte array to a base32 hex string.
/// </summary>
/// <param name="inArray">The input array.</param>
/// <returns>A base32 <see cref="string"/>.</returns>
public static string ToHexString(byte[] inArray)
{
ArgumentNullException.ThrowIfNull(inArray);
return inArray.Length == 0 ? string.Empty : ToString(new ReadOnlySpan<byte>(inArray), Base32HexTable);
}
}
151 changes: 151 additions & 0 deletions src/Sidio.Text.Base32/Base32Encoding.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
namespace Sidio.Text.Base32;

/// <summary>
/// The base32 encoding class.
/// </summary>
public static partial class Base32Encoding
{
private const char UnitSeparator = (char)0x1F;
private const char MaxByte = (char)0xFF;

private static readonly char[] Base32Table =
[
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '2', '3', '4', '5', '6', '7', '='
];

private static readonly int[] Base32DecodeMap = CreateDecodeMap(Base32Table);

/// <summary>
/// Converts a base32 string to a byte array.
/// </summary>
/// <param name="input">The input string.</param>
/// <returns>A <see cref="byte"/> array.</returns>
public static byte[] FromString(string input)
{
ArgumentNullException.ThrowIfNull(input);

// remove padding '=' characters from the end of the input
return input.Length == 0 ? [] : FromString(input.AsSpan().TrimEnd('='), Base32DecodeMap);
}

/// <summary>
/// Converts a byte array to a base32 string.
/// </summary>
/// <param name="inArray">The input array.</param>
/// <returns>A base32 <see cref="string"/>.</returns>
public static string ToString(byte[] inArray)
{
ArgumentNullException.ThrowIfNull(inArray);
return inArray.Length == 0 ? string.Empty : ToString(new ReadOnlySpan<byte>(inArray), Base32Table);
}

private static byte[] FromString(ReadOnlySpan<char> inputSpan, int[] decodeMap)
{
// calculate the expected output byte array length
// each 8 characters of Base32 results in 5 bytes
var outputLength = (inputSpan.Length * 5) / 8;

// stack allocation for small arrays
Span<byte> outputSpan = stackalloc byte[outputLength];

var bitBuffer = 0;
var bitCount = 0;
var outputIndex = 0;

// process each character in the Base32 input
foreach (var c in inputSpan)
{
// get the index value of the character from the Base32 alphabet
var index = decodeMap[c];
if (index == -1)
{
throw new ArgumentException($"Invalid character '{c}' in Base32 string.");
}

// shift the buffer to the left by 5 bits and add the index value
bitBuffer = (bitBuffer << 5) | index;
bitCount += 5;

// extract 8 bits (1 byte) whenever we have enough bits (>= 8)
if (bitCount < 8)
{
continue;
}

outputSpan[outputIndex++] = (byte)((bitBuffer >> (bitCount - 8)) & MaxByte);
bitCount -= 8;
}

// convert the span to a byte array and return it
return outputSpan.ToArray();
}

private static string ToString(ReadOnlySpan<byte> inArray, char[] base32Table)
{
// calculate the length of the output: Base32 encoding is 8/5 times the size of the input
var outputLength = ((inArray.Length * 8) + 4) / 5;

// output length should be padded to a multiple of 8
var paddingLength = (outputLength + 7) / 8 * 8;

// use stackalloc for small memory allocation
Span<char> outputSpan = stackalloc char[paddingLength];

// declare bit buffer and bit count
var bitBuffer = 0;
var bitCount = 0;
var outputIndex = 0;

// process each byte in inputSpan
foreach (var b in inArray)
{
// shift the buffer left by 8 bits and add the byte value to the buffer
bitBuffer = (bitBuffer << 8) | b;
bitCount += 8;

// while we have 5 or more bits in the buffer, process a Base32 character
while (bitCount >= 5)
{
// extract the top 5 bits from the buffer
var index = (bitBuffer >> (bitCount - 5)) & UnitSeparator;
outputSpan[outputIndex++] = base32Table[index];
bitCount -= 5;
}
}

// if there are leftover bits (less than 5), pad with zeros
if (bitCount > 0)
{
var index = (bitBuffer << (5 - bitCount)) & UnitSeparator;
outputSpan[outputIndex++] = base32Table[index];
}

// add padding '=' to make the result length a multiple of 8
while (outputIndex % 8 != 0)
{
outputSpan[outputIndex++] = '=';
}

// return the result as a string
return new string(outputSpan[..outputIndex]);
}

private static int[] CreateDecodeMap(char[] table)
{
var map = new int[256];

for(var i = map.Length; i-- > 0;)
{
// initialize with -1 (invalid character)
map[i] = -1;
}

for(var i = table.Length; i-- > 0;)
{
map[table[i]] = i;
}

return map;
}
}
Loading

0 comments on commit 0dfd0ac

Please sign in to comment.