View Javadoc
1   package org.djutils.decoderdumper;
2   
3   import static org.junit.jupiter.api.Assertions.assertEquals;
4   import static org.junit.jupiter.api.Assertions.assertTrue;
5   import static org.junit.jupiter.api.Assertions.fail;
6   
7   import java.io.BufferedOutputStream;
8   import java.io.ByteArrayOutputStream;
9   import java.io.IOException;
10  import java.io.InputStream;
11  import java.io.OutputStream;
12  import java.io.PrintStream;
13  import java.util.Locale;
14  
15  import org.junit.jupiter.api.Test;
16  
17  /**
18   * Tests for the decoder/dumper package.
19   * <p>
20   * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
21   * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
22   * <p>
23   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
24   * @author <a href="https://www.tudelft.nl/staff/p.knoppers/">Peter Knoppers</a>
25   */
26  public class DecoderDumperTests
27  {
28  
29      /**
30       * Test the Hex decoder and dumper classes.
31       * @throws InterruptedException if that happens; this test has failed.
32       * @throws IOException if that happens; this test has failed.
33       */
34      @SuppressWarnings("checkstyle:methodlength")
35      @Test
36      public final void testHexDumper() throws InterruptedException, IOException
37      {
38          Locale.setDefault(Locale.US);
39          assertEquals("", HexDumper.hexDumper(new byte[] {}), "Empty input yields empty output");
40          byte[] input = new byte[] {1, 2};
41          String output = HexDumper.hexDumper(input);
42          assertTrue(output.startsWith("00000000: "), "Output starts with address \"00000000: \"");
43          for (int length = 1; length < 100; length++)
44          {
45              input = new byte[length];
46              assertTrue(HexDumper.hexDumper(input).endsWith("\n"), "Output ends on newline");
47          }
48          input = new byte[1];
49          for (int value = 0; value < 256; value++)
50          {
51              input[0] = (byte) value;
52              output = HexDumper.hexDumper(input);
53              // System.out.print(String.format("%3d -> %s", value, output));
54              assertTrue(output.contains(String.format(" %02x ", value)),
55                      "Output contains hex value of the only input byte embedded between spaces");
56          }
57          assertEquals(1, HexDumper.hexDumper(new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}).split("\n").length,
58                  "output of 16 byte input fills one lines");
59          assertEquals(2, HexDumper.hexDumper(new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}).split("\n").length,
60                  "output of 17 byte input fills two lines");
61          assertTrue(HexDumper.hexDumper(0x12345, new byte[] {0, 1}).startsWith("00012340"),
62                  "address offset is printed at start of output");
63          Dumper<HexDumper> hd = new HexDumper(0x12345);
64          assertTrue(hd.toString().startsWith("HexDumper"), "toString makes some sense");
65  
66          ByteArrayOutputStream baos = new ByteArrayOutputStream();
67          hd = new HexDumper().setOutputStream(baos);
68          for (int i = 0; i < 100; i++)
69          {
70              hd.append((byte) i);
71              // System.out.println("i=" + i + ", hd=" + hd + " baos=" + baos);
72              assertEquals(Math.max(1, (i + 1) / 16), baos.toString().split("\n").length, "Number of lines check");
73          }
74          // System.out.println(hd.getDump());
75          for (int i = 33; i < 127; i++)
76          {
77              String dump = HexDumper.hexDumper(new byte[] {(byte) i});
78              String letter = "" + (char) i;
79              String trimmed = dump.trim();
80              String lastLetter = trimmed.substring(trimmed.length() - 1);
81              // System.out.print("i=" + i + " letter=" + letter + ", output is: " + dump);
82              assertEquals(letter, lastLetter, "Output ends with the provided printable character");
83          }
84          baos.reset();
85          hd = new HexDumper().addDecoder(0, new TimeStamper()).setOutputStream(baos);
86          long startTimeStamp = System.currentTimeMillis();
87          hd.append((byte) 10);
88          long endTimeStamp = System.currentTimeMillis();
89          Thread.sleep(100);
90          hd.append((byte) 20);
91          hd.flush();
92          String result = baos.toString();
93          int spacePosition = result.indexOf(" ");
94          long recorded = Long.parseLong(result.substring(0, spacePosition).replace(".", "").replace(",", ""));
95          assertTrue(startTimeStamp <= recorded, "Time stamp should be within interval");
96          assertTrue(endTimeStamp >= recorded, "Time stamp should be within interval");
97          hd = new HexDumper().setOutputStream(new OutputStream()
98          {
99  
100             @Override
101             public void write(final int b) throws IOException
102             {
103                 throw new IOException("testing exception handling");
104             }
105         });
106         try
107         {
108             hd.append(new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16});
109             fail("Writing sufficient number of bytes to output that throws an exception should have thrown an exception");
110         }
111         catch (Exception exception)
112         {
113             // Ignore expected exception
114         }
115         baos.reset();
116         hd = new HexDumper().setOutputStream(baos);
117         hd.append(new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16});
118         // By now there should be something in the ByteArrayOutputStream
119         assertTrue(baos.toString().startsWith("00000000: 00 "), "ByteArrayOutputStream contains start of hex dump");
120         baos.reset();
121         PrintStream oldErrOutput = System.err;
122         PrintStream ps = new PrintStream(new BufferedOutputStream(baos));
123         System.setErr(ps);
124         // Redirect the output to System.err
125         hd = new HexDumper().setOutputStream(new OutputStream()
126         {
127             /** The string builder. */
128             private StringBuilder sb = new StringBuilder();
129 
130             @Override
131             public void write(final int b) throws IOException
132             {
133                 if ('\n' == b)
134                 {
135                     System.err.println(this.sb.toString());
136                     this.sb.setLength(0);
137                 }
138                 else
139                 {
140                     this.sb.append((char) b);
141                 }
142             }
143         });
144         for (int value = 0; value < 256; value++)
145         {
146             input[0] = (byte) value;
147             hd.append(input);
148         }
149         Thread.sleep(200);
150         ps.close();
151         System.setErr(oldErrOutput);
152         result = baos.toString();
153         assertEquals(16, result.split("\n").length, "Result should be 16 lines");
154         // System.out.print("baos contains:\n" + result);
155         baos.reset();
156         hd = new HexDumper().setOutputStream(baos).append(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}, 4, 2);
157         hd.flush();
158         result = baos.toString();
159         assertTrue(result.startsWith("00000000: 05 06    "), "start and length parameter select the correct bytes");
160         baos.reset();
161         hd = new HexDumper().setOutputStream(baos);
162         hd.append(new InputStream()
163         {
164             private int callCount = 0;
165 
166             @Override
167             public int read() throws IOException
168             {
169                 if (this.callCount < 10)
170                 {
171                     return this.callCount++;
172                 }
173                 return -1;
174             }
175         });
176         hd.flush();
177         result = baos.toString();
178         // System.out.println(result);
179         assertTrue(result.startsWith("00000000: 00 01 02 03 04 05 06 07  08 09    "), "Ten bytes should have been accumulated");
180         baos.reset();
181         hd = new HexDumper().setSuppressMultipleIdenticalLines(true).setOutputStream(baos);
182         for (int line = 0; line < 20; line++)
183         {
184             hd.append(new byte[] {42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57});
185         }
186         hd.flush();
187         // System.out.println(baos);
188         assertEquals(3, baos.toString().split("\n").length, "Suppression reduced the output to three lines");
189         // System.out.println(baos);
190         baos.reset();
191         hd = new HexDumper().setSuppressMultipleIdenticalLines(true).setOutputStream(baos);
192         for (int line = 0; line < 20; line++)
193         {
194             hd.append(new byte[] {42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57});
195         }
196         hd.append(new byte[] {42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56});
197         hd.flush();
198         assertEquals(4, baos.toString().split("\n").length, "Suppression reduced the output to four lines");
199         // System.out.println(baos);
200         baos.reset();
201         hd = new HexDumper().setSuppressMultipleIdenticalLines(true).setOutputStream(baos);
202         for (int line = 0; line < 20; line++)
203         {
204             hd.append(new byte[] {42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57});
205         }
206         hd.append(new byte[] {42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 99});
207         hd.flush();
208         assertEquals(4, baos.toString().split("\n").length, "Suppression reduced the output to four lines");
209         // System.out.println(baos);
210         // FIXME: not exhaustively testing switching compression on and off between append calls.
211         assertTrue(new TimeStamper().toString().startsWith("TimeStamper ["), "TimeStamper had decent toString method");
212     }
213 
214     /**
215      * Test the Base64 decoder and dumper classes.
216      */
217     @Test
218     public void testBase64Dumper()
219     {
220         assertEquals("", Base64Dumper.base64Dumper(new byte[] {}), "Empty input yields empty output");
221         byte[] input = new byte[] {1, 2};
222         String output = HexDumper.hexDumper(input);
223         assertTrue(output.startsWith("00000000: "), "Output starts with address \"00000000: \"");
224         for (int length = 1; length < 100; length++)
225         {
226             input = new byte[length];
227             assertTrue(HexDumper.hexDumper(input).endsWith("\n"), "Output ends on newline (even though the input is invalid)");
228         }
229         // Generate many possible 24-bit values; then construct the base64 string that would generate the 3 bytes
230         for (int pattern = 0; pattern < 256 * 256 * 256; pattern += 259)
231         {
232             input = new byte[4];
233             for (int index = 0; index < 4; index++)
234             {
235                 input[index] = encode((pattern >> (18 - 6 * index)) & 0x3f);
236             }
237             output = Base64Dumper.base64Dumper(input).substring(30);
238             // System.out.println("input \"" + pattern +"\", output \"" + output + "\"");
239             for (int index = 0; index < 3; index++)
240             {
241                 int theByte = Integer.parseInt(output.substring(index * 3, index * 3 + 2), 16);
242                 int expectedByte = (pattern >> (16 - 8 * index)) & 0xff;
243                 assertEquals(expectedByte, theByte, "Reconstructed byte matches corresponding byte in pattern");
244             }
245         }
246         // Generate all possible 8-bit values and pad with two = signs
247         for (int pattern = 0; pattern < 255; pattern++)
248         {
249             input = new byte[4];
250             for (int index = 0; index < 2; index++)
251             {
252                 input[index] = encode(((pattern << 16) >> (18 - 6 * index)) & 0x3f);
253             }
254             input[2] = (byte) 61;
255             input[3] = (byte) 61;
256             output = Base64Dumper.base64Dumper(input).substring(30);
257             // System.out.println("input " + pattern + ", base64=" + Arrays.toString(input) + ", output \"" + output + "\"");
258             int theByte = Integer.parseInt(output.substring(0, 2), 16);
259             assertEquals(pattern, theByte, "Reconstructed byte matches corresponding byte in patten");
260             assertTrue(output.substring(2).startsWith("          "), "Rest of result starts with at least 10 spaces");
261         }
262         // Generate all possible 16-bit values and pad with one = sign
263         for (int pattern = 0; pattern < 255 * 255; pattern++)
264         {
265             input = new byte[4];
266             for (int index = 0; index < 3; index++)
267             {
268                 input[index] = encode(((pattern << 8) >> (18 - 6 * index)) & 0x3f);
269             }
270             input[3] = (byte) 61;
271             output = Base64Dumper.base64Dumper(input).substring(30);
272             // System.out.println("input " + pattern + ", base64=" + Arrays.toString(input) + ", output \"" + output + "\"");
273             for (int index = 0; index < 2; index++)
274             {
275                 int theByte = Integer.parseInt(output.substring(index * 3, index * 3 + 2), 16);
276                 int expectedByte = (pattern >> (8 - 8 * index)) & 0xff;
277                 assertEquals(expectedByte, theByte, "Reconstructed byte matches corresponding byte in pattern");
278             }
279             assertTrue(output.substring(5).startsWith("          "), "Rest of result starts with at least 10 spaces");
280         }
281         assertTrue(new Base64Dumper().toString().startsWith("Base64Dumper"), "toString makes some sense");
282         // White space in base64 encoded data should be ignored
283         String base64 = "c3VyZS4=";
284         String expectedResult = Base64Dumper.base64Dumper(base64.getBytes()).substring(30);
285         // System.out.print("reference: " + expectedResult);
286         for (int pos = 0; pos <= base64.length(); pos++)
287         {
288             for (String insert : new String[] {" ", "\t", "\n", "\n\t"})
289             {
290                 String modified = base64.substring(0, pos) + insert + base64.substring(pos);
291                 String result = Base64Dumper.base64Dumper(modified.getBytes()).substring(30);
292                 // System.out.print("result: " + result);
293                 assertEquals(expectedResult, result, "Extra space in input does not change output");
294             }
295         }
296         // Error character in input is (currently) silently ignored
297         String result = Base64Dumper.base64Dumper(("!" + base64).getBytes()).substring(30);
298         // System.out.print("result: " + result);
299         assertEquals(expectedResult, result, "bad char in input is (currently) ignored");
300     }
301 
302     /**
303      * Base64 encode one 6-bit value.
304      * @param value int; value in the range 0..63
305      * @return byte; the encoded value
306      */
307     private byte encode(final int value)
308     {
309         if (value < 0 || value > 63)
310         {
311             throw new Error("Bad input: " + value);
312         }
313         if (value < 26)
314         {
315             return (byte) (value + 65);
316         }
317         else if (value < 52)
318         {
319             return (byte) (value - 26 + 97);
320         }
321         else if (value < 62)
322         {
323             return (byte) (value - 52 + 48);
324         }
325         else if (value == 62)
326         {
327             return (byte) 43;
328         }
329         else if (value == 63)
330         {
331             return (byte) 47;
332         }
333         throw new Error("Bad input: " + value);
334     }
335 
336 }