001// Copyright (C) 1999-2002 by Jason Hunter <jhunter_AT_acm_DOT_org>.
002// All rights reserved.  Use of this class is limited.
003// Please see the LICENSE for more information.
004
005package com.oreilly.servlet;
006
007import java.io.*;
008
009/** 
010 * A class to decode Base64 streams and strings.  
011 * See RFC 1521 section 5.2 for details of the Base64 algorithm.
012 * <p>
013 * This class can be used for decoding strings:
014 * <blockquote><pre>
015 * String encoded = "d2VibWFzdGVyOnRyeTJndWVTUw";
016 * String decoded = Base64Decoder.decode(encoded);
017 * </pre></blockquote>
018 * or for decoding streams:
019 * <blockquote><pre>
020 * InputStream in = new Base64Decoder(System.in);
021 * </pre></blockquote>
022 *
023 * @author <b>Jason Hunter</b>, Copyright &#169; 2000
024 * @version 1.1, 2002/11/01, added decodeToBytes() to better handle binary
025 *                           data (thanks to Sean Graham)
026 * @version 1.0, 2000/06/11
027 */
028public class Base64Decoder extends FilterInputStream {
029
030  private static final char[] chars = {
031    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
032    'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
033    'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
034    'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
035    'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x',
036    'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7',
037    '8', '9', '+', '/'
038  };
039
040  // A mapping between char values and six-bit integers
041  private static final int[] ints = new int[128];
042  static {
043    for (int i = 0; i < 64; i++) {
044      ints[chars[i]] = i;
045    }
046  }
047
048  private int charCount;
049  private int carryOver;
050
051  /**
052   * Constructs a new Base64 decoder that reads input from the given
053   * InputStream.
054   *
055   * @param in the input stream
056   */
057  public Base64Decoder(InputStream in) {
058    super(in);
059  }
060
061  /**
062   * Returns the next decoded character from the stream, or -1 if
063   * end of stream was reached.
064   *
065   * @return  the decoded character, or -1 if the end of the
066   *      input stream is reached
067   * @exception IOException if an I/O error occurs
068   */
069  public int read() throws IOException {
070    // Read the next non-whitespace character
071    int x;
072    do {
073      x = in.read();
074      if (x == -1) {
075        return -1;
076      }
077    } while (Character.isWhitespace((char)x));
078    charCount++;
079
080    // The '=' sign is just padding
081    if (x == '=') {
082      return -1;  // effective end of stream
083    }
084
085    // Convert from raw form to 6-bit form
086    x = ints[x];
087
088    // Calculate which character we're decoding now
089    int mode = (charCount - 1) % 4;
090
091    // First char save all six bits, go for another
092    if (mode == 0) {
093      carryOver = x & 63;
094      return read();
095    }
096    // Second char use previous six bits and first two new bits,
097    // save last four bits
098    else if (mode == 1) {
099      int decoded = ((carryOver << 2) + (x >> 4)) & 255;
100      carryOver = x & 15;
101      return decoded;
102    }
103    // Third char use previous four bits and first four new bits,
104    // save last two bits
105    else if (mode == 2) {
106      int decoded = ((carryOver << 4) + (x >> 2)) & 255;
107      carryOver = x & 3;
108      return decoded;
109    }
110    // Fourth char use previous two bits and all six new bits
111    else if (mode == 3) {
112      int decoded = ((carryOver << 6) + x) & 255;
113      return decoded;
114    }
115    return -1;  // can't actually reach this line
116  }
117
118  /**
119   * Reads decoded data into an array of bytes and returns the actual 
120   * number of bytes read, or -1 if end of stream was reached.
121   *
122   * @param buf the buffer into which the data is read
123   * @param off the start offset of the data
124   * @param len the maximum number of bytes to read
125   * @return  the actual number of bytes read, or -1 if the end of the
126   *      input stream is reached
127   * @exception IOException if an I/O error occurs
128   */
129  public int read(byte[] buf, int off, int len) throws IOException {
130    if (buf.length < (len + off - 1)) {
131      throw new IOException("The input buffer is too small: " + len + 
132       " bytes requested starting at offset " + off + " while the buffer " +
133       " is only " + buf.length + " bytes long.");
134    }
135
136    // This could of course be optimized
137    int i;
138    for (i = 0; i < len; i++) {
139      int x = read();
140      if (x == -1 && i == 0) {  // an immediate -1 returns -1
141        return -1;
142      }
143      else if (x == -1) {       // a later -1 returns the chars read so far
144        break;
145      }
146      buf[off + i] = (byte) x;
147    }
148    return i;
149  }
150
151  /**
152   * Returns the decoded form of the given encoded string, as a String.
153   * Note that not all binary data can be represented as a String, so this
154   * method should only be used for encoded String data.  Use decodeToBytes()
155   * otherwise.
156   *
157   * @param encoded the string to decode
158   * @return the decoded form of the encoded string
159   */
160  public static String decode(String encoded) {
161    return new String(decodeToBytes(encoded));
162  }
163
164  /**
165   * Returns the decoded form of the given encoded string, as bytes.
166   *
167   * @param encoded the string to decode
168   * @return the decoded form of the encoded string
169   */
170  public static byte[] decodeToBytes(String encoded) {
171    byte[] bytes = null;
172    try {
173      bytes = encoded.getBytes("8859_1");
174    }
175    catch (UnsupportedEncodingException ignored) { }
176
177    Base64Decoder in = new Base64Decoder(
178                       new ByteArrayInputStream(bytes));
179    
180    ByteArrayOutputStream out = 
181      new ByteArrayOutputStream((int) (bytes.length * 0.67));
182
183    try {
184      byte[] buf = new byte[4 * 1024];  // 4K buffer
185      int bytesRead;
186      while ((bytesRead = in.read(buf)) != -1) {
187        out.write(buf, 0, bytesRead);
188      }
189      out.close();
190
191      return out.toByteArray();
192    }
193    catch (IOException ignored) { return null; }
194  }
195
196  public static void main(String[] args) throws Exception {
197    if (args.length != 1) {
198      System.err.println("Usage: java Base64Decoder fileToDecode");
199      return;
200    }
201
202    Base64Decoder decoder = null;
203    try {
204      decoder = new Base64Decoder(
205                new BufferedInputStream(
206                new FileInputStream(args[0])));
207      byte[] buf = new byte[4 * 1024];  // 4K buffer
208      int bytesRead;
209      while ((bytesRead = decoder.read(buf)) != -1) {
210        System.out.write(buf, 0, bytesRead);
211      }
212    }
213    finally {
214      if (decoder != null) decoder.close();
215    }
216  }
217}