001    /*
002     * This file is part of muCommander, http://www.mucommander.com
003     * Copyright (C) 2002-2008 Maxence Bernard
004     *
005     * muCommander is free software; you can redistribute it and/or modify
006     * it under the terms of the GNU General Public License as published by
007     * the Free Software Foundation; either version 3 of the License, or
008     * (at your option) any later version.
009     *
010     * muCommander is distributed in the hope that it will be useful,
011     * but WITHOUT ANY WARRANTY; without even the implied warranty of
012     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013     * GNU General Public License for more details.
014     *
015     * You should have received a copy of the GNU General Public License
016     * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017     */
018    
019    
020    package com.mucommander.io.base64;
021    
022    import java.io.IOException;
023    import java.io.OutputStream;
024    
025    
026    /**
027     * This <code>OuputStream</code> encodes supplied data to Base64 encoding and writes it to the underlying
028     * <code>OutputStream</code>.
029     *
030     * @see Base64Encoder
031     * @author Maxence Bernard, with the exception of the Base64 description which was found on the Web.
032     */
033    public class Base64OutputStream extends OutputStream {
034        /*
035          Base64 uses a 65 character subset of US-ASCII,
036          allowing 6 bits for each character so the character
037          "m" with a Base64 value of 38, when represented
038          in binary form, is 100110.
039    
040          With a text string, let's say "men" is encoded this
041          is what happens :
042    
043          The text string is converted into its US-ASCII value.
044    
045          The character "m" has the decimal value of 109
046          The character "e" has the decimal value of 101
047          The character "n" has the decimal value of 110
048    
049          When converted to binary the string looks like this :
050    
051          m   01101101
052          e   01100101
053          n   01101110
054    
055          These three "8-bits" are concatenated to make a
056          24 bit stream
057          011011010110010101101110
058    
059          This 24 bit stream is then split up into 4 6-bit
060          sections
061          011011 010110 010101 101110
062    
063          We now have 4 values. These binary values are
064          converted to decimal form
065          27     22     21     46
066    
067          And the corresponding Base64 character are :
068          b      W       V     u
069    
070          The encoding is always on a three characters basis
071          (to have a set of 4 Base64 characters). To encode one
072          or two then, we use the special character "=" to pad
073          until 4 base64 characters is reached.
074    
075          ex. encode "me"
076    
077          01101101  01100101
078          0110110101100101
079          011011 010110 0101
080          111111    (AND to fill the missing bits)
081          011011 010110 010100
082          b     W      U
083          b     W      U     =  ("=" is the padding character)
084    
085          so "bWU="  is the base64 equivalent.
086    
087          encode "m"
088    
089          01101101
090          011011 01
091          111111         (AND to fill the missing bits)
092          011011 010000
093          b     Q     =  =   (two paddings are added)
094    
095          Finally, MIME specifies that lines are 76 characters wide maximum.
096    
097        */
098    
099        /** Base 64 translation table */
100        final static char BASE64_ENCODING_TABLE[] = {
101            'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P',
102            'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f',
103            'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v',
104            'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/'
105        };
106    
107            
108        /** Underlying OutputStream encoded data is sent to */
109        private OutputStream out;
110    
111        /** Array used to accumulate the first 2 bytes of a 3-byte group */
112        private byte byteAcc[] = new byte[2];
113            
114        /** Number of bytes accumulated to form a 3-byte group */
115        private int nbBytesWaiting;
116    
117        /** Specifies whether line breaks should be inserted after 80 chars */
118        private boolean insertLineBreaks;
119    
120        /** Current line length (to insert line return character after 80 chars)*/
121        private int lineLength;
122    
123    
124        /**
125         * Creates a new Base64OutputStream using the underlying OutputStream to write the base64-encoded data to.
126         *
127         * @param out the underlying OutputStream to write the base64-encoded data to
128         * @param insertLineBreaks if <code>true</code>, line breaks will be inserted after every 80 characters written
129         */
130        public Base64OutputStream(OutputStream out, boolean insertLineBreaks) {
131            this.out = out;
132            this.insertLineBreaks = insertLineBreaks;
133        }
134    
135        /**
136         * Writes padding '=' characters to the underlying <code>OutputStream</code> if there currently is an
137         * unfinished 3-byte group. If it's not the case, then this method is a no-op.
138         *
139         * @throws IOException if the padding characters could not be written to the underlying OutputStream.
140         */
141        public void writePadding() throws IOException {
142            // No padding needed
143            if(nbBytesWaiting==0)
144                return;
145    
146            // 1 padding '=' character
147            if (nbBytesWaiting==2) {
148                // 2 bytes left
149                out.write(BASE64_ENCODING_TABLE[(byte)((byteAcc[0] & 0xFC) >> 2)]);
150                out.write(BASE64_ENCODING_TABLE[(byte)(((byteAcc[0] & 0x03) << 4) | ((byteAcc[1] & 0xF0) >> 4))]);
151                out.write(BASE64_ENCODING_TABLE[(byte)((byteAcc[1] & 0x0F) << 2)]);
152                out.write('=');
153            }
154            // 2 padding '=' characters
155            else if (nbBytesWaiting==1) {
156                // 1 byte left
157                out.write(BASE64_ENCODING_TABLE[(byte)((byteAcc[0] & 0xFC) >> 2)]);
158                out.write(BASE64_ENCODING_TABLE[(byte)((byteAcc[0] & 0x03) << 4)]);
159                out.write('=');
160                out.write('=');
161            }
162    
163            // Just in case this method is called again
164            nbBytesWaiting = 0;
165        }
166    
167    
168        /////////////////////////////////
169        // OutputStream implementation //
170        /////////////////////////////////
171    
172        public void write(int i) throws IOException {
173            // We have a 3-byte group
174            if(nbBytesWaiting==2) {
175                // Write 3 bytes as 4 base64 characters
176                out.write(BASE64_ENCODING_TABLE[(byte)((byteAcc[0] & 0xFC) >> 2)]);
177                out.write(BASE64_ENCODING_TABLE[(byte)(((byteAcc[0] & 0x03) << 4) | ((byteAcc[1] & 0xF0) >> 4))]);
178                out.write(BASE64_ENCODING_TABLE[(byte)(((byteAcc[1] & 0x0F) << 2) | ((i & 0xC0) >> 6))]);
179                out.write(BASE64_ENCODING_TABLE[(byte)(i & 0x3F)]);
180    
181                nbBytesWaiting = 0;
182    
183                // Insert a line break after every 80 characters written
184                if (insertLineBreaks && (lineLength += 4) >= 76) {
185                    out.write('\r');
186                    out.write('\n');
187                    lineLength = 0;
188                }
189            }
190            // Waiting for more bytes...
191            else {
192                byteAcc[nbBytesWaiting++] = (byte)i;
193            }
194        }
195    
196        /**
197         * Writes padding if necessary and closes the underlying stream.
198         */
199        public void close() throws IOException {
200            writePadding();
201            out.close();
202        }
203    }