View Javadoc
1   package org.djutils.decoderdumper;
2   
3   import java.io.ByteArrayOutputStream;
4   import java.io.IOException;
5   
6   import org.djutils.logger.CategoryLogger;
7   
8   /**
9    * Decode base64 encoded data and show it as hex bytes. See https://en.wikipedia.org/wiki/Base64
10   * <p>
11   * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
12   * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
13   * </p>
14   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
15   * @author <a href="https://www.tudelft.nl/staff/p.knoppers/">Peter Knoppers</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      private final Dumper<Base64Decoder> internalDumper = new Dumper<>();
27  
28      /** Collector for the output of the internal dumper. */
29      private final 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(final int decodedBytesPerLine, final 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      @Override
54      public String getResult()
55      {
56          try
57          {
58              this.internalDumper.flush();
59              String result = this.baos.toString("UTF-8");
60              this.baos.reset();
61              return result;
62          }
63          catch (IOException ioe)
64          {
65              // Cannot happen because writing to a ByteArrayOutputStream should never fail
66              return null;
67          }
68      }
69  
70      @Override
71      public int getMaximumWidth()
72      {
73          return this.internalDumper.getMaximumWidth();
74      }
75  
76      @Override
77      public boolean append(final int address, final byte theByte) throws IOException
78      {
79          if (theByte == 61)
80          {
81              this.endOfInputCharsSeen++; // equals
82          }
83          if (this.endOfInputCharsSeen > 0)
84          {
85              return false; // This decoder does not handle multiple base64 encoded objects in its input
86          }
87          int value;
88          if (theByte >= 48 && theByte <= 57)
89          {
90              value = theByte - 48 + 52; // Digit
91          }
92          else if (theByte >= 65 && theByte <= 90)
93          {
94              value = theByte - 65 + 0; // Capital letter
95          }
96          else if (theByte >= 97 && theByte <= 122)
97          {
98              value = theByte - 97 + 26;
99          }
100         else if (theByte == 43 || theByte == 45 || theByte == 46)
101         {
102             value = 62; // Plus or minus or dot
103         }
104         else if (theByte == 47 || theByte == 95 || theByte == 44)
105         {
106             value = 63; // Slash or underscore or comma
107         }
108         else if (theByte <= 32)
109         {
110             return false; // White space can appear anywhere and should be ignored (but this test for white space is bad)
111         }
112         else
113         {
114             // Illegal byte in input
115             if (!this.errorDetected) // First error
116             {
117                 CategoryLogger.always().info("illegal character found in Base64Decoder stream at address {}, character {}",
118                         address, theByte);
119             }
120             this.errorDetected = true;
121             return false;
122         }
123         this.notYetDecodedData = (this.notYetDecodedData << 6) + value;
124         this.accumulatedBits += 6;
125         if (this.accumulatedBits >= 8)
126         {
127             int byteValue = this.notYetDecodedData >> (this.accumulatedBits - 8);
128             this.accumulatedBits -= 8;
129             this.notYetDecodedData -= byteValue << this.accumulatedBits;
130             boolean result = this.internalDumper.append((byte) byteValue);
131             return result;
132         }
133         return false;
134     }
135 
136     @Override
137     public boolean ignoreForIdenticalOutputCheck()
138     {
139         return false;
140     }
141 
142 }