1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19
20 package org.apache.geronimo.mail.util;
21
22 import java.io.ByteArrayOutputStream;
23 import java.io.FilterInputStream;
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.io.UnsupportedEncodingException;
27
28 /**
29 * An implementation of a FilterOutputStream that decodes the
30 * stream data in UU encoding format. This version does the
31 * decoding "on the fly" rather than decoding a single block of
32 * data. Since this version is intended for use by the MimeUtilty class,
33 * it also handles line breaks in the encoded data.
34 */
35 public class UUDecoderStream extends FilterInputStream {
36 // maximum number of chars that can appear in a single line
37 protected static final int MAX_CHARS_PER_LINE = 45;
38
39 // our decoder for processing the data
40 protected UUEncoder decoder = new UUEncoder();
41
42 // a buffer for one decoding unit's worth of data (45 bytes).
43 protected byte[] decodedChars;
44 // count of characters in the buffer
45 protected int decodedCount = 0;
46 // index of the next decoded character
47 protected int decodedIndex = 0;
48
49 // indicates whether we've already processed the "begin" prefix.
50 protected boolean beginRead = false;
51
52
53 public UUDecoderStream(InputStream in) {
54 super(in);
55 }
56
57
58 /**
59 * Test for the existance of decoded characters in our buffer
60 * of decoded data.
61 *
62 * @return True if we currently have buffered characters.
63 */
64 private boolean dataAvailable() {
65 return decodedCount != 0;
66 }
67
68 /**
69 * Get the next buffered decoded character.
70 *
71 * @return The next decoded character in the buffer.
72 */
73 private byte getBufferedChar() {
74 decodedCount--;
75 return decodedChars[decodedIndex++];
76 }
77
78 /**
79 * Decode a requested number of bytes of data into a buffer.
80 *
81 * @return true if we were able to obtain more data, false otherwise.
82 */
83 private boolean decodeStreamData() throws IOException {
84 decodedIndex = 0;
85
86 // fill up a data buffer with input data
87 return fillEncodedBuffer() != -1;
88 }
89
90
91 /**
92 * Retrieve a single byte from the decoded characters buffer.
93 *
94 * @return The decoded character or -1 if there was an EOF condition.
95 */
96 private int getByte() throws IOException {
97 if (!dataAvailable()) {
98 if (!decodeStreamData()) {
99 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