Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Spotflow Base62.NET

An extremely fast, dependency-free Base62 encoder and decoder for .NET.

NuGet CI status License

Features

  • Extremely fast (see Benchmarks) encoding and decoding compared to other libraries thanks to:
    • Block-based encoding algorithm that runs in $O(n)$ time instead of the usual $O(n^2)$ time.
    • Span-based character and UTF-8 APIs.
    • Allocation-free destination-based encoding and decoding.
    • A single exact result allocation for the string and array convenience methods.
  • Streaming APIs.
  • No runtime dependencies.
  • Supports .NET 8, .NET 9, and .NET 10.

Why Base62?

Base62 is well suited for user-facing identifiers, links, and tokens:

  • No special characters: Base62 is entirely alphanumeric. It is naturally URL-safe. Double-clicking selects the entire string.
  • Compact: Base62 represents binary data much more efficiently than hexadecimal while remaining entirely alphanumeric. For example, eight bytes use 11 Base62 characters instead of 16 hexadecimal characters.

Installation

dotnet add package Spotflow.Base62

Quick Start

using Spotflow.Base62;

ReadOnlySpan<byte> data = stackalloc byte[] { 1, 2, 3, 4, 5 };
string encoded = Base62.EncodeToString(data);

byte[] decoded = Base62.DecodeFromChars(encoded);

Use destination-based APIs to avoid allocations:

ReadOnlySpan<byte> data = stackalloc byte[] { 1, 2, 3, 4, 5 };

Span<char> encoded = stackalloc char[Base62.GetEncodedLength(data.Length)];
int charsWritten = Base62.EncodeToChars(data, encoded);

Span<byte> decoded = stackalloc byte[Base62.GetDecodedLength(charsWritten)];
int bytesWritten = Base62.DecodeFromChars(encoded, decoded);

Equivalent EncodeToUtf8 and DecodeFromUtf8 overloads operate on ASCII-compatible UTF-8 bytes. Try overloads avoid exceptions for malformed data and small destinations. Advanced overloads return OperationStatus together with consumed and written counts for blockwise processing.

Encoding a String

Convert the string to bytes using an appropriate text encoding before encoding it as Base62:

using System.Text;
using Spotflow.Base62;

const string text = "Hello, world!";

Span<byte> utf8 = stackalloc byte[Encoding.UTF8.GetByteCount(text)];
Encoding.UTF8.GetBytes(text, utf8);

string encoded = Base62.EncodeToString(utf8);

Streaming

Use the OperationStatus overloads to process data without loading the entire input into memory. Preserve any unconsumed bytes between reads so that Base62 blocks remain contiguous:

using System.Buffers;
using Spotflow.Base62;

await using FileStream source = File.OpenRead("input.bin");
await using FileStream destination = File.Create("output.base62");

const int BufferSize = 8 * 1024;
byte[] inputBuffer = new byte[BufferSize];
byte[] outputBuffer = new byte[Base62.GetEncodedLength(BufferSize)];
int buffered = 0;

while (true)
{
    int bytesRead = await source.ReadAsync(inputBuffer.AsMemory(buffered));
    bool isFinalBlock = bytesRead == 0;
    int available = buffered + bytesRead;

    OperationStatus status = Base62.EncodeToUtf8(
        inputBuffer.AsSpan(0, available),
        outputBuffer,
        out int bytesConsumed,
        out int bytesWritten,
        isFinalBlock);

    await destination.WriteAsync(outputBuffer.AsMemory(0, bytesWritten));

    inputBuffer.AsSpan(bytesConsumed, available - bytesConsumed)
        .CopyTo(inputBuffer);
    buffered = available - bytesConsumed;

    if (isFinalBlock && status == OperationStatus.Done)
    {
        break;
    }

    if (status is not (OperationStatus.Done or OperationStatus.NeedMoreData))
    {
        throw new InvalidOperationException($"Unexpected status: {status}");
    }
}

The same pattern works for decoding with Base62.DecodeFromUtf8; decoded input is processed in 11-character blocks instead of 8-byte blocks.

