Base64, Base32 и Base16 кодировки в .NET

BaseN кодировка

BaseN кодировки используются для кодирования двоичных данных в текстовый вид. Где N это размер текстового алфавита используемого для кодирования. Применяются BaseN кодировки для "подготовки данных" к передаче по текстовым протоколам (HTTP, SMTP) или нецифровым каналам (напр. печать на бумаге).

Самый популярный вариант такой кодировки это Base64. Сравниться по частоте применения может только Base16 известный как hexadecimal или просто hex. Инструменты для работы с ними есть в .NET еще с первых версий, но они не очень удобные.

Что есть в .NET Framework (Core)

System.Security.Cryptography

ToBase64Transform/FromBase64Transform

Пара классов для преобразования в/из Base64. С помощью вспомогательного класса CryptoStream его можно использовать для потоковой обработки Stream.

using System.Security.Cryptography;

var base64Transform = new ToBase64Transform();
var output = new MemoryStream();
var base64Encoder = new CryptoStream(output, base64Transform, CryptoStreamMode.Write);

base64Encoder.Write(new byte[] { 122 }, 0, 1);
base64Encoder.Close();
// output.ToArray() -> byte[4] { 101, 103, 61, 61 } aka "eg=="

System.Convert

ToBase64String/FromBase64String

Пара функции для преобразования массива байтов в Base64 строку и обратно.

using System;

var bytes = System.Convert.FromBase64String("eg==")
// bytes -> 122

System.BitConverter

BitConverter.ToString

Функция преобразования массива байтов в Base16 (hexadecimal) строку, где каждый байт разделен дефизом. Пример: 0A-C0-D3

using System;

var hexString = BitConverter.ToString(new byte[] { 122, 122 });
// hexString -> "7A-7A"

System.Runtime.Remoting.Metadata.W3cXsd2001

SoapHexBinary (Framework-only)

Класс позволяющий преобразовывать массив байтов в Base16 (hexadecimal) строку и обратно.

using System.Runtime.Remoting.Metadata.W3cXsd2001;

var result = SoapHexBinary.Parse("A012");
// result.Value -> byte[];

System.Buffers.Text

Base64 (Core or System.Memory package)

Удобный класс для работы Base64 в режиме потока. Можно толкать Span'ы с байтами и получать результат в свой Span байтов.

Что предлагает Nuget

Multiformats.Base - Base2, 8, 16, 32, 58, 64 (+variants), MIT

Удобная библиотека с очень большим набором вариантов BaseN кодировок. Простое API и есть тесты. Из недостатков: он работает только с массивами.

SimpleBase - Base16, 32, 58, 85 (+variants), streaming, span, Apache-2

Библитека с большим набором BaseN кодировок. Есть поддержка .NET Core и работа со Span'ами. Простое API, есть тесты.

Пакет deniszykov.BaseN

Конечно у всех этих классов и библиотек есть фатальный недостаток.

Помимо этого, большинство бросило поддержку .NET Framework 4.5.

С точки зрения фичей, только класс Base64 умеет в Span<byte> to Span<byte> преобразования (везде это char[] <-> byte[] преобразования). А это частый кейс, когда данные приходят из сети. Эти данные не могут быть "интерпретированы" из byte в char по тому, что char занимает 2 байта в памяти. Т.е. для их преобразования, вышеописанными библиотеками, требуется скопировать их во временный буфер вдвое большего размера чем исходные данные.

В этом пакете функции кодирования/декодирования принимают разные типы входящих данных:

Все это лежит внутри двух классов BaseNDecoder/BaseNEncoder.

Поддерживаются следующие словари кодирования:

using deniszykov.BaseN;

var bytes = Base64Convert.ToBytes("eg==");
// bytes[0] -> 122

Более богатый набор предлагает класс BaseNEncoding который реализует System.Text.Encoding.

using deniszykov.BaseN;

var count = BaseNEncoding.Base64Url.GetByteCount("eg=="); // -> 1
var bytes = BaseNEncoding.Base64Url.GetBytes("eg=="); // -> 122

var baseNEncoder = new BaseNEncoder(BaseNAlphabet.Base64Alphabet);
var input = "eg==";
var output = new byte[1024];
baseNEncoder.Convert(input, 0, 4, output, 0, 1, true, out var charsUsed, out var bytesUsed, out var completed);
// output[0] -> 122

