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