1 package org.djutils.decoderdumper;
2
3 import java.io.ByteArrayOutputStream;
4 import java.io.IOException;
5
6 /**
7 * Decode base64 encoded data and show it as hex bytes. See https://en.wikipedia.org/wiki/Base64
8 * <p>
9 * Copyright (c) 2013-2019 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
10 * BSD-style license. See <a href="https://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
11 * <p>
12 * @version $Revision$, $LastChangedDate$, by $Author$, initial version Jan 7, 2019 <br>
13 * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
14 * @author <a href="https://www.tudelft.nl/staff/p.knoppers/">Peter Knoppers</a>
15 * @author <a href="https://www.transport.citg.tudelft.nl">Wouter Schakel</a>
16 */
17 public class Base64Decoder implements Decoder
18 {
19 /** Assembling space for decoded data. */
20 private int notYetDecodedData = 0;
21
22 /** Number of accumulated bits in <code>notYetDecodedData</code>. */
23 private int accumulatedBits = 0;
24
25 /** Dumper used internally to assemble the decoded data into hex values and char values. */
26 final private Dumper<Base64Decoder> internalDumper = new Dumper<>();
27
28 /** Collector for the output of the internal dumper. */
29 final private ByteArrayOutputStream baos;
30
31 /** Count number of equals (=) symbols seen. */
32 private int endOfInputCharsSeen = 0;
33
34 /** Set when an error is detected in the input stream. */
35 private boolean errorDetected = false;
36
37 /**
38 * Construct a new Base64Decoder.
39 * @param decodedBytesPerLine int; maximum number of decoded input characters resulting in one output line
40 * @param extraSpaceAfterEvery int; insert an extra space after every N output fields (a multiple of 3 makes sense for the
41 * base64 decoder because base64 encodes three bytes into 4 characters)
42 */
43 public Base64Decoder(int decodedBytesPerLine, int extraSpaceAfterEvery)
44 {
45 this.baos = new ByteArrayOutputStream();
46 this.internalDumper.setOutputStream(this.baos);
47 int maximumBytesPerOutputLine = (decodedBytesPerLine + 3) / 4 * 3;
48 this.internalDumper.addDecoder(new HexDecoder(maximumBytesPerOutputLine, extraSpaceAfterEvery));
49 this.internalDumper.addDecoder(new FixedString(" "));
50 this.internalDumper.addDecoder(new CharDecoder(maximumBytesPerOutputLine, extraSpaceAfterEvery));
51 }
52
53 /** {@inheritDoc} */
54 @Override
55 public String getResult()
56 {
57 try
58 {
59 this.internalDumper.flush();
60 String result = this.baos.toString();
61 this.baos.reset();
62 return result;
63 }
64 catch (IOException ioe)
65 {
66 // Cannot happen because writing to a ByteArrayOutputStream should never fail
67 return null;
68 }
69 }
70
71 /** {@inheritDoc} */
72 @Override
73 public int getMaximumWidth()
74 {
75 return this.internalDumper.getMaximumWidth();
76 }
77
78 /** {@inheritDoc} */
79 @Override
80 public boolean append(int address, byte theByte) throws IOException
81 {
82 if (theByte == 61)
83 {
84 this.endOfInputCharsSeen++; // equals
85 }
86 if (this.endOfInputCharsSeen > 0)
87 {
88 return false; // This decoder does not handle multiple base64 encoded objects in its input
89 }
90 int value;
91 if (theByte >= 48 && theByte <= 57)
92 {
93 value = theByte - 48 + 52; // Digit
94 }
95 else if (theByte >= 65 && theByte <= 90)
96 {
97 value = theByte - 65 + 0; // Capital letter
98 }
99 else if (theByte >= 97 && theByte <= 122)
100 {
101 value = theByte - 97 + 26;
102 }
103 else if (theByte == 43 || theByte == 45 || theByte == 46)
104 {
105 value = 62; // Plus or minus or dot
106 }
107 else if (theByte == 47 || theByte == 95 || theByte == 44)
108 {
109 value = 63; // Slash or underscore or comma
110 }
111 else if (theByte <= 32)
112 {
113 return false; // White space can appear anywhere and should be ignored (but this test for white space is bad)
114 }
115 else
116 {
117 // Illegal byte in input
118 if (!this.errorDetected) // First error
119 {
120 // At this point we might insert some indicator in the output to indicate the location of the first error
121 }
122 this.errorDetected = true;
123 return false;
124 }
125 this.notYetDecodedData = (this.notYetDecodedData << 6) + value;
126 this.accumulatedBits += 6;
127 if (this.accumulatedBits >= 8)
128 {
129 int byteValue = this.notYetDecodedData >> (this.accumulatedBits - 8);
130 this.accumulatedBits -= 8;
131 this.notYetDecodedData -= byteValue << this.accumulatedBits;
132 boolean result = this.internalDumper.append((byte) byteValue);
133 return result;
134 }
135 return false;
136 }
137
138 /** {@inheritDoc} */
139 @Override
140 public boolean ignoreForIdenticalOutputCheck()
141 {
142 return false;
143 }
144
145 }