Другие примеры "простого" использования лежат тут. Можно использовать свой словарь. Сложных примеров не будет, те кому это надо могут найти их в тестах :)

Install-Package deniszykov.BaseN

@shai_hulud
29.01.2021 23:16 UTC
Первоисточник

Комментарии

@
29.01.2021 20:29 UTC
0
НЛО прилетело и опубликовало эту надпись здесь
@shai_hulud
29.01.2021 20:40 UTC
0
Если Base64, то самый простой способ это класс ToBase64Transform и CryptoStream.
using System.Security.Cryptography;

var base64Transform = new ToBase64Transform();
using var output = new MemoryStream();
using var base64Encoder = new CryptoStream(output, base64Transform, CryptoStreamMode.Write);
using var gZipCompressor = new GZipStream(base64Encoder, CompressionMode.Compress);

gZipCompressor.Write(new byte[] { 255}, 0, 1);


Если другие, то для стримов удобнее SimpleBase и класс StreamHelper.

Для чтения из сокетов и System.IO.Pipelines уже удобнее можно использовать мою либу т.к. там есть подходящий BaseNEncoder.Convert() принимающие Span{byte}.

Хотя если реализовать ICryptoTransform то можно сесть на паравоз CryptoStream и получить поддержку стримов. Я скорее всего скоро это сделаю у себя в либе в ближайшее время.
29.01.2021 20:46 UTC
0
НЛО прилетело и опубликовало эту надпись здесь
29.01.2021 21:07 UTC
0
Типа такого?

public static Stream BaseNEncode(this Stream stream, BaseNAlphabet baseNAlphabet)
{
  return new CryptoStream(stream, new BaseNEncoder(baseNAlphabet), CryptoStreamMode.Write);
}

var output = new MemoryStream();
var writeStream = output
  .BaseNEncode(BaseNAlphabet.Base64Alphabet)
  .GZip();

writeStream.Write(...);


Естественно расширение GZip() я не могу поместить в свою библиотеку т.к. это вне зоны ее ответственности.
30.01.2021 14:00 UTC
0
Добавил наследование ICryptoTransform пару расширений:
stream.BaseNEncode() // в полученный стрим можно писать
stream.BaseNDecode() // из полученного стрима можно читать


Тест с примером.
@DistortNeo
29.01.2021 22:09 UTC
0

Ещё бы производительность всего этого дела сравнить. Например, в System.Buffers.Text.Base64 используются AVX2/SSE интринсики, а это должно давать нехилый выигрыш в производительности.


Я вот взял код отсюда:
https://github.com/deniszykov/BaseN/tree/master/src/deniszykov.BaseN.Benchmark
добавил бенчмарк, использующий System.Buffers.Text.Base64, и получил следующее (NET 5.0):


|                          Method |       Mean |     Error |    StdDev |     Median | Ratio | Gen 0 | Gen 1 | Gen 2 | Allocated |
|-------------------------------- |-----------:|----------:|----------:|-----------:|------:|------:|------:|------:|----------:|
|    Base64_Encode_ByteArray_Char | 109.008 ms | 0.1626 ms | 0.1442 ms | 108.978 ms |  1.00 |     - |     - |     - |   8.12 KB |
|    Base64_Encode_ByteArray_Byte | 111.295 ms | 0.4776 ms | 0.4234 ms | 111.190 ms |  1.02 |     - |     - |     - |   4.12 KB |
| SysBase64_Encode_ByteArray_Byte |   3.349 ms | 0.0010 ms | 0.0008 ms |   3.349 ms |  0.03 |     - |     - |     - |   4.06 KB |
|          Base64_Encode_Ptr_Char | 108.706 ms | 0.6456 ms | 0.5723 ms | 108.542 ms |  1.00 |     - |     - |     - |   8.12 KB |
|          Base64_Encode_Ptr_Byte | 103.263 ms | 0.2176 ms | 0.2036 ms | 103.219 ms |  0.95 |     - |     - |     - |   5.86 KB |
|         Base64_Encode_Span_Char | 103.621 ms | 0.0388 ms | 0.0363 ms | 103.622 ms |  0.95 |     - |     - |     - |   8.12 KB |
|         Base64_Encode_Span_Byte | 103.901 ms | 0.8819 ms | 0.8249 ms | 104.639 ms |  0.95 |     - |     - |     - |   4.12 KB |

