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