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.util.BitSet;
27  
28  import org.apache.commons.codec.CharEncoding;
29  import org.apache.commons.codec.DecoderException;
30  import org.apache.commons.codec.EncoderException;
31  import org.junit.jupiter.api.Test;
32  import org.junit.jupiter.params.ParameterizedTest;
33  import org.junit.jupiter.params.provider.CsvSource;
34  import org.junit.jupiter.params.provider.ValueSource;
35  
36  /**
37   * URL codec test cases
38   */
39  class URLCodecTest {
40  
41      static final int[] SWISS_GERMAN_STUFF_UNICODE = { 0x47, 0x72, 0xFC, 0x65, 0x7A, 0x69, 0x5F, 0x7A, 0xE4, 0x6D, 0xE4 };
42  
43      static final int[] RUSSIAN_STUFF_UNICODE = { 0x412, 0x441, 0x435, 0x43C, 0x5F, 0x43F, 0x440, 0x438, 0x432, 0x435, 0x442 };
44  
45      private String constructString(final int[] unicodeChars) {
46          final StringBuilder buffer = new StringBuilder();
47          if (unicodeChars != null) {
48              for (final int unicodeChar : unicodeChars) {
49                  buffer.append((char) unicodeChar);
50              }
51          }
52          return buffer.toString();
53      }
54  
55      @Test
56      void testBasicEncodeDecode() throws Exception {
57          final URLCodec urlCodec = new URLCodec();
58          final String plain = "Hello there!";
59          final String encoded = urlCodec.encode(plain);
60          assertEquals("Hello+there%21", encoded, "Basic URL encoding test");
61          assertEquals(plain, urlCodec.decode(encoded), "Basic URL decoding test");
62          validateState(urlCodec);
63      }
64  
65      @ParameterizedTest(name = "{0}: {1}")
66      // @formatter:off
67      @CsvSource({
68          "missing escape digits, %",
69          "missing second escape digit, %A",
70          "invalid first escape digit, %WW",
71          "invalid second escape digit, %0W"
72      })
73      // @formatter:on
74      void testDecodeInvalid(final String description, final String encoded) throws Exception {
75          final URLCodec urlCodec = new URLCodec();
76          assertThrows(DecoderException.class, () -> urlCodec.decode(encoded), description);
77          validateState(urlCodec);
78      }
79  
80      @Test
81      void testDecodeInvalidContent() throws DecoderException {
82          final String ch_msg = constructString(SWISS_GERMAN_STUFF_UNICODE);
83          final URLCodec urlCodec = new URLCodec();
84          final byte[] input = ch_msg.getBytes(StandardCharsets.ISO_8859_1);
85          final byte[] output = urlCodec.decode(input);
86          assertEquals(input.length, output.length);
87          for (int i = 0; i < input.length; i++) {
88              assertEquals(input[i], output[i]);
89          }
90          validateState(urlCodec);
91      }
92  
93      @Test
94      void testDecodeObjects() throws Exception {
95          final URLCodec urlCodec = new URLCodec();
96          final String plain = "Hello+there%21";
97          String decoded = (String) urlCodec.decode((Object) plain);
98          assertEquals("Hello there!", decoded, "Basic URL decoding test");
99          final byte[] plainBA = plain.getBytes(StandardCharsets.UTF_8);
100         final byte[] decodedBA = (byte[]) urlCodec.decode((Object) plainBA);
101         decoded = new String(decodedBA);
102         assertEquals("Hello there!", decoded, "Basic URL decoding test");
103         final Object result = urlCodec.decode((Object) null);
104 
105         assertNull(result, "Decoding a null Object should return null");
106 
107         assertThrows(DecoderException.class, () -> urlCodec.decode(Double.valueOf(3.0d)), "Trying to url encode a Double object should cause an exception.");
108         validateState(urlCodec);
109     }
110 
111     @Test
112     void testDecodeStringWithNull() throws Exception {
113         final URLCodec urlCodec = new URLCodec();
114         final String test = null;
115         final String result = urlCodec.decode(test, "charset");
116         assertNull(result, "Result should be null");
117     }
118 
119     @Test
120     void testDecodeUrlWithCustomBitSetAmbiguousEscapes() throws Exception {
121         final BitSet safe = new BitSet();
122         safe.set('%');
123         final byte[] encoded = URLCodec.encodeUrl(safe, new byte[] { '%', '/' });
124         assertEquals("%%2F", new String(encoded, StandardCharsets.US_ASCII));
125         assertArrayEquals(encoded, URLCodec.decodeUrl(safe, encoded));
126     }
127 
128     @ParameterizedTest
129     @ValueSource(strings = { "%", "%A", "%WW", "%0W", "%W0" })
130     void testDecodeUrlWithCustomBitSetInvalidEscapes(final String input) throws Exception {
131         final byte[] bytes = input.getBytes(StandardCharsets.US_ASCII);
132         final BitSet safe = new BitSet();
133         assertThrows(DecoderException.class, () -> URLCodec.decodeUrl(null, bytes));
134         assertThrows(DecoderException.class, () -> URLCodec.decodeUrl(safe, bytes));
135         safe.set('%');
136         assertArrayEquals(bytes, URLCodec.decodeUrl(safe, bytes));
137     }
138 
139     @Test
140     void testDecodeUrlWithCustomBitSetNullAndEmpty() throws Exception {
141         assertNull(URLCodec.decodeUrl(null, null));
142         assertNull(URLCodec.decodeUrl(new BitSet(), null));
143         assertArrayEquals(new byte[0], URLCodec.decodeUrl(null, new byte[0]));
144         assertArrayEquals(new byte[0], URLCodec.decodeUrl(new BitSet(), new byte[0]));
145     }
146 
147     @ParameterizedTest
148     @ValueSource(ints = { 0, 1, 2, 3 })
149     void testDecodeUrlWithCustomBitSetPlusAndSpace(final int flags) throws Exception {
150         final BitSet safe = new BitSet();
151         safe.set(' ', (flags & 1) != 0);
152         safe.set('+', (flags & 2) != 0);
153         final String expected = flags == 1 ? "  +" : "+ +";
154         assertEquals(expected, new String(URLCodec.decodeUrl(safe, "+%20%2b".getBytes(StandardCharsets.US_ASCII)), StandardCharsets.US_ASCII));
155         if (flags == 3) {
156             final byte[] encoded = URLCodec.encodeUrl(safe, " +".getBytes(StandardCharsets.US_ASCII));
157             assertEquals("++", new String(encoded, StandardCharsets.US_ASCII));
158             assertArrayEquals(encoded, URLCodec.decodeUrl(safe, encoded));
159         }
160     }
161 
162     @Test
163     void testDecodeUrlWithCustomBitSetRoundTripAllBytes() throws Exception {
164         final byte[] input = new byte[256];
165         for (int i = 0; i < input.length; i++) {
166             input[i] = (byte) i;
167         }
168         final BitSet literalPlus = new BitSet(256);
169         literalPlus.set(0, 256);
170         literalPlus.clear('%');
171         literalPlus.clear(' ');
172         final BitSet spaceAsPlus = (BitSet) literalPlus.clone();
173         spaceAsPlus.clear('+');
174         spaceAsPlus.set(' ');
175         for (final BitSet safe : new BitSet[] { null, new BitSet(), literalPlus, spaceAsPlus }) {
176             final BitSet original = safe == null ? null : (BitSet) safe.clone();
177             final byte[] encoded = URLCodec.encodeUrl(safe, input);
178             assertArrayEquals(input, URLCodec.decodeUrl(safe, encoded));
179             assertEquals(original, safe, "The safe set must not be modified");
180         }
181     }
182 
183     @Test
184     void testDecodeWithNullArray() throws Exception {
185         final byte[] plain = null;
186         final byte[] result = URLCodec.decodeUrl(plain);
187         assertNull(result, "Result should be null");
188     }
189 
190     @Test
191     void testDefaultEncoding() throws Exception {
192         final String plain = "Hello there!";
193         final URLCodec urlCodec = new URLCodec("UnicodeBig");
194         urlCodec.encode(plain); // To work around a weird quirk in Java 1.2.2
195         final String encoded1 = urlCodec.encode(plain, "UnicodeBig");
196         final String encoded2 = urlCodec.encode(plain);
197         assertEquals(encoded1, encoded2);
198         validateState(urlCodec);
199     }
200 
201     @Test
202     void testEncodeDecodeNull() throws Exception {
203         final URLCodec urlCodec = new URLCodec();
204         assertNull(urlCodec.encode((String) null), "Null string URL encoding test");
205         assertNull(urlCodec.decode((String) null), "Null string URL decoding test");
206         validateState(urlCodec);
207     }
208 
209     @Test
210     void testEncodeNull() throws Exception {
211         final URLCodec urlCodec = new URLCodec();
212         final byte[] plain = null;
213         final byte[] encoded = urlCodec.encode(plain);
214         assertNull(encoded, "Encoding a null string should return null");
215         validateState(urlCodec);
216     }
217 
218     @Test
219     void testEncodeObjects() throws Exception {
220         final URLCodec urlCodec = new URLCodec();
221         final String plain = "Hello there!";
222         String encoded = (String) urlCodec.encode((Object) plain);
223         assertEquals("Hello+there%21", encoded, "Basic URL encoding test");
224 
225         final byte[] plainBA = plain.getBytes(StandardCharsets.UTF_8);
226         final byte[] encodedBA = (byte[]) urlCodec.encode((Object) plainBA);
227         encoded = new String(encodedBA);
228         assertEquals("Hello+there%21", encoded, "Basic URL encoding test");
229 
230         final Object result = urlCodec.encode((Object) null);
231         assertNull(result, "Encoding a null Object should return null");
232 
233         assertThrows(EncoderException.class, () -> urlCodec.encode(Double.valueOf(3.0d)), "Trying to url encode a Double object should cause an exception.");
234         validateState(urlCodec);
235     }
236 
237     @Test
238     void testEncodeStringWithNull() throws Exception {
239         final URLCodec urlCodec = new URLCodec();
240         final String test = null;
241         final String result = urlCodec.encode(test, "charset");
242         assertNull(result, "Result should be null");
243     }
244 
245     @Test
246     void testEncodeUrlWithNullBitSet() throws Exception {
247         final URLCodec urlCodec = new URLCodec();
248         final String plain = "Hello there!%+";
249         final String encoded = new String(URLCodec.encodeUrl(null, plain.getBytes(StandardCharsets.UTF_8)));
250         assertEquals("Hello+there%21%25%2B", encoded, "Basic URL encoding test");
251         assertEquals(plain, urlCodec.decode(encoded), "Basic URL decoding test");
252         validateState(urlCodec);
253     }
254 
255     @Test
256     void testEncodeUrlWithPercentMarkedSafePreservesPercent() {
257         final BitSet safe = new BitSet();
258         safe.set('%');
259         final String plain = "%";
260         final byte[] encoded = URLCodec.encodeUrl(safe, plain.getBytes(StandardCharsets.US_ASCII));
261         final String encodedS = new String(encoded, StandardCharsets.US_ASCII);
262         assertEquals(plain, encodedS, "URLCodec should preserve percent when marked safe");
263         assertThrows(DecoderException.class, () -> URLCodec.decodeUrl(encoded));
264     }
265 
266     @Test
267     void testEncodeUrlWithPlusMarkedSafePreservesPlus() throws Exception {
268         final BitSet safe = new BitSet();
269         safe.set('+');
270         final String plain = "+";
271         final byte[] encoded = URLCodec.encodeUrl(safe, plain.getBytes(StandardCharsets.US_ASCII));
272         final String encodedS = new String(encoded, StandardCharsets.US_ASCII);
273         assertEquals(plain, encodedS, "URLCodec should preserve plus when marked safe");
274         final byte[] decoded = URLCodec.decodeUrl(encoded);
275         assertEquals(" ", new String(decoded, StandardCharsets.US_ASCII), "Default decoding interprets a literal plus as a space");
276     }
277 
278     @Test
279     void testInvalidEncoding() {
280         final URLCodec urlCodec = new URLCodec("NONSENSE");
281         final String plain = "Hello there!";
282         assertThrows(EncoderException.class, () -> urlCodec.encode(plain), "We set the encoding to a bogus NONSENSE value");
283         assertThrows(DecoderException.class, () -> urlCodec.decode(plain), "We set the encoding to a bogus NONSENSE value");
284         validateState(urlCodec);
285     }
286 
287     @Test
288     void testSafeCharEncodeDecode() throws Exception {
289         final URLCodec urlCodec = new URLCodec();
290         final String plain = "abc123_-.*";
291         final String encoded = urlCodec.encode(plain);
292         assertEquals(plain, encoded, "Safe chars URL encoding test");
293         assertEquals(plain, urlCodec.decode(encoded), "Safe chars URL decoding test");
294         validateState(urlCodec);
295     }
296 
297     @Test
298     void testUnsafeEncodeDecode() throws Exception {
299         final URLCodec urlCodec = new URLCodec();
300         final String plain = "~!@#$%^&()+{}\"\\;:`,/[]";
301         final String encoded = urlCodec.encode(plain);
302         assertEquals("%7E%21%40%23%24%25%5E%26%28%29%2B%7B%7D%22%5C%3B%3A%60%2C%2F%5B%5D", encoded, "Unsafe chars URL encoding test");
303         assertEquals(plain, urlCodec.decode(encoded), "Unsafe chars URL decoding test");
304         validateState(urlCodec);
305     }
306 
307     @Test
308     void testUTF8RoundTrip() throws Exception {
309 
310         final String ru_msg = constructString(RUSSIAN_STUFF_UNICODE);
311         final String ch_msg = constructString(SWISS_GERMAN_STUFF_UNICODE);
312 
313         final URLCodec urlCodec = new URLCodec();
314         validateState(urlCodec);
315 
316         assertEquals("%D0%92%D1%81%D0%B5%D0%BC_%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82", urlCodec.encode(ru_msg, CharEncoding.UTF_8));
317         assertEquals("Gr%C3%BCezi_z%C3%A4m%C3%A4", urlCodec.encode(ch_msg, CharEncoding.UTF_8));
318 
319         assertEquals(ru_msg, urlCodec.decode(urlCodec.encode(ru_msg, CharEncoding.UTF_8), CharEncoding.UTF_8));
320         assertEquals(ch_msg, urlCodec.decode(urlCodec.encode(ch_msg, CharEncoding.UTF_8), CharEncoding.UTF_8));
321         validateState(urlCodec);
322     }
323 
324     private void validateState(final URLCodec urlCodec) {
325         // no tests for now.
326     }
327 }