001/*
002 * JPPF.
003 * Copyright (C) 2005-2016 JPPF Team.
004 * http://www.jppf.org
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License");
007 * you may not use this file except in compliance with the License.
008 * You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019package org.jppf.utils.base64;
020
021import static org.jppf.utils.base64.Base64.*;
022
023import java.io.*;
024
025import org.jppf.io.IO;
026
027/**
028 * 
029 * @author Laurent Cohen
030 */
031public final class Base64Decoding
032{
033  /* ********  D E C O D I N G   M E T H O D S  ******** */
034
035  /**
036   * Decodes four bytes from array <var>source</var> and writes the resulting bytes (up to three of them) to <var>destination</var>.
037   * The source and destination arrays can be manipulated anywhere along their length by specifying <var>srcOffset</var> and <var>destOffset</var>.
038   * This method does not check to make sure your arrays are large enough to accomodate <var>srcOffset</var> + 4 for
039   * the <var>source</var> array or <var>destOffset</var> + 3 for the <var>destination</var> array.
040   * This method returns the actual number of bytes that  were converted from the Base64 encoding.
041   * <p>This is the lowest level of the decoding methods with all possible parameters.</p>
042   * @param source the array to convert
043   * @param srcOffset the index where conversion begins
044   * @param destination the array to hold the conversion
045   * @param destOffset the index where output will be put
046   * @param options alphabet type is pulled from this (standard, url-safe, ordered)
047   * @return the number of decoded bytes converted
048   * @throws NullPointerException if source or destination arrays are null
049   * @throws IllegalArgumentException if srcOffset or destOffset are invalid or there is not enough room in the array.
050   * @since 1.3
051   */
052  static int decode4to3(
053      final byte[] source, final int srcOffset,
054      final byte[] destination, final int destOffset, final int options ) {
055    // Lots of error checking and exception throwing
056    if( source == null ) throw new NullPointerException( "Source array was null." );
057    if( destination == null ) throw new NullPointerException( "Destination array was null." );
058    if( srcOffset < 0 || srcOffset + 3 >= source.length )
059      throw new IllegalArgumentException( String.format("Source array with length %d cannot have offset of %d and still process four bytes.", source.length, srcOffset ) );
060    if( destOffset < 0 || destOffset +2 >= destination.length )
061      throw new IllegalArgumentException( String.format("Destination array with length %d cannot have offset of %d and still store three bytes.", destination.length, destOffset ) );
062
063    byte[] DECODABET = getDecodabet( options );
064    // Example: Dk==
065    if( source[ srcOffset + 2] == EQUALS_SIGN ) {
066      // Two ways to do the same thing. Don't know which way I like best.
067      //int outBuff =   ( ( DECODABET[ source[ srcOffset    ] ] << 24 ) >>>  6 )
068      //              | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 );
069      int outBuff =   ( ( DECODABET[ source[ srcOffset    ] ] & 0xFF ) << 18 )
070      | ( ( DECODABET[ source[ srcOffset + 1] ] & 0xFF ) << 12 );
071      destination[ destOffset ] = (byte)( outBuff >>> 16 );
072      return 1;
073    }
074    // Example: DkL=
075    else if( source[ srcOffset + 3 ] == EQUALS_SIGN ) {
076      // Two ways to do the same thing. Don't know which way I like best.
077      //int outBuff =   ( ( DECODABET[ source[ srcOffset     ] ] << 24 ) >>>  6 )
078      //              | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 )
079      //              | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 );
080      int outBuff =   ( ( DECODABET[ source[ srcOffset     ] ] & 0xFF ) << 18 )
081      | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 )
082      | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) <<  6 );
083
084      destination[ destOffset     ] = (byte)( outBuff >>> 16 );
085      destination[ destOffset + 1 ] = (byte)( outBuff >>>  8 );
086      return 2;
087    }
088    // Example: DkLE
089    else {
090      // Two ways to do the same thing. Don't know which way I like best.
091      //int outBuff =   ( ( DECODABET[ source[ srcOffset     ] ] << 24 ) >>>  6 )
092      //              | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 )
093      //              | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 )
094      //              | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 );
095      int outBuff =   ( ( DECODABET[ source[ srcOffset     ] ] & 0xFF ) << 18 )
096      | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 )
097      | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) <<  6)
098      | ( ( DECODABET[ source[ srcOffset + 3 ] ] & 0xFF )      );
099      destination[ destOffset     ] = (byte)( outBuff >> 16 );
100      destination[ destOffset + 1 ] = (byte)( outBuff >>  8 );
101      destination[ destOffset + 2 ] = (byte)( outBuff       );
102      return 3;
103    }
104  }   // end decodeToBytes
105
106  /**
107   * Low-level access to decoding ASCII characters in the form of a byte array. <strong>Ignores GUNZIP option, if
108   * it's set.</strong> This is not generally a recommended method, although it is used internally as part of the decoding process.
109   * Special case: if len = 0, an empty array is returned. Still, if you need more speed and reduced memory footprint (and aren't gzipping), consider this method.
110   * @param source The Base64 encoded data
111   * @return decoded data
112   * @throws IOException if any I/O error occurs.
113   * @since 2.3.1
114   */
115  public static byte[] decode( final byte[] source ) throws IOException {
116    byte[] decoded = null;
117    decoded = decode( source, 0, source.length, Base64.NO_OPTIONS );
118    return decoded;
119  }
120
121  /**
122   * Low-level access to decoding ASCII characters in the form of a byte array. <strong>Ignores GUNZIP option, if
123   * it's set.</strong> This is not generally a recommended method,  although it is used internally as part of the decoding process.
124   * Special case: if len = 0, an empty array is returned. Still, if you need more speed and reduced memory footprint (and aren't gzipping), consider this method.
125   * @param source The Base64 encoded data
126   * @param off    The offset of where to begin decoding
127   * @param len    The length of characters to decode
128   * @param options Can specify options such as alphabet type to use
129   * @return decoded data
130   * @throws IOException If bogus characters exist in source data
131   * @since 1.3
132   */
133  public static byte[] decode( final byte[] source, final int off, final int len, final int options ) throws IOException {
134    // Lots of error checking and exception throwing
135    if( source == null ) throw new NullPointerException( "Cannot decode null source array." );
136    if( off < 0 || off + len > source.length )
137      throw new IllegalArgumentException( String.format("Source array with length %d cannot have offset of %d and process %d bytes.", source.length, off, len ) );
138
139    if( len == 0 )return IO.EMPTY_BYTES;
140    else if( len < 4 ){
141      throw new IllegalArgumentException(
142          "Base64-encoded string must have at least four characters, but length specified was " + len );
143    }   // end if
144    byte[] DECODABET = getDecodabet( options );
145    int    len34   = len * 3 / 4;       // Estimate on array size
146    byte[] outBuff = new byte[ len34 ]; // Upper limit on size of output
147    int    outBuffPosn = 0;             // Keep track of where we're writing
148    byte[] b4        = new byte[4];     // Four byte buffer from source, eliminating white space
149    int    b4Posn    = 0;               // Keep track of four byte input buffer
150    int    i         = 0;               // Source array counter
151    byte   sbiDecode = 0;               // Special value from DECODABET
152
153    for( i = off; i < off+len; i++ ) {  // Loop through source
154      sbiDecode = DECODABET[ source[i]&0xFF ];
155      // White space, Equals sign, or legit Base64 character
156      // Note the values such as -5 and -9 in the DECODABETs at the top of the file.
157      if( sbiDecode >= WHITE_SPACE_ENC )  {
158        if( sbiDecode >= EQUALS_SIGN_ENC ) {
159          b4[ b4Posn++ ] = source[i];         // Save non-whitespace
160          if( b4Posn > 3 ) {                  // Time to decode?
161            outBuffPosn += decode4to3( b4, 0, outBuff, outBuffPosn, options );
162            b4Posn = 0;
163            // If that was the equals sign, break out of 'for' loop
164            if( source[i] == EQUALS_SIGN )break;
165          }   // end if: quartet built
166        }   // end if: equals sign or better
167      }   // end if: white space, equals sign or better
168      else {
169        // There's a bad input character in the Base64 stream.
170        throw new IOException( String.format("Bad Base64 input character decimal %d in array position %d", (source[i])&0xFF, i ) );
171      }   // end else:
172    }   // each input character
173    byte[] out = new byte[ outBuffPosn ];
174    System.arraycopy( outBuff, 0, out, 0, outBuffPosn );
175    return out;
176  }   // end decode
177
178  /**
179   * Decodes data from Base64 notation, automatically detecting gzip-compressed data and decompressing it.
180   * @param s the string to decode
181   * @return the decoded data
182   * @throws IOException If there is a problem
183   * @since 1.4
184   */
185  public static byte[] decode( final String s ) throws IOException {
186    return decode( s, NO_OPTIONS );
187  }
188
189  /**
190   * Decodes data from Base64 notation, automatically detecting gzip-compressed data and decompressing it.
191   * @param s the string to decode
192   * @param options encode options such as URL_SAFE
193   * @return the decoded data
194   * @throws IOException if there is an error
195   * @throws NullPointerException if <tt>s</tt> is null
196   * @since 1.4
197   */
198  public static byte[] decode( final String s, final int options ) throws IOException {
199    if( s == null ) throw new NullPointerException( "Input string was null." );
200    byte[] bytes;
201    try {
202      bytes = s.getBytes( PREFERRED_ENCODING );
203    }   // end try
204    catch( UnsupportedEncodingException uee ) {
205      bytes = s.getBytes();
206    }   // end catch
207
208    // Decode
209    bytes = decode( bytes, 0, bytes.length, options );
210    // Check to see if it's gzip-compressed GZIP Magic Two-Byte Number: 0x8b1f (35615)
211    boolean dontGunzip = (options & DONT_GUNZIP) != 0;
212    if( (bytes != null) && (bytes.length >= 4) && (!dontGunzip) ) {
213      int head = (bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00);
214      if( java.util.zip.GZIPInputStream.GZIP_MAGIC == head )  {
215        ByteArrayInputStream  bais = null;
216        java.util.zip.GZIPInputStream gzis = null;
217        ByteArrayOutputStream baos = null;
218        byte[] buffer = new byte[2048];
219        int    length = 0;
220        try {
221          baos = new ByteArrayOutputStream();
222          bais = new ByteArrayInputStream( bytes );
223          gzis = new java.util.zip.GZIPInputStream( bais );
224          while( ( length = gzis.read( buffer ) ) >= 0 ) {
225            baos.write(buffer,0,length);
226          }   // end while: reading input
227          // No error? Get new bytes.
228          bytes = baos.toByteArray();
229        }   // end try
230        catch( IOException e ) {
231          e.printStackTrace();
232          // Just return originally-decoded bytes
233        }   // end catch
234        finally {
235          try{ baos.close(); } catch( Exception e ){}
236          try{ gzis.close(); } catch( Exception e ){}
237          try{ bais.close(); } catch( Exception e ){}
238        }   // end finally
239      }   // end if: gzipped
240    }   // end if: bytes.length >= 2
241    return bytes;
242  }   // end decode
243
244  /**
245   * Attempts to decode Base64 data and deserialize a Java Object within. Returns <tt>null</tt> if there was an error.
246   * @param encodedObject The Base64 data to decode
247   * @return The decoded and deserialized object
248   * @throws NullPointerException if encodedObject is null
249   * @throws IOException if there is a general error
250   * @throws ClassNotFoundException if the decoded object is of a class that cannot be found by the JVM
251   * @since 1.5
252   */
253  public static Object decodeToObject( final String encodedObject ) throws IOException, ClassNotFoundException {
254    return decodeToObject(encodedObject,NO_OPTIONS,null);
255  }
256
257  /**
258   * Attempts to decode Base64 data and deserialize a Java Object within. Returns <tt>null</tt> if there was an error.
259   * If <tt>loader</tt> is not null, it will be the class loader used when deserializing.
260   * @param encodedObject The Base64 data to decode
261   * @param options Various parameters related to decoding
262   * @param loader Optional class loader to use in deserializing classes.
263   * @return The decoded and deserialized object
264   * @throws NullPointerException if encodedObject is null
265   * @throws IOException if there is a general error
266   * @throws ClassNotFoundException if the decoded object is of a class that cannot be found by the JVM
267   * @since 2.3.4
268   */
269  public static Object decodeToObject(final String encodedObject, final int options, final ClassLoader loader ) throws IOException, java.lang.ClassNotFoundException {
270    // Decode and gunzip if necessary
271    byte[] objBytes = decode( encodedObject, options );
272    ByteArrayInputStream  bais = null;
273    ObjectInputStream     ois  = null;
274    Object obj = null;
275    try {
276      bais = new ByteArrayInputStream( objBytes );
277      // If no custom class loader is provided, use Java's builtin OIS.
278      if( loader == null ) ois  = new ObjectInputStream( bais );
279      // Else make a customized object input stream that uses the provided class loader.
280      else {
281        ois = new ObjectInputStream(bais){
282          @Override
283          public Class<?> resolveClass(final ObjectStreamClass streamClass)
284          throws IOException, ClassNotFoundException {
285            Class c = Class.forName(streamClass.getName(), false, loader);
286            if( c == null ) return super.resolveClass(streamClass);
287            else return c;   // Class loader knows of this class.
288          }   // end resolveClass
289        };  // end ois
290      }   // end else: no custom class loader
291      obj = ois.readObject();
292    }   // end try
293    catch( IOException e ) {
294      throw e;    // Catch and throw in order to execute finally{}
295    }   // end catch
296    catch( java.lang.ClassNotFoundException e ) {
297      throw e;    // Catch and throw in order to execute finally{}
298    }   // end catch
299    finally {
300      try{ bais.close(); } catch( Exception e ){}
301      try{ ois.close();  } catch( Exception e ){}
302    }   // end finally
303    return obj;
304  }   // end decodeObject
305
306  /**
307   * Convenience method for encoding data to a file.
308   * <p>As of v 2.3, if there is a error, the method will throw an IOException. <b>This is new to v2.3!</b>
309   * In earlier versions, it just returned false, but  in retrospect that's a pretty poor way to handle it.</p>
310   * @param dataToEncode byte array of data to encode in base64 form
311   * @param filename Filename for saving encoded data
312   * @throws IOException if there is an error
313   * @throws NullPointerException if dataToEncode is null
314   * @since 2.1
315   */
316  public static void encodeToFile( final byte[] dataToEncode, final String filename )
317  throws IOException {
318    if( dataToEncode == null ) throw new NullPointerException( "Data to encode was null." );
319    Base64OutputStream bos = null;
320    try {
321      bos = new Base64OutputStream(
322          new FileOutputStream( filename ), Base64.ENCODE );
323      bos.write( dataToEncode );
324    }   // end try
325    catch( IOException e ) {
326      throw e; // Catch and throw to execute finally{} block
327    }   // end catch: IOException
328    finally {
329      try{ bos.close(); } catch( Exception e ){}
330    }   // end finally
331  }   // end encodeToFile
332
333  /**
334   * Convenience method for decoding data to a file.
335   * <p>As of v 2.3, if there is a error, the method will throw an IOException. <b>This is new to v2.3!</b>
336   * In earlier versions, it just returned false, but in retrospect that's a pretty poor way to handle it.</p>
337   * @param dataToDecode Base64-encoded data as a string
338   * @param filename Filename for saving decoded data
339   * @throws IOException if there is an error
340   * @since 2.1
341   */
342  public static void decodeToFile( final String dataToDecode, final String filename) throws IOException {
343    Base64OutputStream bos = null;
344    try{
345      bos = new Base64OutputStream(new FileOutputStream( filename ), Base64.DECODE );
346      bos.write( dataToDecode.getBytes( PREFERRED_ENCODING ) );
347    }   // end try
348    catch( IOException e ) {
349      throw e; // Catch and throw to execute finally{} block
350    }   // end catch: IOException
351    finally {
352      try{ bos.close(); } catch( Exception e ){}
353    }   // end finally
354  }   // end decodeToFile
355
356  /**
357   * Convenience method for reading a base64-encoded file and decoding it.
358   * <p>As of v 2.3, if there is a error, the method will throw an IOException. <b>This is new to v2.3!</b>
359   * In earlier versions, it just returned false, but in retrospect that's a pretty poor way to handle it.</p>
360   * @param filename Filename for reading encoded data
361   * @return decoded byte array
362   * @throws IOException if there is an error
363   * @since 2.1
364   */
365  public static byte[] decodeFromFile( final String filename )
366  throws IOException {
367    byte[] decodedData = null;
368    Base64InputStream bis = null;
369    try
370    {
371      // Set up some useful variables
372      File file = new File( filename );
373      byte[] buffer = null;
374      int length   = 0;
375      int numBytes = 0;
376      // Check for size of file
377      if( file.length() > Integer.MAX_VALUE ) throw new IOException( "File is too big for this convenience method (" + file.length() + " bytes)." );
378      buffer = new byte[ (int)file.length() ];
379      // Open a stream
380      bis = new Base64InputStream(new BufferedInputStream(new FileInputStream( file ) ), Base64.DECODE );
381      // Read until done
382      while( ( numBytes = bis.read( buffer, length, 4096 ) ) >= 0 )length += numBytes;
383      // Save in a variable to return
384      decodedData = new byte[ length ];
385      System.arraycopy( buffer, 0, decodedData, 0, length );
386    }   // end try
387    catch( IOException e ) {
388      throw e; // Catch and release to execute finally{}
389    }   // end catch: IOException
390    finally {
391      try{ bis.close(); } catch( Exception e) {}
392    }   // end finally
393    return decodedData;
394  }   // end decodeFromFile
395
396  /**
397   * Convenience method for reading a binary file and base64-encoding it.
398   * <p>As of v 2.3, if there is a error, the method will throw an IOException. <b>This is new to v2.3!</b>
399   * In earlier versions, it just returned false, but in retrospect that's a pretty poor way to handle it.</p>
400   * @param filename Filename for reading binary data
401   * @return base64-encoded string
402   * @throws IOException if there is an error
403   * @since 2.1
404   */
405  public static String encodeFromFile(final String filename) throws IOException {
406    String encodedData = null;
407    Base64InputStream bis = null;
408    try
409    {
410      File file = new File( filename );
411      byte[] buffer = new byte[ Math.max((int)(file.length() * 1.4+1),40) ]; // Need max() for math on small files (v2.2.1); Need +1 for a few corner cases (v2.3.5)
412      int length   = 0;
413      int numBytes = 0;
414      bis = new Base64InputStream(new BufferedInputStream(new FileInputStream( file ) ), Base64.ENCODE );
415      // Read until done
416      while( ( numBytes = bis.read( buffer, length, 4096 ) ) >= 0 ) length += numBytes;
417      // Save in a variable to return
418      encodedData = new String( buffer, 0, length, Base64.PREFERRED_ENCODING );
419    }   // end try
420    catch( IOException e ) {
421      throw e; // Catch and release to execute finally{}
422    }   // end catch: IOException
423    finally {
424      try{ bis.close(); } catch( Exception e) {}
425    }   // end finally
426    return encodedData;
427  }   // end encodeFromFile
428
429  /**
430   * Reads <tt>infile</tt> and encodes it to <tt>outfile</tt>.
431   * @param infile Input file
432   * @param outfile Output file
433   * @throws IOException if there is an error
434   * @since 2.2
435   */
436  public static void encodeFileToFile( final String infile, final String outfile ) throws IOException {
437    String encoded = encodeFromFile( infile );
438    OutputStream out = null;
439    try{
440      out = new BufferedOutputStream(new FileOutputStream( outfile ) );
441      out.write( encoded.getBytes("US-ASCII") ); // Strict, 7-bit output.
442    }   // end try
443    catch( IOException e ) {
444      throw e; // Catch and release to execute finally{}
445    }   // end catch
446    finally {
447      try { out.close(); }
448      catch( Exception ex ){}
449    }   // end finally
450  }   // end encodeFileToFile
451
452  /**
453   * Reads <tt>infile</tt> and decodes it to <tt>outfile</tt>.
454   * @param infile Input file
455   * @param outfile Output file
456   * @throws IOException if there is an error
457   * @since 2.2
458   */
459  public static void decodeFileToFile( final String infile, final String outfile ) throws IOException {
460    byte[] decoded = decodeFromFile( infile );
461    OutputStream out = null;
462    try{
463      out = new BufferedOutputStream(new FileOutputStream( outfile ) );
464      out.write( decoded );
465    }   // end try
466    catch( IOException e ) {
467      throw e; // Catch and release to execute finally{}
468    }   // end catch
469    finally {
470      try { out.close(); }
471      catch( Exception ex ){}
472    }   // end finally
473  }   // end decodeFileToFile
474}