18

I was trying to write some try catch for Convert.FromBase64String() and I found out that it already has TryFromBase64String() method. But it needs 3 arguments:

public static bool TryFromBase64String(string s, Span<byte> bytes, out int bytesWritten);

So how can I use Span<byte> bytes there?

I only found this in docs, but without proper description. Maybe this is too obvious.

https://learn.microsoft.com/en-us/dotnet/api/system.convert.tryfrombase64string?view=netcore-2.1

Thank to @Damien_The_Unbeliever and THIS article I found out more about Span. So...

Span is used for saving memory and don't call GC so much. It can store arrays or portion of array, but I still can't figure out how to use it in that method.

Morasiu
  • 1,204
  • 2
  • 16
  • 38
  • 1
    `Span` is a new type in .NET. There are entire [articles](https://msdn.microsoft.com/en-us/magazine/mt814808.aspx) to read about it. – Damien_The_Unbeliever Jul 12 '18 at 08:20
  • It belongs to `System.Span` type, a C# 7.2 feature. Check for similar issue here: https://stackoverflow.com/questions/47849352/how-do-i-reference-system-spant-in-my-dotnet-core-project. – Tetsuya Yamamoto Jul 12 '18 at 08:20

4 Answers4

15

As written in the linked questions, System.Span<T> is a new C# 7.2 feature (and the Convert.TryFromBase64String is a newer .NET Core feature)

To use System.Span<> you have to install a nuget package:

Install-Package System.Memory

Then to use it:

byte[] buffer = new byte[((b64string.Length * 3) + 3) / 4 -
    (b64string.Length > 0 && b64string[b64string.Length - 1] == '=' ?
        b64string.Length > 1 && b64string[b64string.Length - 2] == '=' ?
            2 : 1 : 0)];

int written;
bool success = Convert.TryFromBase64String(b64string, buffer, out written);

Where b64string is your base-64 string. The over-complicated size for buffer should be the exact length of the buffer based on the length of the b64string.

xanatos
  • 109,618
  • 12
  • 197
  • 280
  • It always return false. I have my `var b64string = "individualtest@email.com"` encoded in base 64 (hash `aW5kaXZpZHVhbHRlc3RAZW1haWwuY29t`) and I get false. – Morasiu Jul 12 '18 at 09:04
  • @Morasiu can't test until this evening. Away from computer. But it should be `var b64string = "aW5kaXZpZHVhbHRlc3RAZW1haWwuY29t"` – xanatos Jul 12 '18 at 09:10
  • 1
    Okay, no problem. I will try resolve by myself then. It is actually `var b64string = Convert.ToBase64String(Encoding.UTF8.GetBytes("individualtest@email.com"));` – Morasiu Jul 12 '18 at 09:11
  • Ok, it worked! But is there a way to make this buffer declaration more simple? And do I need `written` for something? – Morasiu Jul 12 '18 at 09:25
  • 1
    @Morasiu If it is ok for you to have an overlong buffer, then `new byte[b64string.Length * 3 / 4]` will have up to 2 extra bytes. You'll have to use the `written` to know how many byte were written. Note that even with the overlong version, spaces in the middle of the b64 string (like `"aW5 kaX ZpZHVhbHRlc3RAZW1haWwuY29t"`) will cause the `byte[]` to be not fully used. – xanatos Jul 12 '18 at 09:33
  • Ok, so I'll just extract that ugly declaration and forget. Thanks! – Morasiu Jul 12 '18 at 09:43
6

You could use it like this, making use of all the TryFromBase64String arguments:

public string DecodeUtf8Base64(string input)
{
  var bytes = new Span<byte>(new byte[256]); // 256 is arbitrary

  if (!Convert.TryFromBase64String(input, bytes, out var bytesWritten))
  {
    throw new InvalidOperationException("The input is not a valid base64 string");
  }

  return Encoding.UTF8.GetString(bytes.Slice(0, bytesWritten));
}
Loul G.
  • 997
  • 13
  • 27
  • 2
    You could make this better performance using `stackalloc`... Try `Span bytes = stackalloc byte[256];` – Mike Christiansen May 06 '21 at 21:38
  • 2
    @marsze - You're right, that you do not want to pass a possibly large array length into `stackalloc`. [The docs](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/stackalloc) for `stackalloc` give an example on how to handle this. Assuming you've defined: `const int MaxStackLimit = 1024;` Then you can select the appropriate allocation mode depending on requested length: `Span buffer = inputLength <= MaxStackLimit ? stackalloc byte[MaxStackLimit] : new byte[inputLength];` – Mike Christiansen Jul 11 '22 at 13:54
5

Here's another approach, using ArrayPool (to reduce GC pressure), if you need the buffer only temporarily:

// Minimum length that is sure to fit all the data.
// We don't need the exact length, because
// ArrayPool might return a larger buffer anyway.
var length = ((value.Length * 3) + 3) / 4;
var buffer = ArrayPool<byte>.Shared.Rent(length);
try
{
    // (buffer is implicitly cast to Span<byte>)
    if (Convert.TryFromBase64String(value, buffer, out var bytesWritten))
    {
        // do something with it...
        return Encoding.UTF8.GetString(buffer, 0, bytesWritten);
    }
    throw new FormatException("Invalid base-64 sequence.");
}
finally
{
    ArrayPool<byte>.Shared.Return(buffer);
}
marsze
  • 15,079
  • 5
  • 45
  • 61
2

I used it like this:

string base64String = 'somebase64';

Span<byte> bytesBuffer = stackalloc byte[base64String.Length];

if (!Convert.TryFromBase64String(base64String, bytesBuffer, out int bytesWritten))
{
    return false;
}

ReadOnlySpan<byte> actualBytes = bytesBuffer[..bytesWritten];

UPDATE:

more precise way to count bytes

const int bitsEncodedPerChar = 6; 

int bytesExpected = (base64String.Length * bitsEncodedPerChar) >> 3; // divide by 8 bits in a byte

see https://en.wikipedia.org/wiki/Base64#Base64_table_from_RFC_4648

Timur Gaitov
  • 81
  • 2
  • 6