Разница в 30 раз!
И ещё это при том, что на моём процессоре нет AVX2.

@radium
30.01.2021 09:14 UTC
0

Вы использовали EncodeToUtf8 или EncodeToUtf8InPlace? Можете показать код этого теста?

30.01.2021 09:20 UTC
+2
[Benchmark]
public void SysBase64_Encode_ByteArray_Byte()
{
    var encoder = new BaseNDecoder(BaseNAlphabet.Base64Alphabet);
    var output = new byte[4 * 1024];
    for (var i = 0; i < data.Length;)
    {
        Base64.EncodeToUtf8(new ReadOnlySpan<byte>(data, i, data.Length - i), output, out var bytesConsumed, out var bytesWritten);
        i += bytesConsumed;
    }
}
@radium
30.01.2021 09:01 UTC
0

Также полезно сравнить производительность в разрезе кодирования и декодирования утилитарными методами.


Encode (gist)


|                                 Method |      Mean |    Error |   StdDev | Ratio | RatioSD |
|--------------------------------------- |----------:|---------:|---------:|------:|--------:|
|          System_Convert_ToBase64String |  51.32 ms | 0.549 ms | 0.514 ms |  1.00 |    0.00 |
|           BaseN_Base64Convert_ToString | 164.16 ms | 1.372 ms | 1.283 ms |  3.20 |    0.04 |
|           BaseN_Base32Convert_ToString | 185.88 ms | 1.935 ms | 1.810 ms |  3.62 |    0.04 |
| Wiry_Base32Encoding_Standard_GetString | 108.34 ms | 1.035 ms | 0.968 ms |  2.11 |    0.03 |
|       SimpleBase_Base32_Rfc4648_Encode | 131.39 ms | 1.979 ms | 1.851 ms |  2.56 |    0.05 |
|                  Albireo_Base32_Encode | 184.59 ms | 1.801 ms | 1.685 ms |  3.60 |    0.05 |

Decode (gist)


|                               Method |        Mean |     Error |   StdDev | Ratio | RatioSD |
|------------------------------------- |------------:|----------:|---------:|------:|--------:|
|      System_Convert_FromBase64String |    86.81 ms |  0.619 ms | 0.579 ms |  1.00 |    0.00 |
|          BaseN_Base64Convert_ToBytes |   180.61 ms |  1.902 ms | 1.588 ms |  2.08 |    0.02 |
|          BaseN_Base32Convert_ToBytes |   200.32 ms |  1.687 ms | 1.408 ms |  2.30 |    0.02 |
| Wiry_Base32Encoding_Standard_ToBytes |    56.67 ms |  0.510 ms | 0.477 ms |  0.65 |    0.01 |
|     SimpleBase_Base32_Rfc4648_Decode |   104.86 ms |  1.144 ms | 1.070 ms |  1.21 |    0.02 |
|                Albireo_Base32_Decode | 1,485.12 ms | 10.363 ms | 9.694 ms | 17.11 |    0.17 |

Полный лог: gist

@DistortNeo
30.01.2021 09:21 UTC
+1

Добавьте System.Buffers.Text.Base64.EncodeToUtf8 — удивитесь.

30.01.2021 11:06 UTC
0

О, да.


Код.


Intel Core i5-7440HQ CPU 2.80GHz (Kaby Lake), 1 CPU, 4 logical and 4 physical cores
.NET Core SDK=5.0.101
  [Host]     : .NET Core 5.0.1 (CoreCLR 5.0.120.57516, CoreFX 5.0.120.57516), X64 RyuJIT
  DefaultJob : .NET Core 5.0.1 (CoreCLR 5.0.120.57516, CoreFX 5.0.120.57516), X64 RyuJIT

