001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * https://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 018package org.apache.commons.codec.net; 019 020import java.io.UnsupportedEncodingException; 021import java.nio.charset.Charset; 022import java.nio.charset.StandardCharsets; 023import java.nio.charset.UnsupportedCharsetException; 024import java.util.BitSet; 025 026import org.apache.commons.codec.DecoderException; 027import org.apache.commons.codec.EncoderException; 028import org.apache.commons.codec.StringDecoder; 029import org.apache.commons.codec.StringEncoder; 030 031/** 032 * Similar to the Quoted-Printable content-transfer-encoding defined in 033 * <a href="https://www.ietf.org/rfc/rfc1521.txt">RFC 1521</a> and designed to allow text containing mostly ASCII 034 * characters to be decipherable on an ASCII terminal without decoding. 035 * <p> 036 * <a href="https://www.ietf.org/rfc/rfc1522.txt">RFC 1522</a> describes techniques to allow the encoding of non-ASCII 037 * text in various portions of a RFC 822 [2] message header, in a manner which is unlikely to confuse existing message 038 * handling software. 039 * </p> 040 * <p> 041 * This class is conditionally thread-safe. 042 * The instance field for encoding blanks is mutable {@link #setEncodeBlanks(boolean)} 043 * but is not volatile, and accesses are not synchronized. 044 * If an instance of the class is shared between threads, the caller needs to ensure that suitable synchronization 045 * is used to ensure safe publication of the value between threads, and must not invoke 046 * {@link #setEncodeBlanks(boolean)} after initial setup. 047 * </p> 048 * 049 * @see <a href="https://www.ietf.org/rfc/rfc1522.txt">MIME (Multipurpose Internet Mail Extensions) Part Two: Message 050 * Header Extensions for Non-ASCII Text</a> 051 * 052 * @since 1.3 053 */ 054public class QCodec extends RFC1522Codec implements StringEncoder, StringDecoder { 055 056 /** 057 * BitSet of printable characters as defined in RFC 1522. 058 */ 059 private static final BitSet PRINTABLE_CHARS = new BitSet(256); 060 061 // Static initializer for printable chars collection 062 static { 063 // alpha characters 064 PRINTABLE_CHARS.set(' '); 065 PRINTABLE_CHARS.set('!'); 066 PRINTABLE_CHARS.set('"'); 067 PRINTABLE_CHARS.set('#'); 068 PRINTABLE_CHARS.set('$'); 069 PRINTABLE_CHARS.set('%'); 070 PRINTABLE_CHARS.set('&'); 071 PRINTABLE_CHARS.set('\''); 072 PRINTABLE_CHARS.set('('); 073 PRINTABLE_CHARS.set(')'); 074 PRINTABLE_CHARS.set('*'); 075 PRINTABLE_CHARS.set('+'); 076 PRINTABLE_CHARS.set(','); 077 PRINTABLE_CHARS.set('-'); 078 PRINTABLE_CHARS.set('.'); 079 PRINTABLE_CHARS.set('/'); 080 for (int i = '0'; i <= '9'; i++) { 081 PRINTABLE_CHARS.set(i); 082 } 083 PRINTABLE_CHARS.set(':'); 084 PRINTABLE_CHARS.set(';'); 085 PRINTABLE_CHARS.set('<'); 086 PRINTABLE_CHARS.set('>'); 087 PRINTABLE_CHARS.set('@'); 088 for (int i = 'A'; i <= 'Z'; i++) { 089 PRINTABLE_CHARS.set(i); 090 } 091 PRINTABLE_CHARS.set('['); 092 PRINTABLE_CHARS.set('\\'); 093 PRINTABLE_CHARS.set(']'); 094 PRINTABLE_CHARS.set('^'); 095 PRINTABLE_CHARS.set('`'); 096 for (int i = 'a'; i <= 'z'; i++) { 097 PRINTABLE_CHARS.set(i); 098 } 099 PRINTABLE_CHARS.set('{'); 100 PRINTABLE_CHARS.set('|'); 101 PRINTABLE_CHARS.set('}'); 102 PRINTABLE_CHARS.set('~'); 103 } 104 105 private static final byte UNDERSCORE = 95; 106 107 private boolean encodeBlanks; 108 109 /** 110 * Constructs a new instance. 111 */ 112 public QCodec() { 113 this(StandardCharsets.UTF_8); 114 } 115 116 /** 117 * Constructs a new instance for the selection of a default Charset. 118 * 119 * @param charset 120 * the default string Charset to use. 121 * 122 * @see Charset 123 * @since 1.7 124 */ 125 public QCodec(final Charset charset) { 126 super(charset); 127 } 128 129 /** 130 * Constructs a new instance for the selection of a default Charset. 131 * 132 * @param charsetName 133 * the Charset to use. 134 * @throws java.nio.charset.UnsupportedCharsetException 135 * Thrown if the named Charset is unavailable. 136 * @since 1.7 throws UnsupportedCharsetException if the named Charset is unavailable 137 * @see Charset 138 */ 139 public QCodec(final String charsetName) { 140 this(Charset.forName(charsetName)); 141 } 142 143 /** 144 * Decodes a quoted-printable object into its original form. Escaped characters are converted back to their original 145 * representation. 146 * 147 * @param obj 148 * quoted-printable object to convert into its original form. 149 * @return original object. 150 * @throws DecoderException 151 * Thrown if the argument is not a {@code String}. Thrown if a failure condition is encountered 152 * during the decode process. 153 */ 154 @Override 155 public Object decode(final Object obj) throws DecoderException { 156 if (obj == null) { 157 return null; 158 } 159 if (obj instanceof String) { 160 return decode((String) obj); 161 } 162 throw new DecoderException("Objects of type " + obj.getClass().getName() + " cannot be decoded using Q codec"); 163 } 164 165 /** 166 * Decodes a quoted-printable string into its original form. Escaped characters are converted back to their original 167 * representation. 168 * 169 * <p> 170 * Uses {@link QuotedPrintableCodec#decodeQuotedPrintable(byte[])} to decode the encoded text. Since 1.23.0, unescaped CR and LF bytes in malformed 171 * encoded words are preserved rather than discarded, and {@code =CR} without a following LF is rejected. This lenient handling does not make such 172 * encoded words valid under RFC 2047. 173 * </p> 174 * 175 * @param str 176 * quoted-printable string to convert into its original form. 177 * @return original string. 178 * @throws DecoderException 179 * Thrown if a failure condition is encountered during the decoding process. 180 */ 181 @Override 182 public String decode(final String str) throws DecoderException { 183 try { 184 return decodeText(str); 185 } catch (final UnsupportedEncodingException e) { 186 throw new DecoderException(e.getMessage(), e); 187 } 188 } 189 190 @Override 191 protected byte[] doDecoding(final byte[] bytes) throws DecoderException { 192 if (bytes == null) { 193 return null; 194 } 195 boolean hasUnderscores = false; 196 for (final byte b : bytes) { 197 if (b == UNDERSCORE) { 198 hasUnderscores = true; 199 break; 200 } 201 } 202 if (hasUnderscores) { 203 final byte[] tmp = new byte[bytes.length]; 204 for (int i = 0; i < bytes.length; i++) { 205 final byte b = bytes[i]; 206 if (b != UNDERSCORE) { 207 tmp[i] = b; 208 } else { 209 tmp[i] = Utils.SPACE; 210 } 211 } 212 return QuotedPrintableCodec.decodeQuotedPrintable(tmp); 213 } 214 return QuotedPrintableCodec.decodeQuotedPrintable(bytes); 215 } 216 217 @Override 218 protected byte[] doEncoding(final byte[] bytes) { 219 if (bytes == null) { 220 return null; 221 } 222 final byte[] data = QuotedPrintableCodec.encodeQuotedPrintable(PRINTABLE_CHARS, bytes); 223 if (this.encodeBlanks) { 224 for (int i = 0; i < data.length; i++) { 225 if (data[i] == Utils.SPACE) { 226 data[i] = UNDERSCORE; 227 } 228 } 229 } 230 return data; 231 } 232 233 /** 234 * Encodes an object into its quoted-printable form using the default Charset. Unsafe characters are escaped. 235 * 236 * @param obj 237 * object to convert to quoted-printable form. 238 * @return quoted-printable object. 239 * @throws EncoderException 240 * Thrown if a failure condition is encountered during the encoding process. 241 */ 242 @Override 243 public Object encode(final Object obj) throws EncoderException { 244 if (obj == null) { 245 return null; 246 } 247 if (obj instanceof String) { 248 return encode((String) obj); 249 } 250 throw new EncoderException("Objects of type " + obj.getClass().getName() + " cannot be encoded using Q codec"); 251 } 252 253 /** 254 * Encodes a string into its quoted-printable form using the default Charset. Unsafe characters are escaped. 255 * 256 * @param sourceStr 257 * string to convert to quoted-printable form. 258 * @return quoted-printable string. 259 * @throws EncoderException 260 * Thrown if a failure condition is encountered during the encoding process. 261 */ 262 @Override 263 public String encode(final String sourceStr) throws EncoderException { 264 return encode(sourceStr, getCharset()); 265 } 266 267 /** 268 * Encodes a string into its quoted-printable form using the specified Charset. Unsafe characters are escaped. 269 * 270 * @param sourceStr 271 * string to convert to quoted-printable form. 272 * @param sourceCharset 273 * the Charset for sourceStr. 274 * @return quoted-printable string. 275 * @throws EncoderException 276 * Thrown if a failure condition is encountered during the encoding process. 277 * @since 1.7 278 */ 279 public String encode(final String sourceStr, final Charset sourceCharset) throws EncoderException { 280 return encodeText(sourceStr, sourceCharset); 281 } 282 283 /** 284 * Encodes a string into its quoted-printable form using the specified Charset. Unsafe characters are escaped. 285 * 286 * @param sourceStr 287 * string to convert to quoted-printable form. 288 * @param sourceCharset 289 * the Charset for sourceStr. 290 * @return quoted-printable string. 291 * @throws EncoderException 292 * Thrown if a failure condition is encountered during the encoding process. 293 */ 294 public String encode(final String sourceStr, final String sourceCharset) throws EncoderException { 295 try { 296 return encodeText(sourceStr, sourceCharset); 297 } catch (final UnsupportedCharsetException e) { 298 throw new EncoderException(e.getMessage(), e); 299 } 300 } 301 302 @Override 303 protected String getEncoding() { 304 return "Q"; 305 } 306 307 /** 308 * Tests whether the optional transformation of SPACE characters is to be used. 309 * 310 * @return {@code true} if SPACE characters are to be transformed, {@code false} otherwise. 311 */ 312 public boolean isEncodeBlanks() { 313 return this.encodeBlanks; 314 } 315 316 /** 317 * Sets whether the optional transformation of SPACE characters is to be used. 318 * 319 * @param b 320 * {@code true} if SPACE characters are to be transformed, {@code false} otherwise. 321 */ 322 public void setEncodeBlanks(final boolean b) { 323 this.encodeBlanks = b; 324 } 325}