11/*
2- * Copyright (c) 2022-2023 , FusionAuth, All Rights Reserved
2+ * Copyright (c) 2022-2025 , FusionAuth, All Rights Reserved
33 *
44 * Licensed under the Apache License, Version 2.0 (the "License");
55 * you may not use this file except in compliance with the License.
1717
1818import java .io .ByteArrayInputStream ;
1919import java .net .URLEncoder ;
20+ import java .nio .charset .Charset ;
2021import java .nio .charset .StandardCharsets ;
22+ import java .util .ArrayList ;
2123import java .util .HashMap ;
2224import java .util .List ;
2325import java .util .Map ;
4244public class HTTPToolsTest {
4345 @ Test
4446 public void parseEncodedData () {
45- // Test bad encoding
46-
47- Map <String , List <String >> actual = new HashMap <>();
48-
4947 // Happy path
50- byte [] body = "foo=bar" .getBytes (StandardCharsets .UTF_8 );
51- HTTPTools .parseEncodedData (body , 0 , body .length , actual );
52- assertEquals (actual , Map .of ("foo" , List .of ("bar" )), "Actual:\n " + ToString .toString (actual ));
48+ assertEncoding ("bar" , "bar" , StandardCharsets .UTF_8 );
5349
5450 // Note, there are 3 try/catches in the parseEncodedDate. This tests them in order, each hitting a specific try/catch.
5551
5652 // Bad name encoding
57- actual .clear ();
58- byte [] badName = "foo=bar&%%%=baz" .getBytes (StandardCharsets .UTF_8 );
59- HTTPTools .parseEncodedData (badName , 0 , badName .length , actual );
60- assertEquals (actual , Map .of ("foo" , List .of ("bar" )), "Actual:\n " + ToString .toString (actual ));
53+ assertEncoding ("bar&%%%=baz" , "bar" , StandardCharsets .UTF_8 );
6154
6255 // Bad value encoding
63- actual .clear ();
64- byte [] badValue1 = "foo=bar&bar=ba%å&=boom" .getBytes (StandardCharsets .UTF_8 );
65- HTTPTools .parseEncodedData (badValue1 , 0 , badValue1 .length , actual );
66- assertEquals (actual , Map .of ("foo" , List .of ("bar" )), "Actual:\n " + ToString .toString (actual ));
56+ assertEncoding ("bar&bar=ba%å&=boom" , "bar" , StandardCharsets .UTF_8 );
6757
6858 // Bad value encoding
69- actual .clear ();
70- byte [] badValue2 = "foo=bar&bar=% % %" .getBytes (StandardCharsets .UTF_8 );
71- HTTPTools .parseEncodedData (badValue2 , 0 , badValue2 .length , actual );
72- assertEquals (actual , Map .of ("foo" , List .of ("bar" )), "Actual:\n " + ToString .toString (actual ));
59+ assertEncoding ("bar&bar=% % %" , "bar" , StandardCharsets .UTF_8 );
60+
61+ // UTF-8 encoding of characters not in the ISO-8859-1 character set
62+ assertEncoding ("😎" , "😎" , StandardCharsets .UTF_8 );
63+ assertEncoding ("é" , "é" , StandardCharsets .UTF_8 );
64+ assertEncoding ("€" , "€" , StandardCharsets .UTF_8 );
65+ assertEncoding ("Héllö" , "Héllö" , StandardCharsets .UTF_8 );
66+
67+ // Double byte values are outside ISO-88559-1, so we should expect them to not render correctly. See next test.
68+ assertHexValue ("😎" , "D83D DE0E" );
69+ assertHexValue ("€" , "20AC" );
70+
71+ // ISO-8559-1 encoding of characters outside the character set
72+ assertEncoding ("😎" , "?" , StandardCharsets .ISO_8859_1 );
73+ assertEncoding ("€" , "?" , StandardCharsets .ISO_8859_1 );
74+
75+ // These values are within the ISO-8559-1 charset, expect them to render correctly.
76+ assertHexValue ("é" , "E9" );
77+ assertHexValue ("Héllö" , "48 E9 6C 6C F6" );
78+
79+ // ISO-8559-1 encoding of non-ASCII characters inside the character set
80+ assertEncoding ("é" , "é" , StandardCharsets .ISO_8859_1 );
81+ assertEncoding ("Héllö" , "Héllö" , StandardCharsets .ISO_8859_1 );
82+
83+ // Mixing and matching. Expect some wonky behavior.
84+ // - Encoded using ISO-8559-1 and decoded as UTF-8
85+ assertEncoding ("Héllö" , "H�ll�" , StandardCharsets .ISO_8859_1 , StandardCharsets .UTF_8 );
86+ assertEncoding ("Hello world" , "Hello world" , StandardCharsets .ISO_8859_1 , StandardCharsets .UTF_8 );
87+ // - Reverse
88+ // The é fails here because while it does exist in both UTF-8 and ISO-8859-1, it is not the same byte. So expect the rendering to be off.
89+ assertHexValue ("é" , "C3 A9" , StandardCharsets .UTF_8 );
90+ assertHexValue ("é" , "E9" , StandardCharsets .ISO_8859_1 );
91+ assertHexValue ("Ã" , "C3" , StandardCharsets .ISO_8859_1 );
92+ assertHexValue ("©" , "A9" , StandardCharsets .ISO_8859_1 );
93+ assertEncoding ("é" , "é" , StandardCharsets .UTF_8 , StandardCharsets .ISO_8859_1 );
94+
95+ // The ö fails here because while it does exist in both UTF-8 and ISO-8859-1, it is not the same byte. So expect the rendering to be off.
96+ assertHexValue ("ö" , "C3 B6" , StandardCharsets .UTF_8 );
97+ assertHexValue ("ö" , "F6" , StandardCharsets .ISO_8859_1 );
98+ assertHexValue ("¶" , "B6" , StandardCharsets .ISO_8859_1 );
99+ assertEncoding ("Héllö" , "Héllö" , StandardCharsets .UTF_8 , StandardCharsets .ISO_8859_1 );
100+ assertEncoding ("Hello world" , "Hello world" , StandardCharsets .UTF_8 , StandardCharsets .ISO_8859_1 );
73101 }
74102
75103 @ Test
76104 public void parseHeaderValue () {
77105 String iso = "åpple" ;
78106 String utf = "😁" ;
107+ assertEquals (HTTPTools .parseHeaderValue ("text/plain; charset=iso8859-1" ), new HeaderValue ("text/plain" , Map .of ("charset" , "iso8859-1" )));
79108 assertEquals (HTTPTools .parseHeaderValue ("text/plain; charset=iso-8859-1" ), new HeaderValue ("text/plain" , Map .of ("charset" , "iso-8859-1" )));
109+ assertEquals (HTTPTools .parseHeaderValue ("text/plain; charset=iso8859-1; boundary=FOOBAR" ), new HeaderValue ("text/plain" , Map .of ("boundary" , "FOOBAR" , "charset" , "iso8859-1" )));
80110 assertEquals (HTTPTools .parseHeaderValue ("text/plain; charset=iso-8859-1; boundary=FOOBAR" ), new HeaderValue ("text/plain" , Map .of ("boundary" , "FOOBAR" , "charset" , "iso-8859-1" )));
81111 assertEquals (HTTPTools .parseHeaderValue ("form-data; filename=foo.jpg" ), new HeaderValue ("form-data" , Map .of ("filename" , "foo.jpg" )));
82112 assertEquals (HTTPTools .parseHeaderValue ("form-data; filename=\" foo.jpg\" " ), new HeaderValue ("form-data" , Map .of ("filename" , "foo.jpg" )));
@@ -89,6 +119,7 @@ public void parseHeaderValue() {
89119 assertEquals (HTTPTools .parseHeaderValue ("form-data; filename=ignore.jpg; filename*=ISO-8859-1'en'foo.jpg" ), new HeaderValue ("form-data" , Map .of ("filename" , "foo.jpg" )));
90120
91121 // Encoded
122+ assertEquals (HTTPTools .parseHeaderValue ("form-data; filename*=ISO8859-1'en'" + URLEncoder .encode (iso , StandardCharsets .ISO_8859_1 )), new HeaderValue ("form-data" , Map .of ("filename" , iso )));
92123 assertEquals (HTTPTools .parseHeaderValue ("form-data; filename*=ISO-8859-1'en'" + URLEncoder .encode (iso , StandardCharsets .ISO_8859_1 )), new HeaderValue ("form-data" , Map .of ("filename" , iso )));
93124 assertEquals (HTTPTools .parseHeaderValue ("form-data; filename*=UTF-8'en'" + URLEncoder .encode (utf , StandardCharsets .UTF_8 )), new HeaderValue ("form-data" , Map .of ("filename" , utf )));
94125
@@ -182,4 +213,41 @@ public void parsePreamble() throws Exception {
182213 int nextRequestRead = pushbackInputStream .read (buffer );
183214 assertEquals (nextRequestRead , 16 );
184215 }
216+
217+ private void assertEncoding (String actualValue , String expectedValue , Charset charset ) {
218+ assertEncoding (actualValue , expectedValue , charset , charset );
219+
220+ }
221+
222+ private void assertEncoding (String actualValue , String expectedValue , Charset encodingCharset , Charset decodingCharset ) {
223+ Map <String , List <String >> result = new HashMap <>(1 );
224+ byte [] encoded = ("foo=" + actualValue ).getBytes (encodingCharset );
225+ HTTPTools .parseEncodedData (encoded , 0 , encoded .length , decodingCharset , result );
226+ assertEquals (result , Map .of ("foo" , List .of (expectedValue )), "Actual:\n " + ToString .toString (result ));
227+ }
228+
229+ private void assertHexValue (String s , String expected ) {
230+ assertEquals (hex (s ), expected );
231+ }
232+
233+ private void assertHexValue (String s , String expected , Charset charset ) {
234+ assertEquals (hex (s .getBytes (charset )), expected );
235+ }
236+
237+ private String hex (byte [] bytes ) {
238+ List <String > result = new ArrayList <>();
239+ for (byte b : bytes ) {
240+
241+ result .add (Integer .toHexString (0xFF & b ).toUpperCase ());
242+ }
243+ return String .join (" " , result );
244+ }
245+
246+ private String hex (String s ) {
247+ List <String > result = new ArrayList <>();
248+ for (char ch : s .toCharArray ()) {
249+ result .add (Integer .toHexString (ch ).toUpperCase ());
250+ }
251+ return String .join (" " , result );
252+ }
185253}
0 commit comments