1 // Licensed to the .NET Foundation under one or more agreements. 2 // The .NET Foundation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System.Runtime.CompilerServices; 6 using System.Runtime.InteropServices; 7 8 #if !netstandard 9 using Internal.Runtime.CompilerServices; 10 #endif 11 12 namespace System.Buffers.Text 13 { 14 /// <summary> 15 /// Convert between binary data and UTF-8 encoded text that is represented in base 64. 16 /// </summary> 17 public static partial class Base64 18 { 19 /// <summary> 20 /// Encode the span of binary data into UTF-8 encoded text represented as base 64. 21 /// 22 /// <param name="bytes">The input span which contains binary data that needs to be encoded.</param> 23 /// <param name="utf8">The output span which contains the result of the operation, i.e. the UTF-8 encoded text in base 64.</param> 24 /// <param name="consumed">The number of input bytes consumed during the operation. This can be used to slice the input for subsequent calls, if necessary.</param> 25 /// <param name="written">The number of bytes written into the output span. This can be used to slice the output for subsequent calls, if necessary.</param> 26 /// <param name="isFinalBlock">True (default) when the input span contains the entire data to decode. 27 /// Set to false only if it is known that the input span contains partial data with more data to follow.</param> 28 /// <returns>It returns the OperationStatus enum values: 29 /// - Done - on successful processing of the entire input span 30 /// - DestinationTooSmall - if there is not enough space in the output span to fit the encoded input 31 /// - NeedMoreData - only if isFinalBlock is false, otherwise the output is padded if the input is not a multiple of 3 32 /// It does not return InvalidData since that is not possible for base 64 encoding.</returns> 33 /// </summary> EncodeToUtf8(ReadOnlySpan<byte> bytes, Span<byte> utf8, out int consumed, out int written, bool isFinalBlock = true)34 public static OperationStatus EncodeToUtf8(ReadOnlySpan<byte> bytes, Span<byte> utf8, out int consumed, out int written, bool isFinalBlock = true) 35 { 36 ref byte srcBytes = ref MemoryMarshal.GetReference(bytes); 37 ref byte destBytes = ref MemoryMarshal.GetReference(utf8); 38 39 int srcLength = bytes.Length; 40 int destLength = utf8.Length; 41 42 int maxSrcLength = 0; 43 if (srcLength <= MaximumEncodeLength && destLength >= GetMaxEncodedToUtf8Length(srcLength)) 44 { 45 maxSrcLength = srcLength - 2; 46 } 47 else 48 { 49 maxSrcLength = (destLength >> 2) * 3 - 2; 50 } 51 52 int sourceIndex = 0; 53 int destIndex = 0; 54 int result = 0; 55 56 ref byte encodingMap = ref s_encodingMap[0]; 57 58 while (sourceIndex < maxSrcLength) 59 { 60 result = Encode(ref Unsafe.Add(ref srcBytes, sourceIndex), ref encodingMap); 61 Unsafe.WriteUnaligned(ref Unsafe.Add(ref destBytes, destIndex), result); 62 destIndex += 4; 63 sourceIndex += 3; 64 } 65 66 if (maxSrcLength != srcLength - 2) 67 goto DestinationSmallExit; 68 69 if (isFinalBlock != true) 70 goto NeedMoreDataExit; 71 72 if (sourceIndex == srcLength - 1) 73 { 74 result = EncodeAndPadTwo(ref Unsafe.Add(ref srcBytes, sourceIndex), ref encodingMap); 75 Unsafe.WriteUnaligned(ref Unsafe.Add(ref destBytes, destIndex), result); 76 destIndex += 4; 77 sourceIndex += 1; 78 } 79 else if (sourceIndex == srcLength - 2) 80 { 81 result = EncodeAndPadOne(ref Unsafe.Add(ref srcBytes, sourceIndex), ref encodingMap); 82 Unsafe.WriteUnaligned(ref Unsafe.Add(ref destBytes, destIndex), result); 83 destIndex += 4; 84 sourceIndex += 2; 85 } 86 87 consumed = sourceIndex; 88 written = destIndex; 89 return OperationStatus.Done; 90 91 NeedMoreDataExit: 92 consumed = sourceIndex; 93 written = destIndex; 94 return OperationStatus.NeedMoreData; 95 96 DestinationSmallExit: 97 consumed = sourceIndex; 98 written = destIndex; 99 return OperationStatus.DestinationTooSmall; 100 } 101 102 /// <summary> 103 /// Returns the maximum length (in bytes) of the result if you were to encode binary data within a byte span of size "length". 104 /// </summary> 105 /// <exception cref="System.ArgumentOutOfRangeException"> 106 /// Thrown when the specified <paramref name="length"/> is less than 0 or larger than 1610612733 (since encode inflates the data by 4/3). 107 /// </exception> 108 [MethodImpl(MethodImplOptions.AggressiveInlining)] GetMaxEncodedToUtf8Length(int length)109 public static int GetMaxEncodedToUtf8Length(int length) 110 { 111 if (length < 0 || length > MaximumEncodeLength) 112 ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length); 113 114 return (((length + 2) / 3) * 4); 115 } 116 117 /// <summary> 118 /// Encode the span of binary data (in-place) into UTF-8 encoded text represented as base 64. 119 /// The encoded text output is larger than the binary data contained in the input (the operation inflates the data). 120 /// 121 /// <param name="buffer">The input span which contains binary data that needs to be encoded. 122 /// It needs to be large enough to fit the result of the operation.</param> 123 /// <param name="dataLength">The amount of binary data contained within the buffer that needs to be encoded 124 /// (and needs to be smaller than the buffer length).</param> 125 /// <param name="written">The number of bytes written into the buffer.</param> 126 /// <returns>It returns the OperationStatus enum values: 127 /// - Done - on successful processing of the entire buffer 128 /// - DestinationTooSmall - if there is not enough space in the buffer beyond dataLength to fit the result of encoding the input 129 /// It does not return NeedMoreData since this method tramples the data in the buffer and hence can only be called once with all the data in the buffer. 130 /// It does not return InvalidData since that is not possible for base 64 encoding.</returns> 131 /// </summary> EncodeToUtf8InPlace(Span<byte> buffer, int dataLength, out int written)132 public static OperationStatus EncodeToUtf8InPlace(Span<byte> buffer, int dataLength, out int written) 133 { 134 int encodedLength = GetMaxEncodedToUtf8Length(dataLength); 135 if (buffer.Length < encodedLength) 136 goto FalseExit; 137 138 int leftover = dataLength - dataLength / 3 * 3; // how many bytes after packs of 3 139 140 int destinationIndex = encodedLength - 4; 141 int sourceIndex = dataLength - leftover; 142 int result = 0; 143 144 ref byte encodingMap = ref s_encodingMap[0]; 145 ref byte bufferBytes = ref MemoryMarshal.GetReference(buffer); 146 147 // encode last pack to avoid conditional in the main loop 148 if (leftover != 0) 149 { 150 if (leftover == 1) 151 { 152 result = EncodeAndPadTwo(ref Unsafe.Add(ref bufferBytes, sourceIndex), ref encodingMap); 153 Unsafe.WriteUnaligned(ref Unsafe.Add(ref bufferBytes, destinationIndex), result); 154 destinationIndex -= 4; 155 } 156 else 157 { 158 result = EncodeAndPadOne(ref Unsafe.Add(ref bufferBytes, sourceIndex), ref encodingMap); 159 Unsafe.WriteUnaligned(ref Unsafe.Add(ref bufferBytes, destinationIndex), result); 160 destinationIndex -= 4; 161 } 162 } 163 164 sourceIndex -= 3; 165 while (sourceIndex >= 0) 166 { 167 result = Encode(ref Unsafe.Add(ref bufferBytes, sourceIndex), ref encodingMap); 168 Unsafe.WriteUnaligned(ref Unsafe.Add(ref bufferBytes, destinationIndex), result); 169 destinationIndex -= 4; 170 sourceIndex -= 3; 171 } 172 173 written = encodedLength; 174 return OperationStatus.Done; 175 176 FalseExit: 177 written = 0; 178 return OperationStatus.DestinationTooSmall; 179 } 180 181 [MethodImpl(MethodImplOptions.AggressiveInlining)] Encode(ref byte threeBytes, ref byte encodingMap)182 private static int Encode(ref byte threeBytes, ref byte encodingMap) 183 { 184 int i = (threeBytes << 16) | (Unsafe.Add(ref threeBytes, 1) << 8) | Unsafe.Add(ref threeBytes, 2); 185 186 int i0 = Unsafe.Add(ref encodingMap, i >> 18); 187 int i1 = Unsafe.Add(ref encodingMap, (i >> 12) & 0x3F); 188 int i2 = Unsafe.Add(ref encodingMap, (i >> 6) & 0x3F); 189 int i3 = Unsafe.Add(ref encodingMap, i & 0x3F); 190 191 return i0 | (i1 << 8) | (i2 << 16) | (i3 << 24); 192 } 193 194 [MethodImpl(MethodImplOptions.AggressiveInlining)] EncodeAndPadOne(ref byte twoBytes, ref byte encodingMap)195 private static int EncodeAndPadOne(ref byte twoBytes, ref byte encodingMap) 196 { 197 int i = (twoBytes << 16) | (Unsafe.Add(ref twoBytes, 1) << 8); 198 199 int i0 = Unsafe.Add(ref encodingMap, i >> 18); 200 int i1 = Unsafe.Add(ref encodingMap, (i >> 12) & 0x3F); 201 int i2 = Unsafe.Add(ref encodingMap, (i >> 6) & 0x3F); 202 203 return i0 | (i1 << 8) | (i2 << 16) | (EncodingPad << 24); 204 } 205 206 [MethodImpl(MethodImplOptions.AggressiveInlining)] EncodeAndPadTwo(ref byte oneByte, ref byte encodingMap)207 private static int EncodeAndPadTwo(ref byte oneByte, ref byte encodingMap) 208 { 209 int i = (oneByte << 8); 210 211 int i0 = Unsafe.Add(ref encodingMap, i >> 10); 212 int i1 = Unsafe.Add(ref encodingMap, (i >> 4) & 0x3F); 213 214 return i0 | (i1 << 8) | (EncodingPad << 16) | (EncodingPad << 24); 215 } 216 217 // Pre-computing this table using a custom string(s_characters) and GenerateEncodingMapAndVerify (found in tests) 218 private static readonly byte[] s_encodingMap = { 219 65, 66, 67, 68, 69, 70, 71, 72, //A..H 220 73, 74, 75, 76, 77, 78, 79, 80, //I..P 221 81, 82, 83, 84, 85, 86, 87, 88, //Q..X 222 89, 90, 97, 98, 99, 100, 101, 102, //Y..Z, a..f 223 103, 104, 105, 106, 107, 108, 109, 110, //g..n 224 111, 112, 113, 114, 115, 116, 117, 118, //o..v 225 119, 120, 121, 122, 48, 49, 50, 51, //w..z, 0..3 226 52, 53, 54, 55, 56, 57, 43, 47 //4..9, +, / 227 }; 228 229 private const byte EncodingPad = (byte)'='; // '=', for padding 230 231 private const int MaximumEncodeLength = (int.MaxValue >> 2) * 3; // 1610612733 232 } 233 } 234