001/**
002 * Java Web Archive Toolkit - Software to read and validate ARC, WARC
003 * and GZip files. (http://jwat.org/)
004 * Copyright 2011-2012 Netarkivet.dk (http://netarkivet.dk/)
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License");
007 * you may not use this file except in compliance with the License.
008 * 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, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.jwat.common;
019
020import java.io.ByteArrayOutputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.nio.ByteBuffer;
024import java.nio.CharBuffer;
025import java.nio.charset.Charset;
026import java.nio.charset.CharsetDecoder;
027import java.nio.charset.CoderMalfunctionError;
028import java.nio.charset.CoderResult;
029import java.nio.charset.CodingErrorAction;
030import java.nio.charset.IllegalCharsetNameException;
031import java.nio.charset.UnsupportedCharsetException;
032
033/**
034 * Class used to decode and validate an Encoded-Word string as specified
035 * in RFC 2047.
036 *
037 * Examples:
038 * =?US-ASCII?Q?Keith_Moore?=
039 * =?ISO-8859-1?Q?Keld_J=F8rn_Simonsen?=
040 * =?ISO-8859-1?Q?Andr=E9?=
041 * =?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=
042 * =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=
043 *
044 * @author nicl
045 */
046public class EncodedWords {
047
048    /** Looking for an equal sign state. */
049    protected static final int S_START_EQ = 0;
050    /** Looking for a question mark state. */
051    protected static final int S_START_QM = 1;
052    /** Reading the charset state. */
053    protected static final int S_CHARSET = 2;
054    /** Reading the encoding state. */
055    protected static final int S_ENCODING = 3;
056    /** Reading the encoded words state. */
057    protected static final int S_ENCODED_WORDS = 4;
058    /** Looking for the last equal sign state. */
059    protected static final int S_END_EQ = 5;
060
061    /** Base64 encoding id. */
062    public static final int ENC_BASE64 = 1;
063    /** Quoted-Printable encoding id. */
064    public static final int ENC_QUOTEDPRINTABLE = 2;
065
066    /** Control character characteristic. */
067    protected static final int CC_CONTROL = 1;
068    /** Separator character characteristic. */
069    protected static final int CC_SEPARATOR_WS = 2;
070
071    /** rfc2616 separator minus space and tab. */
072    protected static final String separators = "()<>@,;:\\\"/[]?={} \t";
073
074    /** Table of separator and control characters. */
075    protected static final byte[] charCharacteristicsTab = new byte[256];
076
077    /*
078     * Populate table with separator and control characters.
079     */
080    static {
081        for (int i=0; i<separators.length(); ++i) {
082            charCharacteristicsTab[separators.charAt(i)] = CC_SEPARATOR_WS;
083        }
084        for (int i=0; i<32; ++i) {
085            charCharacteristicsTab[i] = CC_CONTROL;
086        }
087    }
088
089    /** Indicates whether the charset is valid or not. */
090    public boolean bValidCharset;
091
092    /** Raw charset name. */
093    public String charsetStr;
094
095    /** Identified encoding, 0 indicates an unknown encoding. */
096    public int encoding;
097
098    /** Raw encoding name. */
099    public String encodingStr;
100
101    /** Raw encoded text string */
102    public String encoded_text;
103
104    /** Indicates whether there were errors during character conversion. */
105    public boolean bConversionError;
106
107    /** Decoded text, after decoding and character conversion. */
108    public String decoded_text;
109
110    /** Raw bytes parsed, usable to re-insert original string after validation. */
111    public byte[] line;
112
113    /** Indicates where the encoded words could be validated and decoded. */
114    public boolean bIsValid = false;
115
116    /**
117     * Attempt to validate and decode an encoded-word from the input stream.
118     * In case of failure the already parsed raw data is available for further
119     * processing.
120     * @param in input stream containing a possible encoded-word
121     * @param bParseEqQm true if we should look for leading "=?", false if not
122     * @return result of the validation and decoding
123     * @throws IOException if an I/O error occurs during parsing
124     */
125    public static EncodedWords parseEncodedWords(InputStream in, boolean bParseEqQm) throws IOException {
126        EncodedWords ew = new EncodedWords();
127        ByteArrayOutputStream lineOut = new ByteArrayOutputStream();
128        StringBuffer sb = new StringBuffer();
129        Charset charset = null;
130        int state;
131        if (bParseEqQm) {
132            state = S_START_EQ;
133        } else {
134            state = S_CHARSET;
135        }
136        int c;
137        boolean bLoop = true;
138        while (bLoop) {
139            c = in.read();
140            if (c != -1) {
141                lineOut.write(c);
142            }
143            switch (state) {
144            case S_START_EQ:
145                if (c == '=') {
146                    state = S_START_QM;
147                } else {
148                    // -1 etc.
149                    bLoop = false;
150                }
151                break;
152            case S_START_QM:
153                if (c == '?') {
154                    state = S_CHARSET;
155                } else {
156                    // -1 etc.
157                    bLoop = false;
158                }
159                break;
160            case S_CHARSET:
161                switch (c) {
162                case -1:
163                case '\r':
164                case '\n':
165                    bLoop = false;
166                    break;
167                case '?':
168                    ew.charsetStr = sb.toString().toUpperCase();
169                    sb.setLength(0);
170                    if (ew.charsetStr.length() > 0) {
171                        try {
172                            charset = Charset.forName(ew.charsetStr);
173                            ew.bValidCharset = true;
174                        } catch (IllegalCharsetNameException e) {
175                            //System.err.println("ERROR IllegalCharsetNameException Charset: " + ew.charsetStr + " => " + e.getMessage());
176                            charset = Charset.defaultCharset();
177                            ew.bValidCharset = false;
178                        } catch (UnsupportedCharsetException e) {
179                            // System.err.println("ERROR UnsupportedCharsetException Charset: " + ew.charsetStr + " => " + e.getMessage());
180                            charset = Charset.defaultCharset();
181                            ew.bValidCharset = false;
182                        }
183                        state = S_ENCODING;
184                    } else {
185                        bLoop = false;
186                    }
187                    break;
188                default:
189                    if (charCharacteristicsTab[c] == 0 && c < 127) {
190                        sb.append((char) c);
191                    } else {
192                        bLoop = false;
193                    }
194                    break;
195                }
196                break;
197            case S_ENCODING:
198                switch (c) {
199                case -1:
200                case '\r':
201                case '\n':
202                    bLoop = false;
203                    break;
204                case '?':
205                    ew.encodingStr = sb.toString().toUpperCase();
206                    sb.setLength(0);
207                    if (ew.encodingStr.length() > 0) {
208                        if ("b".equalsIgnoreCase(ew.encodingStr)) {
209                            ew.encoding = ENC_BASE64;
210                        } else if ("q".equalsIgnoreCase(ew.encodingStr)) {
211                            ew.encoding = ENC_QUOTEDPRINTABLE;
212                        }
213                        state = S_ENCODED_WORDS;
214                    } else {
215                        bLoop = false;
216                    }
217                    break;
218                default:
219                    if (charCharacteristicsTab[c] == 0 && c < 127) {
220                        sb.append((char) c);
221                    } else {
222                        bLoop = false;
223                    }
224                    break;
225                }
226                break;
227            case S_ENCODED_WORDS:
228                switch (c) {
229                case -1:
230                case '\r':
231                case '\n':
232                    bLoop = false;
233                    break;
234                case '?':
235                    ew.encoded_text = sb.toString();
236                    sb.setLength(0);
237                    byte[] decoded = null;
238                    if (ew.encoding == ENC_BASE64) {
239                        decoded = Base64.decodeToArray(ew.encoded_text, true);
240                    } else if (ew.encoding == ENC_QUOTEDPRINTABLE) {
241                        decoded = QuotedPrintable.decode(ew.encoded_text);
242                    } else {
243                        // debug
244                        //System.out.println("Encoding: " + ew.encodingStr);
245                    }
246                    if (decoded != null) {
247                        ByteBuffer bb = ByteBuffer.wrap(decoded);
248                        CharBuffer cb = CharBuffer.allocate(bb.capacity());
249                        CharsetDecoder decoder = charset.newDecoder();
250                        decoder.onMalformedInput(CodingErrorAction.REPORT);
251                        decoder.onUnmappableCharacter(CodingErrorAction.REPORT);
252                        try {
253                            boolean bDecodeLoop = true;
254                            while (bDecodeLoop) {
255                                CoderResult result = decoder.decode(bb, cb, true);
256                                sb.append(cb.array(), cb.arrayOffset(), cb.position());
257                                cb.clear();
258                                if (result == CoderResult.UNDERFLOW) {
259                                       bDecodeLoop = false;
260                                } else if (result.isError()) {
261                                    bb.position(Math.min(bb.position() + result.length(), bb.limit()));
262                                    sb.append('?');
263                                    ew.bConversionError = true;
264                                }
265                            }
266                        } catch (CoderMalfunctionError e) {
267                        }
268                        ew.decoded_text = sb.toString();
269                    }
270                    state = S_END_EQ;
271                    break;
272                default:
273                    if (c > 32 && c < 127) {
274                        sb.append((char) c);
275                    } else {
276                        bLoop = false;
277                    }
278                    break;
279                }
280                break;
281            case S_END_EQ:
282                if (c == -1) {
283                    bLoop = false;
284                } else if (c == '=') {
285                    ew.bIsValid = true;
286                    bLoop = false;
287                }
288                break;
289            }
290        }
291        ew.line = lineOut.toByteArray();
292        ew.bIsValid = ew.bIsValid & ew.bValidCharset & ew.encoding != 0 && ew.decoded_text != null;
293        return ew;
294    }
295
296}