|                                 Method |       Mean |     Error |    StdDev | Ratio | RatioSD |
|--------------------------------------- |-----------:|----------:|----------:|------:|--------:|
|          System_Convert_ToBase64String |  51.039 ms | 0.4572 ms | 0.4276 ms |  1.00 |    0.00 |
|           BaseN_Base64Convert_ToString | 162.071 ms | 0.9066 ms | 0.7571 ms |  3.17 |    0.03 |
|             System_Base64_EncodeToUtf8 |   2.416 ms | 0.0266 ms | 0.0236 ms |  0.05 |    0.00 |
|                              NaiveLoad |   6.463 ms | 0.0337 ms | 0.0316 ms |  0.13 |    0.00 |
|                                AvxLoad |   1.649 ms | 0.0187 ms | 0.0175 ms |  0.03 |    0.00 |
30.01.2021 14:08 UTC
-1
Ссылка на код неправильная.
Так можно сравнивать что-угодно с чем угодно.

а) не проверять корректность теста, что на выходе правильные данные
б) сравнивать теплое с мягким
@shai_hulud
30.01.2021 13:53 UTC
-2
Учел критику/пожелания и добавил немного оптимизаций. Бенчмарк в репозитории.

|                                 Method |      Mean | Allocated |
|--------------------------------------- |----------:|----------:|
|          System_Convert_ToBase64String |  48.80 ms |  53.33 MB |
|           System_Memory_Base64ToString |  15.29 ms |  26.67 MB |
|             BaseN_BaseNDecoder_Convert |  49.13 ms |  26.67 MB |
|           BaseN_Base64Convert_ToString |  59.41 ms |  53.33 MB |
|           BaseN_Base32Convert_ToString |  70.00 ms |     64 MB |
| Wiry_Base32Encoding_Standard_GetString | 100.28 ms |    128 MB |
|       SimpleBase_Base32_Rfc4648_Encode | 102.78 ms |     64 MB |
|                  Albireo_Base32_Encode | 152.63 ms |    128 MB |


Сравнивать с System.Buffers.Text.Base64 некорректно т.к. любой general алгоритм будет проигрывать специализированному. У меня принимается словарь на вход, даже пользовательский, а в System.Buffers.Text.Base64 словарь зашит в алгоритм. Плюс там ввернуто много SSE и AVX, которые будет очень сложно навернуть на общий алгоритм.
30.01.2021 14:28 UTC
0
Сравнивать с System.Buffers.Text.Base64 некорректно т.к. любой general алгоритм будет проигрывать специализированному.

Не вижу никаких проблем вызывать оптимизированный алгоритм при выполнении определённых условий.


У меня принимается словарь на вход, даже пользовательский, а в System.Buffers.Text.Base64 словарь зашит в алгоритм.

И часто ли случается ситуация, когда в качестве алфавита используется что-то отличное от RFC4648? Как мне кажется, это настолько редкая ситуация, что нет смысла из-за неё снижать производительность алгоритма на полтора порядка.

30.01.2021 14:44 UTC
0
Не вижу никаких проблем вызывать оптимизированный алгоритм при выполнении определённых условий

Полностью согласен. Даже более, я сам использую System.Buffers.Text.Base64 везде, где его можно применить.
Но сравнивать производительность, и говорить «почему ваш общий алгоритм медленнее специализированного» не очень корректно.
И часто ли случается ситуация, когда в качестве алфавита используется что-то отличное от RFC4648?

Да, есть богомерзкий Base64 URL который всё чаще торчит из разных Web API.
снижать производительность алгоритма на полтора порядка

Сейчас в два с половиной раза (табличка вверху). Полтора порядка это 15 раз.
30.01.2021 15:02 UTC
0
Бенчмарк в репозитории.

Не нашёл изменений в репозитории.


Сейчас в два с половиной раза (табличка вверху). Полтора порядка это 15 раз.

Потому что при использовании System.Buffers.Text.Base64 должно быть около 3 мс, а не 15 мс.

30.01.2021 15:43 UTC
0
Залил, версия 3.1.0.
Я не смог получить 3мс на кодировании 20мб данных через Base64.EncodeToUtf8. И даже если такое получится, то советую проверить output на то что там действительно правильно закодировалось.
30.01.2021 16:10 UTC
0

А я смог. Всё просто: значительная доля времени тратится на выделение памяти.
И вот, что в итоге получилось у меня, когда я не стал создавать новый буфер на каждой итерации:


