View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.codec.net;
19  
20  import static org.junit.jupiter.api.Assertions.assertArrayEquals;
21  import static org.junit.jupiter.api.Assertions.assertEquals;
22  import static org.junit.jupiter.api.Assertions.assertNull;
23  import static org.junit.jupiter.api.Assertions.assertThrows;
24  
25  import java.nio.charset.StandardCharsets;
26  import java.nio.charset.UnsupportedCharsetException;
27  import java.util.stream.Stream;
28  
29  import org.apache.commons.codec.CharEncoding;
30  import org.apache.commons.codec.DecoderException;
31  import org.apache.commons.codec.EncoderException;
32  import org.apache.commons.codec.binary.StringUtils;
33  import org.junit.jupiter.api.Test;
34  import org.junit.jupiter.params.ParameterizedTest;
35  import org.junit.jupiter.params.provider.Arguments;
36  import org.junit.jupiter.params.provider.MethodSource;
37  import org.junit.jupiter.params.provider.ValueSource;
38  
39  /**
40   * Quoted-printable codec test cases
41   */
42  class QuotedPrintableCodecTest {
43  
44      static final int[] SWISS_GERMAN_STUFF_UNICODE = { 0x47, 0x72, 0xFC, 0x65, 0x7A, 0x69, 0x5F, 0x7A, 0xE4, 0x6D, 0xE4 };
45  
46      static final int[] RUSSIAN_STUFF_UNICODE = { 0x412, 0x441, 0x435, 0x43C, 0x5F, 0x43F, 0x440, 0x438, 0x432, 0x435, 0x442 };
47  
48      private static Stream<Arguments> softLineBreakBoundaryCases() {
49          // @formatter:off
50          return Stream.of(
51                  Arguments.of("=\r\n", ""),
52                  Arguments.of("=\r\nA=\r\n", "A"),
53                  Arguments.of("A=\r\n=\r\nB", "AB"),
54                  Arguments.of("A=\r\n\r\nB", "A\r\nB"),
55                  Arguments.of("A\r\n=\r\nB", "A\r\nB"),
56                  Arguments.of("A=0D=0AB", "A\r\nB"));
57          // @formatter:on
58      }
59  
60      private String constructString(final int[] unicodeChars) {
61          final StringBuilder buffer = new StringBuilder();
62          if (unicodeChars != null) {
63              for (final int unicodeChar : unicodeChars) {
64                  buffer.append((char) unicodeChar);
65              }
66          }
67          return buffer.toString();
68      }
69  
70      @Test
71      void testBasicEncodeDecode() throws Exception {
72          final QuotedPrintableCodec qpcodec = new QuotedPrintableCodec();
73          final String plain = "= Hello there =\r\n";
74          final String encoded = qpcodec.encode(plain);
75          assertEquals("=3D Hello there =3D=0D=0A", encoded, "Basic quoted-printable encoding test");
76          assertEquals(plain, qpcodec.decode(encoded), "Basic quoted-printable decoding test");
77      }
78  
79      @Test
80      void testDecodeInvalid() {
81          final QuotedPrintableCodec qpcodec = new QuotedPrintableCodec();
82          assertThrows(DecoderException.class, () -> qpcodec.decode("="));
83          assertThrows(DecoderException.class, () -> qpcodec.decode("=A"));
84          assertThrows(DecoderException.class, () -> qpcodec.decode("=WW"));
85      }
86  
87      @Test
88      void testDecodeObjects() throws Exception {
89          final QuotedPrintableCodec qpcodec = new QuotedPrintableCodec();
90          final String plain = "1+1 =3D 2";
91          String decoded = (String) qpcodec.decode((Object) plain);
92          assertEquals("1+1 = 2", decoded, "Basic quoted-printable decoding test");
93  
94          final byte[] plainBA = plain.getBytes(StandardCharsets.UTF_8);
95          final byte[] decodedBA = (byte[]) qpcodec.decode((Object) plainBA);
96          decoded = new String(decodedBA);
97          assertEquals("1+1 = 2", decoded, "Basic quoted-printable decoding test");
98  
99          final Object result = qpcodec.decode((Object) null);
100         assertNull(result, "Decoding a null Object should return null");
101 
102         assertThrows(DecoderException.class, () -> qpcodec.decode(Double.valueOf(3.0d)), "Trying to url encode a Double object should cause an exception.");
103     }
104 
105     @Test
106     void testDecodeStringWithNull() throws Exception {
107         final QuotedPrintableCodec qpcodec = new QuotedPrintableCodec();
108         final String test = null;
109         final String result = qpcodec.decode(test, "charset");
110         assertNull(result, "Result should be null");
111     }
112 
113     @Test
114     void testDecodeWithNullArray() throws Exception {
115         final byte[] plain = null;
116         final byte[] result = QuotedPrintableCodec.decodeQuotedPrintable(plain);
117         assertNull(result, "Result should be null");
118     }
119 
120     @Test
121     void testDefaultEncoding() throws Exception {
122         final String plain = "Hello there!";
123         final QuotedPrintableCodec qpcodec = new QuotedPrintableCodec("UnicodeBig");
124         qpcodec.encode(plain); // To work around a weird quirk in Java 1.2.2
125         final String encoded1 = qpcodec.encode(plain, "UnicodeBig");
126         final String encoded2 = qpcodec.encode(plain);
127         assertEquals(encoded1, encoded2);
128     }
129 
130     @Test
131     void testEncodeDecodeNull() throws Exception {
132         final QuotedPrintableCodec qpcodec = new QuotedPrintableCodec();
133         assertNull(qpcodec.encode((String) null), "Null string quoted-printable encoding test");
134         assertNull(qpcodec.decode((String) null), "Null string quoted-printable decoding test");
135     }
136 
137     @Test
138     void testEncodeNull() throws Exception {
139         final QuotedPrintableCodec qpcodec = new QuotedPrintableCodec();
140         final byte[] plain = null;
141         final byte[] encoded = qpcodec.encode(plain);
142         assertNull(encoded, "Encoding a null string should return null");
143     }
144 
145     @Test
146     void testEncodeObjects() throws Exception {
147         final QuotedPrintableCodec qpcodec = new QuotedPrintableCodec();
148         final String plain = "1+1 = 2";
149         String encoded = (String) qpcodec.encode((Object) plain);
150 
151         assertEquals("1+1 =3D 2", encoded, "Basic quoted-printable encoding test");
152         final byte[] plainBA = plain.getBytes(StandardCharsets.UTF_8);
153         final byte[] encodedBA = (byte[]) qpcodec.encode((Object) plainBA);
154         encoded = new String(encodedBA);
155         assertEquals("1+1 =3D 2", encoded, "Basic quoted-printable encoding test");
156 
157         final Object result = qpcodec.encode((Object) null);
158         assertNull(result, "Encoding a null Object should return null");
159 
160         assertThrows(EncoderException.class, () -> qpcodec.encode(Double.valueOf(3.0d)), "Trying to url encode a Double object should cause an exception.");
161     }
162 
163     @Test
164     void testEncodeStringWithNull() throws Exception {
165         final QuotedPrintableCodec qpcodec = new QuotedPrintableCodec();
166         final String test = null;
167         final String result = qpcodec.encode(test, "charset");
168         assertNull(result, "Result should be null");
169     }
170 
171     @Test
172     void testEncodeUrlWithNullBitSet() throws Exception {
173         final QuotedPrintableCodec qpcodec = new QuotedPrintableCodec();
174         final String plain = "1+1 = 2";
175         final String encoded = new String(QuotedPrintableCodec.encodeQuotedPrintable(null, plain.getBytes(StandardCharsets.UTF_8)));
176         assertEquals("1+1 =3D 2", encoded, "Basic quoted-printable encoding test");
177         assertEquals(plain, qpcodec.decode(encoded), "Basic quoted-printable decoding test");
178     }
179 
180     @Test
181     void testFinalBytes() throws Exception {
182         // whitespace, but does not need to be encoded
183         final String plain = "This is a example of a quoted=printable text file. There is no tt";
184         final String expected = "This is a example of a quoted=3Dprintable text file. There is no tt";
185 
186         assertEquals(expected, new QuotedPrintableCodec(true).encode(plain));
187     }
188 
189     @ParameterizedTest
190     @ValueSource(strings = { "Line one\r\nLine two", "SEC\nRET", "SEC\rRET", "\r\n", "\r", "\n", "\r\nA\r\n\r\n" })
191     void testHardLineBreakDecode(final String input) throws Exception {
192         final byte[] bytes = StringUtils.getBytesUsAscii(input);
193         assertArrayEquals(bytes, QuotedPrintableCodec.decodeQuotedPrintable(bytes));
194         for (final boolean strict : new boolean[] { false, true }) {
195             assertEquals(input, new QuotedPrintableCodec(strict).decode(input));
196         }
197     }
198 
199     @Test
200     void testInvalidEncoding() {
201         assertThrows(UnsupportedCharsetException.class, () -> new QuotedPrintableCodec("NONSENSE"));
202     }
203 
204     @ParameterizedTest
205     @ValueSource(strings = { "foo=\rbar", "foo=\r", "foo=\nbar", "=\r", "=\n", "=\r\r\n" })
206     void testInvalidSoftLineBreakDecode(final String input) {
207         assertThrows(DecoderException.class, () -> QuotedPrintableCodec.decodeQuotedPrintable(StringUtils.getBytesUsAscii(input)));
208         for (final boolean strict : new boolean[] { false, true }) {
209             assertThrows(DecoderException.class, () -> new QuotedPrintableCodec(strict).decode(input));
210         }
211     }
212 
213     @Test
214     void testPreserveNotEncodedCRLF() throws Exception {
215         final String qpdata = "CRLF in an\n encoded text should be=20=\r\n\rpreserved in the\r decoding.";
216         final String expected = "CRLF in an\n encoded text should be \rpreserved in the\r decoding.";
217 
218         final QuotedPrintableCodec qpcodec = new QuotedPrintableCodec(true);
219         assertEquals(expected, qpcodec.decode(qpdata));
220 
221         final String encoded = qpcodec.encode(expected);
222         assertEquals(expected, qpcodec.decode(encoded));
223     }
224 
225     @Test
226     void testSafeCharEncodeDecode() throws Exception {
227         final QuotedPrintableCodec qpcodec = new QuotedPrintableCodec();
228         final String plain = "abc123_-.*~!@#$%^&()+{}\"\\;:`,/[]";
229         final String encoded = qpcodec.encode(plain);
230         assertEquals(plain, encoded, "Safe chars quoted-printable encoding test");
231         assertEquals(plain, qpcodec.decode(encoded), "Safe chars quoted-printable decoding test");
232     }
233 
234     @ParameterizedTest
235     @MethodSource("softLineBreakBoundaryCases")
236     void testSoftLineBreakBoundaries(final String input, final String expected) throws Exception {
237         assertArrayEquals(StringUtils.getBytesUsAscii(expected), QuotedPrintableCodec.decodeQuotedPrintable(StringUtils.getBytesUsAscii(input)));
238         for (final boolean strict : new boolean[] { false, true }) {
239             assertEquals(expected, new QuotedPrintableCodec(strict).decode(input));
240         }
241     }
242 
243     @Test
244     void testSoftLineBreakDecode() throws Exception {
245         final String qpdata = "If you believe that truth=3Dbeauty, then surely=20=\r\nmathematics is the most beautiful branch of philosophy.";
246         final String expected = "If you believe that truth=beauty, then surely mathematics is the most beautiful branch of philosophy.";
247 
248         final QuotedPrintableCodec qpcodec = new QuotedPrintableCodec();
249         assertEquals(expected, qpcodec.decode(qpdata));
250 
251         final String encoded = qpcodec.encode(expected);
252         assertEquals(expected, qpcodec.decode(encoded));
253     }
254 
255     @Test
256     void testSoftLineBreakEncode() throws Exception {
257         final String qpdata = "If you believe that truth=3Dbeauty, then surely mathematics is the most b=\r\neautiful branch of philosophy.";
258         final String expected = "If you believe that truth=beauty, then surely mathematics is the most beautiful branch of philosophy.";
259 
260         final QuotedPrintableCodec qpcodec = new QuotedPrintableCodec(true);
261         assertEquals(qpdata, qpcodec.encode(expected));
262 
263         final String decoded = qpcodec.decode(qpdata);
264         assertEquals(qpdata, qpcodec.encode(decoded));
265     }
266 
267     @Test
268     void testTooShortByteArray() throws Exception {
269         final QuotedPrintableCodec qpcodec = new QuotedPrintableCodec(true);
270         assertNull(qpcodec.encode("AA"), "Result should be null.");
271     }
272 
273     @Test
274     void testTrailingSpecial() throws Exception {
275         final QuotedPrintableCodec qpcodec = new QuotedPrintableCodec(true);
276 
277         String plain = "This is a example of a quoted-printable text file. This might contain sp=cial chars.";
278         String expected = "This is a example of a quoted-printable text file. This might contain sp=3D=\r\ncial chars.";
279         assertEquals(expected, qpcodec.encode(plain));
280 
281         plain = "This is a example of a quoted-printable text file. This might contain ta\tbs as well.";
282         expected = "This is a example of a quoted-printable text file. This might contain ta=09=\r\nbs as well.";
283         assertEquals(expected, qpcodec.encode(plain));
284     }
285 
286     @Test
287     void testUltimateSoftBreak() throws Exception {
288         final QuotedPrintableCodec qpcodec = new QuotedPrintableCodec(true);
289 
290         String plain = "This is a example of a quoted-printable text file. There is no end to it\t";
291         String expected = "This is a example of a quoted-printable text file. There is no end to i=\r\nt=09";
292 
293         assertEquals(expected, qpcodec.encode(plain));
294 
295         plain = "This is a example of a quoted-printable text file. There is no end to it ";
296         expected = "This is a example of a quoted-printable text file. There is no end to i=\r\nt=20";
297 
298         assertEquals(expected, qpcodec.encode(plain));
299 
300         // whitespace before soft break
301         plain = "This is a example of a quoted-printable text file. There is no end to   ";
302         expected = "This is a example of a quoted-printable text file. There is no end to=20=\r\n =20";
303 
304         assertEquals(expected, qpcodec.encode(plain));
305 
306         // non-printable character before soft break
307         plain = "This is a example of a quoted-printable text file. There is no end to=  ";
308         expected = "This is a example of a quoted-printable text file. There is no end to=3D=\r\n =20";
309 
310         assertEquals(expected, qpcodec.encode(plain));
311     }
312 
313     @Test
314     void testUnsafeEncodeDecode() throws Exception {
315         final QuotedPrintableCodec qpcodec = new QuotedPrintableCodec();
316         final String plain = "=\r\n";
317         final String encoded = qpcodec.encode(plain);
318         assertEquals("=3D=0D=0A", encoded, "Unsafe chars quoted-printable encoding test");
319         assertEquals(plain, qpcodec.decode(encoded), "Unsafe chars quoted-printable decoding test");
320     }
321 
322     @Test
323     void testUTF8RoundTrip() throws Exception {
324 
325         final String ru_msg = constructString(RUSSIAN_STUFF_UNICODE);
326         final String ch_msg = constructString(SWISS_GERMAN_STUFF_UNICODE);
327 
328         final QuotedPrintableCodec qpcodec = new QuotedPrintableCodec();
329 
330         assertEquals("=D0=92=D1=81=D0=B5=D0=BC_=D0=BF=D1=80=D0=B8=D0=B2=D0=B5=D1=82", qpcodec.encode(ru_msg, CharEncoding.UTF_8));
331         assertEquals("Gr=C3=BCezi_z=C3=A4m=C3=A4", qpcodec.encode(ch_msg, CharEncoding.UTF_8));
332 
333         assertEquals(ru_msg, qpcodec.decode(qpcodec.encode(ru_msg, CharEncoding.UTF_8), CharEncoding.UTF_8));
334         assertEquals(ch_msg, qpcodec.decode(qpcodec.encode(ch_msg, CharEncoding.UTF_8), CharEncoding.UTF_8));
335     }
336 }