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}