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