001 /* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. 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, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019 020 package org.apache.geronimo.mail.util; 021 022 import java.io.IOException; 023 import java.io.InputStream; 024 import java.io.FilterInputStream; 025 026 /** 027 * An implementation of a FilterInputStream that decodes the 028 * stream data in BASE64 encoding format. This version does the 029 * decoding "on the fly" rather than decoding a single block of 030 * data. Since this version is intended for use by the MimeUtilty class, 031 * it also handles line breaks in the encoded data. 032 */ 033 public class Base64DecoderStream extends FilterInputStream { 034 035 static protected final String MAIL_BASE64_IGNOREERRORS = "mail.mime.base64.ignoreerrors"; 036 037 // number of decodeable units we'll try to process at one time. We'll attempt to read that much 038 // data from the input stream and decode in blocks. 039 static protected final int BUFFERED_UNITS = 2000; 040 041 // our decoder for processing the data 042 protected Base64Encoder decoder = new Base64Encoder(); 043 044 // can be overridden by a system property. 045 protected boolean ignoreErrors = false; 046 047 // buffer for reading in chars for decoding (which can support larger bulk reads) 048 protected byte[] encodedChars = new byte[BUFFERED_UNITS * 4]; 049 // a buffer for one decoding unit's worth of data (3 bytes). This is the minimum amount we 050 // can read at one time. 051 protected byte[] decodedChars = new byte[BUFFERED_UNITS * 3]; 052 // count of characters in the buffer 053 protected int decodedCount = 0; 054 // index of the next decoded character 055 protected int decodedIndex = 0; 056 057 058 public Base64DecoderStream(InputStream in) { 059 super(in); 060 // make sure we get the ignore errors flag 061 ignoreErrors = SessionUtil.getBooleanProperty(MAIL_BASE64_IGNOREERRORS, false); 062 } 063 064 /** 065 * Test for the existance of decoded characters in our buffer 066 * of decoded data. 067 * 068 * @return True if we currently have buffered characters. 069 */ 070 private boolean dataAvailable() { 071 return decodedCount != 0; 072 } 073 074 /** 075 * Get the next buffered decoded character. 076 * 077 * @return The next decoded character in the buffer. 078 */ 079 private byte getBufferedChar() { 080 decodedCount--; 081 return decodedChars[decodedIndex++]; 082 } 083 084 /** 085 * Decode a requested number of bytes of data into a buffer. 086 * 087 * @return true if we were able to obtain more data, false otherwise. 088 */ 089 private boolean decodeStreamData() throws IOException { 090 decodedIndex = 0; 091 092 // fill up a data buffer with input data 093 int readCharacters = fillEncodedBuffer(); 094 095 if (readCharacters > 0) { 096 decodedCount = decoder.decode(encodedChars, 0, readCharacters, decodedChars); 097 return true; 098 } 099 return false; 100 } 101 102 103 /** 104 * Retrieve a single byte from the decoded characters buffer. 105 * 106 * @return The decoded character or -1 if there was an EOF condition. 107 */ 108 private int getByte() throws IOException { 109 if (!dataAvailable()) { 110 if (!decodeStreamData()) { 111 return -1; 112 } 113 } 114 decodedCount--; 115 // we need to ensure this doesn't get sign extended 116 return decodedChars[decodedIndex++] & 0xff; 117 } 118 119 private int getBytes(byte[] data, int offset, int length) throws IOException { 120 121 int readCharacters = 0; 122 while (length > 0) { 123 // need data? Try to get some 124 if (!dataAvailable()) { 125 // if we can't get this, return a count of how much we did get (which may be -1). 126 if (!decodeStreamData()) { 127 return readCharacters > 0 ? readCharacters : -1; 128 } 129 } 130 131 // now copy some of the data from the decoded buffer to the target buffer 132 int copyCount = Math.min(decodedCount, length); 133 System.arraycopy(decodedChars, decodedIndex, data, offset, copyCount); 134 decodedIndex += copyCount; 135 decodedCount -= copyCount; 136 offset += copyCount; 137 length -= copyCount; 138 readCharacters += copyCount; 139 } 140 return readCharacters; 141 } 142 143 144 /** 145 * Fill our buffer of input characters for decoding from the 146 * stream. This will attempt read a full buffer, but will 147 * terminate on an EOF or read error. This will filter out 148 * non-Base64 encoding chars and will only return a valid 149 * multiple of 4 number of bytes. 150 * 151 * @return The count of characters read. 152 */ 153 private int fillEncodedBuffer() throws IOException 154 { 155 int readCharacters = 0; 156 157 while (true) { 158 // get the next character from the stream 159 int ch = in.read(); 160 // did we hit an EOF condition? 161 if (ch == -1) { 162 // now check to see if this is normal, or potentially an error 163 // if we didn't get characters as a multiple of 4, we may need to complain about this. 164 if ((readCharacters % 4) != 0) { 165 // the error checking can be turned off...normally it isn't 166 if (!ignoreErrors) { 167 throw new IOException("Base64 encoding error, data truncated"); 168 } 169 // we're ignoring errors, so round down to a multiple and return that. 170 return (readCharacters / 4) * 4; 171 } 172 // return the count. 173 return readCharacters; 174 } 175 // if this character is valid in a Base64 stream, copy it to the buffer. 176 else if (decoder.isValidBase64(ch)) { 177 encodedChars[readCharacters++] = (byte)ch; 178 // if we've filled up the buffer, time to quit. 179 if (readCharacters >= encodedChars.length) { 180 return readCharacters; 181 } 182 } 183 184 // we're filtering out whitespace and CRLF characters, so just ignore these 185 } 186 } 187 188 189 // in order to function as a filter, these streams need to override the different 190 // read() signature. 191 192 public int read() throws IOException 193 { 194 return getByte(); 195 } 196 197 198 public int read(byte [] buffer, int offset, int length) throws IOException { 199 return getBytes(buffer, offset, length); 200 } 201 202 203 public boolean markSupported() { 204 return false; 205 } 206 207 208 public int available() throws IOException { 209 return ((in.available() / 4) * 3) + decodedCount; 210 } 211 }