BenchmarkDotNet=v0.12.1, OS=arch 
Intel Core i7-2600K CPU 3.40GHz (Sandy Bridge), 1 CPU, 4 logical and 4 physical cores
.NET Core SDK=5.0.101
  [Host]     : .NET Core 5.0.1 (CoreCLR 5.0.120.57516, CoreFX 5.0.120.57516), X64 RyuJIT
  DefaultJob : .NET Core 5.0.1 (CoreCLR 5.0.120.57516, CoreFX 5.0.120.57516), X64 RyuJIT

|                                 Method |       Mean |     Error |    StdDev | Ratio | RatioSD |    Gen 0 |    Gen 1 |    Gen 2 |   Allocated |
|--------------------------------------- |-----------:|----------:|----------:|------:|--------:|---------:|---------:|---------:|------------:|
|          System_Convert_ToBase64String |  42.735 ms | 0.7655 ms | 0.7160 ms |  1.00 |    0.00 | 166.6667 | 166.6667 | 166.6667 |  55924150 B |
|   System_Memory_Base64ToString_NoAlloc |   4.451 ms | 0.0383 ms | 0.0320 ms |  0.10 |    0.00 |        - |        - |        - |         2 B |
|           System_Memory_Base64ToString |  11.969 ms | 0.2381 ms | 0.2742 ms |  0.28 |    0.01 | 125.0000 | 125.0000 | 125.0000 |  27962095 B |
|             BaseN_BaseNDecoder_Convert |  73.296 ms | 0.5288 ms | 0.4946 ms |  1.72 |    0.04 |        - |        - |        - |  27962097 B |
|           BaseN_Base64Convert_ToString |  80.486 ms | 0.0185 ms | 0.0145 ms |  1.89 |    0.03 |        - |        - |        - |  55924121 B |
|           BaseN_Base32Convert_ToString |  85.490 ms | 0.3087 ms | 0.2737 ms |  2.00 |    0.04 | 142.8571 | 142.8571 | 142.8571 |  67108970 B |
| Wiry_Base32Encoding_Standard_GetString |  89.434 ms | 1.5743 ms | 1.4726 ms |  2.09 |    0.03 | 333.3333 | 333.3333 | 333.3333 | 134217916 B |
|       SimpleBase_Base32_Rfc4648_Encode | 107.411 ms | 0.5663 ms | 0.5297 ms |  2.51 |    0.04 |        - |        - |        - |  67108946 B |
|                  Albireo_Base32_Encode | 149.534 ms | 0.6898 ms | 0.5385 ms |  3.51 |    0.07 | 500.0000 | 500.0000 | 500.0000 | 134217986 B |

Можно, конечно, упирать на то, что память все равно надо выделять. Но вот только что мешает использовать эффективные решения и для этого сценария? (MemoryPool, обработка данных фрагментами и т.д.)

30.01.2021 17:32 UTC
0
Ну если тестить на кейсе где output летит в трубу, то стоит тестить по честному:

Код (тестилось на версии 3.1.1)
Результат:
|                               Method |      Mean | Ratio |
|------------------------------------- |----------:|------:|
| System_Memory_Base64ToString_NoAlloc |  3.480 ms |  1.00 |
|   BaseN_BaseNDecoder_Convert_NoAlloc | 18.628 ms |  5.36 |


Результаты сравнения с остальными альтернативами. Теперь немного быстрее встроенного Convert.ToBase64String() и много быстрее альтернатив.
@radium
30.01.2021 09:39 UTC
0

Настораживает отсутствие валидации входных данных. Это must have для практического использования.

@shai_hulud
30.01.2021 13:46 UTC
-1
В readme указано, что это желаемое (или нежелаемое для некоторых) поведение. Средства валидации присуствуют
GetCharCount(buffer) != GetMaxCharCount(buffer)
для тех кто хочет с этим что-то делать.
@khoramus
11.11.2024 16:01 UTC
0

Где метод Encoding, который принимает байты и выдает строку в Base64 в этой библиотеке? Почему-то методы, как будто только строки принимают это так не должно работать.

@khoramus
11.11.2024 16:14 UTC
0

А ладно вижу