How It Works

Important

Base62 has no single standard binary encoding. This library guarantees a single deterministic encoding for any input data and a single deterministic decoding for any Base62 string generated by this library. It is not guaranteed to decode Base62 strings generated by other libraries as they may use different alphabets, block sizes, etc.

The alphabet is 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz. Data is split into blocks of up to eight bytes. Each block is interpreted as a little-endian unsigned integer, and Base62 digits are written least-significant first. Full blocks contain 11 characters; the final partial block has a dynamic width based on the number of input bytes. The encoding is deterministic and preserves leading and trailing zero bytes.

Examples:

Input (hex) Encoded value
01 10
FFFF 13H
FFFFFFFFFFFFFFFF FYHA61aHgyL
010203040506070809 zipgg0YFjg090

Error Handling

Method Condition Exception
Base62.DecodeFromChars, Base62.DecodeFromUtf8 The input contains an invalid character, block length, or block value FormatException
Base62.EncodeToChars, Base62.EncodeToUtf8, destination-based decode methods The output buffer is too small ArgumentException
Base62.GetDecodedLength The encoded length cannot represent valid data FormatException
Length methods The supplied length is negative ArgumentOutOfRangeException

Base62.TryGetDecodedLength returns false for an encoded length that cannot represent valid data. The other Try methods return false instead of throwing for malformed data or insufficient destination space. On failure, their written count reports the successfully processed prefix and the destination may be partially modified. The OperationStatus overloads distinguish InvalidData, DestinationTooSmall, and NeedMoreData.

Benchmarks

Run the encoding and decoding benchmarks against a supported framework:

dotnet run --project benchmarks/Spotflow.Base62.Benchmarks -c Release -f net10.0

The benchmarks compare Spotflow.Base62 with SimpleBase and ghost1face/base62 using each library's binary API. Spotflow reuses destination buffers for encoding and decoding. SimpleBase returns a string when encoding, while ghost1face returns Base62 digit values as a byte[], so allocation results should be interpreted with those API differences in mind. Destination buffers are reused whenever an API supports them.

Results

Spotflow.Base62 is consistently faster and offers zero allocations for destination-based APIs. The difference is most pronounced for larger inputs thanks to the linear-time block-based algorithm (e.g. 4,000x faster for 4 KiB inputs).

Each result shows mean time / ratio to Spotflow / allocated memory.

Encoding

Input Spotflow.Base62 SimpleBase ghost1face/base62
64 B 96.33 ns / 1.00x / 0 B 5.664 us / 58.81x / 200 B 7.566 us / 78.55x / 400 B
128 B 194.55 ns / 1.00x / 0 B 28.774 us / 147.90x / 368 B 31.418 us / 161.50x / 704 B
1024 B 1.652 us / 1.00x / 0 B 1.840 ms / 1,118.81x / 5,560 B 2.353 ms / 1,430.51x / 4,904 B
4096 B 7.741 us / 1.00x / 0 B 32.646 ms / 4,218.55x / 22,072 B 34.194 ms / 4,418.60x / 19,308 B

Decoding

Input Spotflow.Base62 SimpleBase ghost1face/base62
64 B 77.76 ns / 1.00x / 0 B 3.919 us / 50.41x / 0 B 7.602 us / 97.79x / 424 B
128 B 148.73 ns / 1.00x / 0 B 12.939 us / 87.01x / 0 B 32.664 us / 219.66x / 752 B
1024 B 908.78 ns / 1.00x / 0 B 676.44 us / 744.92x / 0 B 1.693 ms / 1,864.66x / 5,256 B
4096 B 3.643 us / 1.00x / 0 B 10.591 ms / 2,908.78x / 0 B 27.814 ms / 7,638.59x / 20,717 B

Maintainers

Contributing

See the contribution guidelines before opening a pull request. Security issues should be reported according to the security policy.

License

This project is licensed under the MIT license.

About

High-performance base62 encoder and decoder for .NET

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

6 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages