diff --git a/.gitignore b/.gitignore
index 940794e..09f0f9e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,6 +22,7 @@ x86/
bld/
[Bb]in/
[Oo]bj/
+[Bb]enchmark[Dd]ot[Nn]et.Artifacts/
[Ll]og/
# Visual Studio 2015 cache/options directory
diff --git a/Base62.Benchmark/Base62.Benchmark.csproj b/Base62.Benchmark/Base62.Benchmark.csproj
new file mode 100644
index 0000000..653f9f1
--- /dev/null
+++ b/Base62.Benchmark/Base62.Benchmark.csproj
@@ -0,0 +1,30 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ validation_data.txt
+ PreserveNewest
+ false
+
+
+
+
diff --git a/Base62.Benchmark/Base62Benchmark.cs b/Base62.Benchmark/Base62Benchmark.cs
new file mode 100644
index 0000000..0712f50
--- /dev/null
+++ b/Base62.Benchmark/Base62Benchmark.cs
@@ -0,0 +1,98 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Order;
+
+namespace Base62.Benchmark;
+
+[Orderer(SummaryOrderPolicy.Method)]
+public class Base62Benchmark
+{
+ private readonly Base62Converter _converter = new();
+ private readonly Base62Converter _invertedConverter = new(Base62Converter.CharacterSet.INVERTED);
+ private string[] _inputs = [];
+ private string[] _encodedValues = [];
+ private byte[] _bytesWithLeadingZeros = [];
+
+ [GlobalSetup]
+ public void GlobalSetup()
+ {
+ var validationPath = Path.Combine(AppContext.BaseDirectory, "validation_data.txt");
+ var inputs = new List();
+ var encoded = new List();
+
+ foreach (var line in File.ReadLines(validationPath))
+ {
+ var parts = line.Split('\t');
+ if (parts.Length < 2)
+ continue;
+
+ inputs.Add(parts[0]);
+ encoded.Add(parts[1]);
+ }
+
+ _inputs = inputs.ToArray();
+ _encodedValues = encoded.ToArray();
+ _bytesWithLeadingZeros = [0, 0, 1, 2, 0, 0];
+ }
+
+ [Benchmark]
+ public void EncodeString_AllValidationSamples()
+ {
+ foreach (var input in _inputs)
+ {
+ _ = _converter.Encode(input);
+ }
+ }
+
+ [Benchmark]
+ public void DecodeString_AllValidationSamples()
+ {
+ foreach (var value in _encodedValues)
+ {
+ _ = _converter.Decode(value);
+ }
+ }
+
+ [Benchmark]
+ public void EncodeDecode_RoundTrip_AllValidationSamples()
+ {
+ foreach (var input in _inputs)
+ {
+ var encoded = _converter.Encode(input);
+ _ = _converter.Decode(encoded);
+ }
+ }
+
+ [Benchmark]
+ public void EncodeBytes_WithLeadingZeros()
+ {
+ _ = _converter.Encode(_bytesWithLeadingZeros);
+ }
+
+ [Benchmark]
+ public void DecodeBytes_WithLeadingZeros()
+ {
+ var encoded = _converter.Encode(_bytesWithLeadingZeros);
+ _ = _converter.Decode(encoded);
+ }
+
+ [Benchmark]
+ public void EncodeString_InvertedCharacterSet()
+ {
+ foreach (var input in _inputs)
+ {
+ _ = _invertedConverter.Encode(input);
+ }
+ }
+
+ [Benchmark]
+ public void EncodeString_ShortAscii()
+ {
+ _ = _converter.Encode("120");
+ }
+
+ [Benchmark]
+ public void EncodeString_Unicode()
+ {
+ _ = _converter.Encode("love爱");
+ }
+}
diff --git a/Base62.Benchmark/BenchmarkConfig.cs b/Base62.Benchmark/BenchmarkConfig.cs
new file mode 100644
index 0000000..cf1439d
--- /dev/null
+++ b/Base62.Benchmark/BenchmarkConfig.cs
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+using BenchmarkDotNet.Columns;
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Diagnosers;
+using BenchmarkDotNet.Environments;
+using BenchmarkDotNet.Exporters;
+using BenchmarkDotNet.Exporters.Csv;
+using BenchmarkDotNet.Exporters.Json;
+using BenchmarkDotNet.Jobs;
+using BenchmarkDotNet.Loggers;
+
+namespace Base62.Benchmark;
+
+public class BenchmarkConfig : ManualConfig
+{
+ public BenchmarkConfig(IEnumerable latestPackageVersionNumbers)
+ {
+ var framework = CoreRuntime.Core10_0;
+
+ AddExporter(MarkdownExporter.GitHub);
+ AddExporter(CsvExporter.Default);
+ AddExporter(JsonExporter.Default);
+
+ AddLogger(ConsoleLogger.Default);
+
+ AddColumnProvider(DefaultColumnProviders.Instance);
+
+ AddDiagnoser(MemoryDiagnoser.Default);
+
+ foreach (var versionNumber in latestPackageVersionNumbers)
+ {
+ var nugetPackageJob = Job.ShortRun
+ .WithRuntime(framework)
+ .WithMsBuildArguments($"/p:NugetPackageVersion={versionNumber}")
+ .WithId($"v{versionNumber}");
+
+ AddJob(nugetPackageJob);
+ }
+
+ var currentProjectJob = Job.ShortRun
+ .WithRuntime(framework)
+ .WithId("current");
+
+ AddJob(currentProjectJob);
+ }
+}
diff --git a/Base62.Benchmark/Nuget/NugetPackage.cs b/Base62.Benchmark/Nuget/NugetPackage.cs
new file mode 100644
index 0000000..ee56ed3
--- /dev/null
+++ b/Base62.Benchmark/Nuget/NugetPackage.cs
@@ -0,0 +1,12 @@
+using System.Text.Json.Serialization;
+
+namespace Base62.Benchmark.Nuget;
+
+public class NugetPackage
+{
+ [JsonPropertyName("id")]
+ public required string Id { get; set; }
+
+ [JsonPropertyName("version")]
+ public required string Version { get; set; }
+}
diff --git a/Base62.Benchmark/Nuget/NugetSearchResultRoot.cs b/Base62.Benchmark/Nuget/NugetSearchResultRoot.cs
new file mode 100644
index 0000000..b640f32
--- /dev/null
+++ b/Base62.Benchmark/Nuget/NugetSearchResultRoot.cs
@@ -0,0 +1,15 @@
+using System.Text.Json.Serialization;
+
+namespace Base62.Benchmark.Nuget;
+
+public class NugetSearchResultRoot
+{
+ [JsonPropertyName("version")]
+ public int Version { get; set; }
+
+ [JsonPropertyName("problems")]
+ public List Problems { get; set; } = [];
+
+ [JsonPropertyName("searchResult")]
+ public List SearchResult { get; set; } = [];
+}
diff --git a/Base62.Benchmark/Nuget/NugetSourceResult.cs b/Base62.Benchmark/Nuget/NugetSourceResult.cs
new file mode 100644
index 0000000..5de9b3b
--- /dev/null
+++ b/Base62.Benchmark/Nuget/NugetSourceResult.cs
@@ -0,0 +1,12 @@
+using System.Text.Json.Serialization;
+
+namespace Base62.Benchmark.Nuget;
+
+public class NugetSourceResult
+{
+ [JsonPropertyName("sourceName")]
+ public required string SourceName { get; set; }
+
+ [JsonPropertyName("packages")]
+ public List Packages { get; set; } = [];
+}
diff --git a/Base62.Benchmark/Program.cs b/Base62.Benchmark/Program.cs
new file mode 100644
index 0000000..99ec082
--- /dev/null
+++ b/Base62.Benchmark/Program.cs
@@ -0,0 +1,47 @@
+using System.Diagnostics;
+using System.Text.Json;
+using Base62.Benchmark.Nuget;
+using BenchmarkDotNet.Running;
+
+namespace Base62.Benchmark;
+
+public static class Program
+{
+ public static async Task Main(string[] args)
+ {
+ var latestVersions = await GetLatestPackageVersionNumbers("Base62", topN: 1);
+
+ _ = BenchmarkRunner.Run(new BenchmarkConfig(latestVersions));
+ }
+
+ private static async Task> GetLatestPackageVersionNumbers(
+ string packageName,
+ int topN = 1,
+ CancellationToken cancellationToken = default)
+ {
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = "dotnet",
+ Arguments = $"package search {packageName} --format json --exact-match",
+ RedirectStandardOutput = true,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ };
+
+ using var process = Process.Start(startInfo)
+ ?? throw new InvalidOperationException("Failed to start dotnet process");
+
+ await process.WaitForExitAsync(cancellationToken);
+
+ var output = await process.StandardOutput.ReadToEndAsync(cancellationToken);
+
+ var packages = JsonSerializer.Deserialize(output);
+
+ var latestPackages = packages?.SearchResult
+ .SelectMany(sr => sr.Packages)
+ .OrderByDescending(p => Version.Parse(p.Version))
+ .Take(topN);
+
+ return latestPackages?.Select(p => p.Version).ToList() ?? [];
+ }
+}
diff --git a/Base62.Tests/Base62Tests.cs b/Base62.Tests/Base62Tests.cs
index 966c6fb..5b7efab 100644
--- a/Base62.Tests/Base62Tests.cs
+++ b/Base62.Tests/Base62Tests.cs
@@ -66,6 +66,106 @@ public void FirstZeroBytesAreConvertedCorrectly()
Assert.Equal(sourceBytes, decoded);
}
+ [Fact]
+ public void EmptyString_EncodesAndDecodesToEmpty()
+ {
+ var converter = new Base62Converter();
+
+ var encoded = converter.Encode(string.Empty);
+ var decoded = converter.Decode(encoded);
+
+ Assert.Equal(string.Empty, encoded);
+ Assert.Equal(string.Empty, decoded);
+ }
+
+ [Fact]
+ public void EmptyByteArray_EncodesAndDecodesToEmpty()
+ {
+ var converter = new Base62Converter();
+ var empty = Array.Empty();
+
+ var encoded = converter.Encode(empty);
+ var decoded = converter.Decode(encoded);
+
+ Assert.Empty(encoded);
+ Assert.Empty(decoded);
+ }
+
+ [Fact]
+ public void AllZeroBytes_ArePreservedOnRoundTrip()
+ {
+ var sourceBytes = new byte[] { 0, 0, 0 };
+ var converter = new Base62Converter();
+ var encodedBytes = converter.Encode(sourceBytes);
+ var decoded = converter.Decode(encodedBytes);
+
+ Assert.Equal(sourceBytes, decoded);
+ Assert.Equal("000", converter.Encode("\0\0\0"));
+ }
+
+ [Fact]
+ public void StringWithLeadingNullBytes_RoundTrips()
+ {
+ var input = "\0\0\0";
+ var converter = new Base62Converter();
+ var encoded = converter.Encode(input);
+ var decoded = converter.Decode(encoded);
+
+ Assert.Equal(input, decoded);
+ Assert.StartsWith("0", encoded);
+ }
+
+ [Fact]
+ public void SingleZeroByte_RoundTrips()
+ {
+ var sourceBytes = new byte[] { 0 };
+ var converter = new Base62Converter();
+
+ Assert.Equal(sourceBytes, converter.Decode(converter.Encode(sourceBytes)));
+ }
+
+ [Fact]
+ public void Decode_WithCharacterOutsideCharset_DoesNotThrow()
+ {
+ var converter = new Base62Converter();
+ var validEncoded = converter.Encode("120");
+
+ var decoded = converter.Decode(validEncoded + "@");
+
+ Assert.NotEqual("120", decoded);
+ }
+
+ [Fact]
+ public void Decode_WithCharacterAboveAsciiRange_DoesNotThrow()
+ {
+ var converter = new Base62Converter();
+ var validEncoded = converter.Encode("120");
+
+ var decoded = converter.Decode(validEncoded + "\u0080");
+
+ Assert.NotEqual("120", decoded);
+ }
+
+ [Fact]
+ public void InvertedCharacterSet_AllZeroBytes_RoundTrip()
+ {
+ var sourceBytes = new byte[] { 0, 0 };
+ var converter = new Base62Converter(Base62Converter.CharacterSet.INVERTED);
+
+ Assert.Equal(sourceBytes, converter.Decode(converter.Encode(sourceBytes)));
+ }
+
+ [Fact]
+ public void DefaultConstructor_MatchesExplicitDefaultCharacterSet()
+ {
+ var input = "120";
+ var implicitConverter = new Base62Converter();
+ var explicitConverter = new Base62Converter(Base62Converter.CharacterSet.DEFAULT);
+
+ Assert.Equal(explicitConverter.Encode(input), implicitConverter.Encode(input));
+ Assert.Equal(explicitConverter.Decode("DWjo"), implicitConverter.Decode("DWjo"));
+ }
+
public static IEnumerable