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