-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Marthijn van den Heuvel
committed
Oct 3, 2024
1 parent
f166739
commit 0dfd0ac
Showing
9 changed files
with
415 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -396,3 +396,4 @@ FodyWeavers.xsd | |
|
||
# JetBrains Rider | ||
*.sln.iml | ||
.idea/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
28
src/Sidio.Text.Base32.Tests/Sidio.Text.Base32.Tests.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.