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
11 changes: 11 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# These directories hold test fixtures captured verbatim from reference
# implementations - raw encoded/decoded bytes, not human-authored text. Some of those
# byte sequences happen to contain a lone LF (0x0A); without this, Git's line-ending
# conversion (autocrlf) can silently rewrite it to CRLF on checkout, corrupting the
# fixture without the tracked blob ever changing. Disable text conversion for them.
src/CyberDot.Encoding.Base2048.Tests/test-data/** -text
src/CyberDot.Encoding.Base65536.Tests/Bad/** -text
src/CyberDot.Encoding.Base65536.Tests/DoubledBytes/** -text
src/CyberDot.Encoding.Base65536.Tests/IgnoreGarbage/** -text
src/CyberDot.Encoding.Base65536.Tests/Pairs/** -text
src/CyberDot.Encoding.Base65536.Tests/SingleBytes/** -text
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ jobs:
run: |
for project in \
src/CyberDot.Encoding.Base65536/CyberDot.Encoding.Base65536.csproj \
src/CyberDot.Encoding.Base2048/CyberDot.Encoding.Base2048.csproj; do
src/CyberDot.Encoding.Base2048/CyberDot.Encoding.Base2048.csproj \
src/CyberDot.Encoding.Base84/CyberDot.Encoding.Base84.csproj; do
dotnet pack "$project" --configuration Debug --no-build --output "${{ runner.temp }}/nupkg-verify"
done
22 changes: 22 additions & 0 deletions .github/workflows/release-base84.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Release CyberDot.Encoding.Base84

# Manually-triggered release for the Base84 package. See _release-package.yml for
# what this actually does.

on:
workflow_dispatch:
inputs:
version:
description: 'Release version override (e.g. 1.2.0). Leave empty to use the version in the csproj.'
required: false
type: string

jobs:
release:
permissions:
contents: write
uses: ./.github/workflows/_release-package.yml
with:
project-path: src/CyberDot.Encoding.Base84/CyberDot.Encoding.Base84.csproj
tag-prefix: base84
version: ${{ inputs.version }}
28 changes: 27 additions & 1 deletion CyberDot.Encoding.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.9.12105.275 stable
VisualStudioVersion = 18.9.12105.275
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CyberDot.Encoding.Base65536", "src\CyberDot.Encoding.Base65536\CyberDot.Encoding.Base65536.csproj", "{C021E149-EB05-4EF0-9A4C-78A0808AC413}"
EndProject
Expand All @@ -11,6 +11,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CyberDot.Encoding.Base2048"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CyberDot.Encoding.Base2048.Tests", "src\CyberDot.Encoding.Base2048.Tests\CyberDot.Encoding.Base2048.Tests.csproj", "{287EE26D-CAE7-4AA3-93C5-4A6B2C8A9D05}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CyberDot.Encoding.Base84", "src\CyberDot.Encoding.Base84\CyberDot.Encoding.Base84.csproj", "{9551EC7C-8F4A-462A-AABC-50D1FD1E60B5}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Base2048", "Base2048", "{7F2B409E-7822-462C-8152-0DE5BEE42CAD}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Base65536", "Base65536", "{EF960794-148F-4CC2-A1B6-4001FA1A9095}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CyberDot.Encoding.Base84.Tests", "src\CyberDot.Encoding.Base84.Tests\CyberDot.Encoding.Base84.Tests.csproj", "{3762FA86-26D7-4037-B7CF-E5CA94F03D8B}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Base84", "Base84", "{4C6BEADC-6B8C-425C-9EC4-559597A7915C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -33,10 +43,26 @@ Global
{287EE26D-CAE7-4AA3-93C5-4A6B2C8A9D05}.Debug|Any CPU.Build.0 = Debug|Any CPU
{287EE26D-CAE7-4AA3-93C5-4A6B2C8A9D05}.Release|Any CPU.ActiveCfg = Release|Any CPU
{287EE26D-CAE7-4AA3-93C5-4A6B2C8A9D05}.Release|Any CPU.Build.0 = Release|Any CPU
{9551EC7C-8F4A-462A-AABC-50D1FD1E60B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9551EC7C-8F4A-462A-AABC-50D1FD1E60B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9551EC7C-8F4A-462A-AABC-50D1FD1E60B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9551EC7C-8F4A-462A-AABC-50D1FD1E60B5}.Release|Any CPU.Build.0 = Release|Any CPU
{3762FA86-26D7-4037-B7CF-E5CA94F03D8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3762FA86-26D7-4037-B7CF-E5CA94F03D8B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3762FA86-26D7-4037-B7CF-E5CA94F03D8B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3762FA86-26D7-4037-B7CF-E5CA94F03D8B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{C021E149-EB05-4EF0-9A4C-78A0808AC413} = {EF960794-148F-4CC2-A1B6-4001FA1A9095}
{F4232660-0EA0-432E-8713-F4612E88AA80} = {EF960794-148F-4CC2-A1B6-4001FA1A9095}
{C7494689-9E71-4F15-883E-027E244E3E6D} = {7F2B409E-7822-462C-8152-0DE5BEE42CAD}
{287EE26D-CAE7-4AA3-93C5-4A6B2C8A9D05} = {7F2B409E-7822-462C-8152-0DE5BEE42CAD}
{9551EC7C-8F4A-462A-AABC-50D1FD1E60B5} = {4C6BEADC-6B8C-425C-9EC4-559597A7915C}
{3762FA86-26D7-4037-B7CF-E5CA94F03D8B} = {4C6BEADC-6B8C-425C-9EC4-559597A7915C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {9D040A49-47AE-4503-B1C2-7D3E82AF66AB}
EndGlobalSection
Expand Down
218 changes: 218 additions & 0 deletions src/CyberDot.Encoding.Base84.Tests/Base84Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
using AwesomeAssertions;

namespace CyberDot.Encoding.Base84.Tests
{
public class Base84Tests
{
// Vectors below are taken from the reference Zig implementation's test suite.
[Fact]
public void Should_encode_and_decode_known_vectors()
{
AssertVector("", "");
AssertVector("M", "]A");
AssertVector("Ma", "tsD");
AssertVector("Man", "pRRM");
AssertVector("Man ", ";dA^K");
AssertVector("Hello, World!", "s@Etk'#Qedrxz+hhA");
AssertVector("The quick brown fox jumps over the lazy dog.",
"wBB]KdJ}phb-#tmbNB^K!!J_K^3h=l_pj[nJXJLnkB3kkhkT_KM}r1P");
}

[Fact]
public void Should_encode_and_decode_runs_of_zero_bytes()
{
AssertVector(new byte[] { 0x00 }, "AA");
AssertVector(new byte[] { 0x00, 0x00 }, "AAA");
AssertVector(new byte[] { 0x00, 0x00, 0x00 }, "AAAA");
AssertVector(new byte[] { 0x00, 0x00, 0x00, 0x00 }, "AAAAA");
AssertVector(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00 }, "AAAAAAA");
}

[Fact]
public void Should_encode_and_decode_runs_of_ff_bytes()
{
AssertVector(new byte[] { 0xff }, "DD");
AssertVector(new byte[] { 0xff, 0xff }, "PYJ");
AssertVector(new byte[] { 0xff, 0xff, 0xff }, "#8Zc");
AssertVector(new byte[] { 0xff, 0xff, 0xff, 0xff }, "rxQLrB");
AssertVector(new byte[] { 0xff, 0xff, 0xff, 0xff, 0xff }, "rxQLrHG");
}

[Fact]
public void Should_encode_and_decode_mixed_bytes()
{
AssertVector(new byte[] { 0x00, 0xe0, 0xff, 0x01 }, "AYy4A");
AssertVector(
new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f },
"8%LBB${'eC(No8D-dMGF");
}

[Theory]
[InlineData("AA/")]
[InlineData("A A")]
[InlineData("AA\x00")]
[InlineData("AA\xff")]
[InlineData("AA<")]
public void Should_throw_on_unrecognised_character(string input)
{
var act = () => Base84.Decode(input);

act.Should().Throw<ArgumentException>();
}

[Theory]
[InlineData("A")]
[InlineData("AE")]
[InlineData("AAK")]
[InlineData("AAAd")]
[InlineData("rxQLr")]
[InlineData("oi'-o")]
[InlineData("AAAAAA")]
[InlineData("rxQLrrxQLrrxQLrrxQLrrxQLrrxQLrrxQLrAA")]
public void Should_throw_on_invalid_padding(string input)
{
var act = () => Base84.Decode(input);

act.Should().Throw<ArgumentException>();
}

[Fact]
public void Should_throw_on_null_input()
{
var encodeAct = () => Base84.Encode(null);
var decodeAct = () => Base84.Decode(null);

encodeAct.Should().Throw<ArgumentNullException>();
decodeAct.Should().Throw<ArgumentNullException>();
}

[Fact]
public void Should_round_trip_all_single_bytes()
{
for (var b = 0; b <= 255; b++)
{
var data = new[] { (byte)b };

var encoded = Base84.Encode(data);
Base84.Decode(encoded).Should().BeEquivalentTo(data);
}
}

[Fact]
public void Should_round_trip_all_byte_pairs()
{
for (var a = 0; a <= 255; a++)
{
for (var b = 0; b <= 255; b++)
{
var data = new[] { (byte)a, (byte)b };

var encoded = Base84.Encode(data);
Base84.Decode(encoded).Should().BeEquivalentTo(data);
}
}
}

[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
[InlineData(5)]
[InlineData(6)]
[InlineData(17)]
[InlineData(31)]
[InlineData(32)]
[InlineData(33)]
[InlineData(256)]
public void Should_round_trip_random_data(int length)
{
var data = new byte[length];
new Random(length).NextBytes(data);

var encoded = Base84.Encode(data);
Base84.Decode(encoded).Should().BeEquivalentTo(data);
}

[Fact]
public void Should_round_trip_many_random_lengths()
{
var random = new Random(12345);
var buffer = new byte[256];

for (var i = 0; i < 5000; i++)
{
var len = random.Next(0, buffer.Length + 1);
random.NextBytes(buffer);
var data = new byte[len];
Array.Copy(buffer, data, len);

var encoded = Base84.Encode(data);
Base84.Decode(encoded).Should().BeEquivalentTo(data);
}
}

private const string AlphabetChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&'()+,-;=@[]^_`{}~";

[Fact]
public void Should_reject_altered_encoded_data_or_accept_it_and_round_trip()
{
// Any mutation of a validly-encoded string must either be rejected as invalid
// padding, or - if it happens to still be well-formed - round trip cleanly
// through a fresh encode.
var random = new Random(999);
var buffer = new byte[32];

for (var i = 0; i < 5000; i++)
{
var len = random.Next(0, buffer.Length + 1);
random.NextBytes(buffer);
var data = new byte[len];
Array.Copy(buffer, data, len);

var encodedChars = Base84.Encode(data).ToCharArray();
var alphabetChar = AlphabetChars[random.Next(AlphabetChars.Length)];

switch (random.Next(3))
{
case 0 when encodedChars.Length > 0:
encodedChars[random.Next(encodedChars.Length)] = alphabetChar;
break;
case 1:
Array.Resize(ref encodedChars, encodedChars.Length + 1);
encodedChars[encodedChars.Length - 1] = alphabetChar;
break;
case 2 when encodedChars.Length > 0:
Array.Resize(ref encodedChars, encodedChars.Length - 1);
break;
}

var altered = new string(encodedChars);

byte[] decoded;
try
{
decoded = Base84.Decode(altered);
}
catch (ArgumentException)
{
continue;
}

Base84.Encode(decoded).Should().Be(altered);
}
}

private static void AssertVector(string decoded, string encoded)
{
AssertVector(System.Text.Encoding.UTF8.GetBytes(decoded), encoded);
}

private static void AssertVector(byte[] decoded, string encoded)
{
Base84.Encode(decoded).Should().Be(encoded);
Base84.Decode(encoded).Should().BeEquivalentTo(decoded);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AwesomeAssertions" Version="9.5.0" />
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
</ItemGroup>

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

<ItemGroup>
<ProjectReference Include="..\CyberDot.Encoding.Base84\CyberDot.Encoding.Base84.csproj" />
</ItemGroup>

</Project>
Loading
Loading