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