19

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.

2

4 Answers 4

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.

7
  • It always return false. I have my var b64string = "[email protected]" encoded in base 64 (hash aW5kaXZpZHVhbHRlc3RAZW1haWwuY29t) and I get false.
    – Morasiu
    Commented Jul 12, 2018 at 9:04
  • @Morasiu can't test until this evening. Away from computer. But it should be var b64string = "aW5kaXZpZHVhbHRlc3RAZW1haWwuY29t"
    – xanatos
    Commented Jul 12, 2018 at 9:10
  • 1
    Okay, no problem. I will try resolve by myself then. It is actually var b64string = Convert.ToBase64String(Encoding.UTF8.GetBytes("[email protected]"));
    – Morasiu
    Commented Jul 12, 2018 at 9:11
  • 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
    Commented Jul 12, 2018 at 9:33
  • 1
    You don't have to use the NuGet package on .NET Core. That is only for .NET Framework.
    – Neme
    Commented Apr 4 at 16:11
8

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

// Minimum length that is guaranteed to fit all the data.
// We don't need the exact length, because the pool
// might return a larger buffer anyway.
var minLength = ((value.Length * 3) + 3) / 4;
var buffer = ArrayPool<byte>.Shared.Rent(minLength);
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);
}

For even better GC efficiency and small buffer sizes, have a look at stackalloc.

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));
}
2
  • 2
    You could make this better performance using stackalloc... Try Span<byte> bytes = stackalloc byte[256]; Commented May 6, 2021 at 21:38
  • 4
    @marsze - You're right, that you do not want to pass a possibly large array length into stackalloc. The docs 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<byte> buffer = inputLength <= MaxStackLimit ? stackalloc byte[MaxStackLimit] : new byte[inputLength]; Commented Jul 11, 2022 at 13:54
4

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

3
  • +1 for the bit-shift part. I'll use that now when needing to calc decoded size. It's sometimes a few more than actual, but should be faster than the traditional formula: (3 * (content length / 4)) - (number of "=" padding characters), since it's not determining padding characters and not using division.
    – Gen1-1
    Commented Oct 20, 2023 at 18:46
  • 1
    Imo the bytesBuffer shouldn't be initialized with a size depending on the user input. A large input can take down your application domain with a stackoverflow exception. A good rule of thumb is to never allocate more than a KB with stackalloc. Update: just now saw Mike Christiansens excellent comment about this. Commented Jul 9 at 10:14
  • It makes sense. Thanks Commented Jul 9 at 14:21

Not the answer you're looking for? Browse other questions tagged or ask your own question.