001 /** 002 * 003 * Copyright 2003-2004 The Apache Software Foundation 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018 package org.apache.geronimo.mail.util; 019 020 import java.io.ByteArrayOutputStream; 021 import java.io.FilterInputStream; 022 import java.io.IOException; 023 import java.io.InputStream; 024 import java.io.UnsupportedEncodingException; 025 026 /** 027 * An implementation of a FilterOutputStream that decodes the 028 * stream data in UU 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 UUDecoderStream extends FilterInputStream { 034 // maximum number of chars that can appear in a single line 035 protected static final int MAX_CHARS_PER_LINE = 45; 036 037 // our decoder for processing the data 038 protected UUEncoder decoder = new UUEncoder(); 039 040 // a buffer for one decoding unit's worth of data (45 bytes). 041 protected byte[] decodedChars; 042 // count of characters in the buffer 043 protected int decodedCount = 0; 044 // index of the next decoded character 045 protected int decodedIndex = 0; 046 047 // indicates whether we've already processed the "begin" prefix. 048 protected boolean beginRead = false; 049 050 051 public UUDecoderStream(InputStream in) { 052 super(in); 053 } 054 055 056 /** 057 * Test for the existance of decoded characters in our buffer 058 * of decoded data. 059 * 060 * @return True if we currently have buffered characters. 061 */ 062 private boolean dataAvailable() { 063 return decodedCount != 0; 064 } 065 066 /** 067 * Get the next buffered decoded character. 068 * 069 * @return The next decoded character in the buffer. 070 */ 071 private byte getBufferedChar() { 072 decodedCount--; 073 return decodedChars[decodedIndex++]; 074 } 075 076 /** 077 * Decode a requested number of bytes of data into a buffer. 078 * 079 * @return true if we were able to obtain more data, false otherwise. 080 */ 081 private boolean decodeStreamData() throws IOException { 082 decodedIndex = 0; 083 084 // fill up a data buffer with input data 085 return fillEncodedBuffer() != -1; 086 } 087 088 089 /** 090 * Retrieve a single byte from the decoded characters buffer. 091 * 092 * @return The decoded character or -1 if there was an EOF condition. 093 */ 094 private int getByte() throws IOException { 095 if (!dataAvailable()) { 096 if (!decodeStreamData()) { 097 return -1; 098 } 099 } 100 decodedCount--; 101 return decodedChars[decodedIndex++]; 102 } 103 104 private int getBytes(byte[] data, int offset, int length) throws IOException { 105 106 int readCharacters = 0; 107 while (length > 0) { 108 // need data? Try to get some 109 if (!dataAvailable()) { 110 // if we can't get this, return a count of how much we did get (which may be -1). 111 if (!decodeStreamData()) { 112 return readCharacters > 0 ? readCharacters : -1; 113 } 114 } 115 116 // now copy some of the data from the decoded buffer to the target buffer 117 int copyCount = Math.min(decodedCount, length); 118 System.arraycopy(decodedChars, decodedIndex, data, offset, copyCount); 119 decodedIndex += copyCount; 120 decodedCount -= copyCount; 121 offset += copyCount; 122 length -= copyCount; 123 readCharacters += copyCount; 124 } 125 return readCharacters; 126 } 127 128 /** 129 * Verify that the first line of the buffer is a valid begin 130 * marker. 131 * 132 * @exception IOException 133 */ 134 private void checkBegin() throws IOException { 135 // we only do this the first time we're requested to read from the stream. 136 if (beginRead) { 137 return; 138 } 139 140 // we might have to skip over lines to reach the marker. If we hit the EOF without finding 141 // the begin, that's an error. 142 while (true) { 143 String line = readLine(); 144 if (line == null) { 145 throw new IOException("Missing UUEncode begin command"); 146 } 147 148 // is this our begin? 149 if (line.regionMatches(true, 0, "begin ", 0, 6)) { 150 // This is the droid we're looking for..... 151 beginRead = true; 152 return; 153 } 154 } 155 } 156 157 158 /** 159 * Read a line of data. Returns null if there is an EOF. 160 * 161 * @return The next line read from the stream. Returns null if we 162 * hit the end of the stream. 163 * @exception IOException 164 */ 165 protected String readLine() throws IOException { 166 decodedIndex = 0; 167 // get an accumulator for the data 168 StringBuffer buffer = new StringBuffer(); 169 170 // now process a character at a time. 171 int ch = in.read(); 172 while (ch != -1) { 173 // a naked new line completes the line. 174 if (ch == '\n') { 175 break; 176 } 177 // a carriage return by itself is ignored...we're going to assume that this is followed 178 // by a new line because we really don't have the capability of pushing this back . 179 else if (ch == '\r') { 180 ; 181 } 182 else { 183 // add this to our buffer 184 buffer.append((char)ch); 185 } 186 ch = in.read(); 187 } 188 189 // if we didn't get any data at all, return nothing 190 if (ch == -1 && buffer.length() == 0) { 191 return null; 192 } 193 // convert this into a string. 194 return buffer.toString(); 195 } 196 197 198 /** 199 * Fill our buffer of input characters for decoding from the 200 * stream. This will attempt read a full buffer, but will 201 * terminate on an EOF or read error. This will filter out 202 * non-Base64 encoding chars and will only return a valid 203 * multiple of 4 number of bytes. 204 * 205 * @return The count of characters read. 206 */ 207 private int fillEncodedBuffer() throws IOException 208 { 209 checkBegin(); 210 // reset the buffer position 211 decodedIndex = 0; 212 213 while (true) { 214 215 // we read these as character lines. We need to be looking for the "end" marker for the 216 // end of the data. 217 String line = readLine(); 218 219 // this should NOT be happening.... 220 if (line == null) { 221 throw new IOException("Missing end in UUEncoded data"); 222 } 223 224 // Is this the end marker? EOF baby, EOF! 225 if (line.equalsIgnoreCase("end")) { 226 // this indicates we got nuttin' more to do. 227 return -1; 228 } 229 230 ByteArrayOutputStream out = new ByteArrayOutputStream(MAX_CHARS_PER_LINE); 231 232 byte [] lineBytes; 233 try { 234 lineBytes = line.getBytes("US-ASCII"); 235 } catch (UnsupportedEncodingException e) { 236 throw new IOException("Invalid UUEncoding"); 237 } 238 239 // decode this line 240 decodedCount = decoder.decode(lineBytes, 0, lineBytes.length, out); 241 242 // not just a zero-length line? 243 if (decodedCount != 0) { 244 // get the resulting byte array 245 decodedChars = out.toByteArray(); 246 return decodedCount; 247 } 248 } 249 } 250 251 252 // in order to function as a filter, these streams need to override the different 253 // read() signature. 254 255 public int read() throws IOException 256 { 257 return getByte(); 258 } 259 260 261 public int read(byte [] buffer, int offset, int length) throws IOException { 262 return getBytes(buffer, offset, length); 263 } 264 265 266 public boolean markSupported() { 267 return false; 268 } 269 270 271 public int available() throws IOException { 272 return ((in.available() / 4) * 3) + decodedCount; 273 } 274 } 275