View Javadoc
1   /*
2    * Copyright 2012 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  /*
17   * Written by Robert Harder and released to the public domain, as explained at
18   * https://creativecommons.org/licenses/publicdomain
19   */
20  package io.netty5.handler.codec.base64;
21  
22  import io.netty5.buffer.api.Buffer;
23  import io.netty5.buffer.api.BufferAllocator;
24  import io.netty5.util.ByteProcessor;
25  
26  import static io.netty5.buffer.api.DefaultBufferAllocators.offHeapAllocator;
27  import static io.netty5.buffer.api.DefaultBufferAllocators.onHeapAllocator;
28  import static java.util.Objects.requireNonNull;
29  
30  /**
31   * Utility class for {@link Buffer} that encodes and decodes to and from
32   * <a href="https://en.wikipedia.org/wiki/Base64">Base64</a> notation.
33   * <p>
34   * The encoding and decoding algorithm in this class has been derived from
35   * <a href="http://iharder.sourceforge.net/current/java/base64/">Robert Harder's Public Domain
36   * Base64 Encoder/Decoder</a>.
37   */
38  public final class Base64 {
39  
40      /** Maximum line length (76) of Base64 output. */
41      private static final int MAX_LINE_LENGTH = 76;
42  
43      /** The equals sign (=) as a byte. */
44      private static final byte EQUALS_SIGN = (byte) '=';
45  
46      /** The new line character (\n) as a byte. */
47      private static final byte NEW_LINE = (byte) '\n';
48  
49      private static final byte WHITE_SPACE_ENC = -5; // Indicates white space in encoding
50  
51      private static final byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding
52  
53      private static byte[] alphabet(Base64Dialect dialect) {
54          requireNonNull(dialect, "dialect");
55          return dialect.alphabet;
56      }
57  
58      private static byte[] decodabet(Base64Dialect dialect) {
59          requireNonNull(dialect, "dialect");
60          return dialect.decodabet;
61      }
62  
63      private static boolean breakLines(Base64Dialect dialect) {
64          requireNonNull(dialect, "dialect");
65          return dialect.breakLinesByDefault;
66      }
67  
68      public static Buffer encode(Buffer src) {
69          return encode(src, Base64Dialect.STANDARD);
70      }
71  
72      public static Buffer encode(Buffer src, Base64Dialect dialect) {
73          return encode(src, breakLines(dialect), dialect);
74      }
75  
76      public static Buffer encode(Buffer src, boolean breakLines) {
77          return encode(src, breakLines, Base64Dialect.STANDARD);
78      }
79  
80      public static Buffer encode(Buffer src, boolean breakLines, Base64Dialect dialect) {
81          requireNonNull(src, "src");
82  
83          Buffer dest = encode(src, src.readerOffset(), src.readableBytes(), breakLines, dialect);
84          src.readerOffset(src.writerOffset());
85          return dest;
86      }
87  
88      public static Buffer encode(Buffer src, int off, int len) {
89          return encode(src, off, len, Base64Dialect.STANDARD);
90      }
91  
92      public static Buffer encode(Buffer src, int off, int len, Base64Dialect dialect) {
93          return encode(src, off, len, breakLines(dialect), dialect);
94      }
95  
96      public static Buffer encode(
97              Buffer src, int off, int len, boolean breakLines) {
98          return encode(src, off, len, breakLines, Base64Dialect.STANDARD);
99      }
100 
101     public static Buffer encode(
102             Buffer src, int off, int len, boolean breakLines, Base64Dialect dialect) {
103         return encode(src, off, len, breakLines, dialect,
104                       src.isDirect() ? offHeapAllocator() : onHeapAllocator());
105     }
106 
107     public static Buffer encode(
108             Buffer src, int off, int len, boolean breakLines, Base64Dialect dialect, BufferAllocator allocator) {
109         requireNonNull(src, "src");
110         requireNonNull(dialect, "dialect");
111 
112         Buffer dest = allocator.allocate(encodedBufferSize(len, breakLines));
113         byte[] alphabet = alphabet(dialect);
114         int d = 0;
115         int e = 0;
116         int len2 = len - 2;
117         int lineLength = 0;
118         for (; d < len2; d += 3, e += 4) {
119             encode3to4(src, d + off, 3, dest, e, alphabet);
120 
121             lineLength += 4;
122 
123             if (breakLines && lineLength == MAX_LINE_LENGTH) {
124                 dest.setByte(e + 4, NEW_LINE);
125                 e ++;
126                 lineLength = 0;
127             } // end if: end of line
128         } // end for: each piece of array
129 
130         if (d < len) {
131             encode3to4(src, d + off, len - d, dest, e, alphabet);
132             e += 4;
133         } // end if: some padding needed
134 
135         // Remove last byte if it's a newline
136         if (e > 1 && dest.getByte(e - 1) == NEW_LINE) {
137             e--;
138         }
139 
140         return dest.writerOffset(e);
141     }
142 
143     private static void encode3to4(
144             Buffer src, int srcOffset, int numSigBytes, Buffer dest, int destOffset, byte[] alphabet) {
145         //           1         2         3
146         // 01234567890123456789012345678901 Bit position
147         // --------000000001111111122222222 Array position from threeBytes
148         // --------|    ||    ||    ||    | Six bit groups to index ALPHABET
149         //          >>18  >>12  >> 6  >> 0  Right shift necessary
150         //                0x3f  0x3f  0x3f  Additional AND
151 
152         // Create buffer with zero-padding if there are only one or two
153         // significant bytes passed in the array.
154         // We have to shift left 24 in order to flush out the 1's that appear
155         // when Java treats a value as negative that is cast from a byte to an int.
156         final int inBuff;
157         switch (numSigBytes) {
158         case 1:
159             inBuff = toInt(src.getByte(srcOffset));
160             break;
161         case 2:
162             inBuff = toIntBE(src.getShort(srcOffset));
163             break;
164         default:
165             inBuff = numSigBytes <= 0 ? 0 : toIntBE(src.getMedium(srcOffset));
166             break;
167         }
168         encode3to4BigEndian(inBuff, numSigBytes, dest, destOffset, alphabet);
169     }
170 
171     // package-private for testing
172     static int encodedBufferSize(int len, boolean breakLines) {
173         // Cast len to long to prevent overflow
174         long len43 = ((long) len << 2) / 3;
175 
176         // Account for padding
177         long ret = (len43 + 3) & ~3;
178 
179         if (breakLines) {
180             ret += len43 / MAX_LINE_LENGTH;
181         }
182 
183         return ret < Integer.MAX_VALUE ? (int) ret : Integer.MAX_VALUE;
184     }
185 
186     private static int toInt(byte value) {
187         return (value & 0xff) << 16;
188     }
189 
190     private static int toIntBE(short value) {
191         return (value & 0xff00) << 8 | (value & 0xff) << 8;
192     }
193 
194     private static int toIntBE(int mediumValue) {
195         return (mediumValue & 0xff0000) | (mediumValue & 0xff00) | (mediumValue & 0xff);
196     }
197 
198     private static void encode3to4BigEndian(
199             int inBuff, int numSigBytes, Buffer dest, int destOffset, byte[] alphabet) {
200         // Packing bytes into an int to reduce bound and reference count checking.
201         switch (numSigBytes) {
202             case 3:
203                 dest.setInt(destOffset, alphabet[inBuff >>> 18       ] << 24 |
204                                         alphabet[inBuff >>> 12 & 0x3f] << 16 |
205                                         alphabet[inBuff >>>  6 & 0x3f] << 8  |
206                                         alphabet[inBuff        & 0x3f]);
207                 break;
208             case 2:
209                 dest.setInt(destOffset, alphabet[inBuff >>> 18       ] << 24 |
210                                         alphabet[inBuff >>> 12 & 0x3f] << 16 |
211                                         alphabet[inBuff >>> 6  & 0x3f] << 8  |
212                                         EQUALS_SIGN);
213                 break;
214             case 1:
215                 dest.setInt(destOffset, alphabet[inBuff >>> 18       ] << 24 |
216                                         alphabet[inBuff >>> 12 & 0x3f] << 16 |
217                                         EQUALS_SIGN << 8                     |
218                                         EQUALS_SIGN);
219                 break;
220             default:
221                 // NOOP
222                 break;
223         }
224     }
225 
226     public static Buffer decode(Buffer src) {
227         return decode(src, Base64Dialect.STANDARD);
228     }
229 
230     public static Buffer decode(Buffer src, Base64Dialect dialect) {
231         requireNonNull(src, "src");
232 
233         Buffer dest = decode(src, src.readerOffset(), src.readableBytes(), dialect);
234         src.readerOffset(src.writerOffset());
235         return dest;
236     }
237 
238     public static Buffer decode(
239             Buffer src, int off, int len) {
240         return decode(src, off, len, Base64Dialect.STANDARD);
241     }
242 
243     public static Buffer decode(
244             Buffer src, int off, int len, Base64Dialect dialect) {
245         return decode(src, off, len, dialect, src.isDirect() ? offHeapAllocator() : onHeapAllocator());
246     }
247 
248     public static Buffer decode(
249             Buffer src, int off, int len, Base64Dialect dialect, BufferAllocator allocator) {
250         requireNonNull(src, "src");
251         requireNonNull(dialect, "dialect");
252 
253         // Using a ByteProcessor to reduce bound and reference count checking.
254         return new Decoder().decode(src, off, len, allocator, dialect);
255     }
256 
257     // package-private for testing
258     static int decodedBufferSize(int len) {
259         return len - (len >>> 2);
260     }
261 
262     private static final class Decoder implements ByteProcessor {
263         private final byte[] b4 = new byte[4];
264         private int b4Posn;
265         private byte[] decodabet;
266         private int outBuffPosn;
267         private Buffer dest;
268 
269         Buffer decode(Buffer src, int off, int len, BufferAllocator allocator, Base64Dialect dialect) {
270             dest = allocator.allocate(decodedBufferSize(len)); // Upper limit on size of output
271 
272             try {
273                 decodabet = decodabet(dialect);
274                 src.openCursor(off, len).process(this);
275                 return dest.writerOffset(outBuffPosn);
276             } catch (Throwable cause) {
277                 dest.close();
278                 throw cause;
279             }
280         }
281 
282         @Override
283         public boolean process(byte value) {
284             if (value > 0) {
285                 byte sbiDecode = decodabet[value];
286                 if (sbiDecode >= WHITE_SPACE_ENC) { // White space, Equals sign or better
287                     if (sbiDecode >= EQUALS_SIGN_ENC) { // Equals sign or better
288                         b4[b4Posn ++] = value;
289                         if (b4Posn > 3) { // Quartet built
290                             outBuffPosn += decode4to3(b4, dest, outBuffPosn, decodabet);
291                             b4Posn = 0;
292 
293                             // If that was the equals sign, break out of 'for' loop
294                             return value != EQUALS_SIGN;
295                         }
296                     }
297                     return true;
298                 }
299             }
300             throw new IllegalArgumentException(
301                     "invalid Base64 input character: " + (short) (value & 0xFF) + " (decimal)");
302         }
303 
304         private static int decode4to3(byte[] src, Buffer dest, int destOffset, byte[] decodabet) {
305             final byte src0 = src[0];
306             final byte src1 = src[1];
307             final byte src2 = src[2];
308             final int decodedValue;
309             if (src2 == EQUALS_SIGN) {
310                 // Example: Dk==
311                 try {
312                     decodedValue = (decodabet[src0] & 0xff) << 2 | (decodabet[src1] & 0xff) >>> 4;
313                 } catch (IndexOutOfBoundsException ignored) {
314                     throw new IllegalArgumentException("not encoded in Base64");
315                 }
316                 dest.setUnsignedByte(destOffset, (byte) decodedValue);
317                 return 1;
318             }
319 
320             final byte src3 = src[3];
321             if (src3 == EQUALS_SIGN) {
322                 // Example: DkL=
323                 final byte b1 = decodabet[src1];
324                 // Packing bytes into a short to reduce bound and reference count checking.
325                 try {
326                     // The decodabet bytes are meant to straddle byte boundaries and so we must carefully mask out
327                     // the bits we care about.
328                     decodedValue = ((decodabet[src0] & 0x3f) << 2 | (b1 & 0xf0) >> 4) << 8 |
329                                    (b1 & 0xf) << 4 | (decodabet[src2] & 0xfc) >>> 2;
330                 } catch (IndexOutOfBoundsException ignored) {
331                     throw new IllegalArgumentException("not encoded in Base64");
332                 }
333                 dest.setUnsignedShort(destOffset, decodedValue);
334                 return 2;
335             }
336 
337             // Example: DkLE
338             try {
339                 decodedValue = (decodabet[src0] & 0x3f) << 18 |
340                                (decodabet[src1] & 0xff) << 12 |
341                                (decodabet[src2] & 0xff) << 6 |
342                                decodabet[src3] & 0xff;
343             } catch (IndexOutOfBoundsException ignored) {
344                 throw new IllegalArgumentException("not encoded in Base64");
345             }
346             dest.setMedium(destOffset, decodedValue);
347             return 3;
348         }
349     }
350 
351     private Base64() {
352         // Unused
353     }
354 }