001    // Copyright (c) 2001 Hursh Jain (http://www.mollypages.org) 
002    // The Molly framework is freely distributable under the terms of an
003    // MIT-style license. For details, see the molly pages web site at:
004    // http://www.mollypages.org/. Use, modify, have fun !
005    
006    package fc.io;
007    
008    import java.io.*;
009    
010    import fc.util.*;
011    
012    /** 
013    Writes supplied bytes in hex/ascii form. Useful for hex dumps and
014    debugging. Each <tt>write(..)</tt> method call is independent of previous
015    invokations and prints data seperately from previous lines. This stream
016    can also optionally print in other bases instead of hex.
017    <p>
018    In addition to it's streamy goodness, this class also provided some misc.
019    static utility functions such as {@link #toHex}
020    
021    @author hursh jain
022    **/
023    public final class HexOutputStream extends FilterOutputStream
024    {
025    static final boolean dbg = false;
026    
027    final PrintStream   out;
028    final String    sp1  = " ";
029    final String    sp2  = "  ";
030    final String    sp3  = "   ";
031    final String    nl   = IOUtil.LINE_SEP;
032    
033    boolean     showHex = true;
034    int       baseValue;
035    
036    //max digits needed to print 0..255 for any given base
037    int       baseMaxWidth = 2; //default for hex  
038    
039    boolean     autoflush = true;
040    boolean     linenumbers = true;
041    int       width;
042    byte[]      tempBuf;  //to collect write(int) calls
043    int       tempBufPtr;
044    int       tempBufLimit;
045    
046    /** 
047    Constructs a new HexOutputStream with a default width of
048    16 hex numbers (and corresponding ascii values) per line.
049    
050    @param   out the underlying outputstream to send the data to
051    **/
052    public HexOutputStream(OutputStream out) 
053      {
054      this (out, 16);
055      }
056    
057    /** 
058    Constructs a new HexOutputStream with the specified column width.
059    
060    @param   out  the underlying outputstream to send the data to
061    @param   width  the number of hex numbers to print per line
062    **/ 
063    public HexOutputStream(OutputStream out, int width) 
064      {
065      super(null);
066      
067      Argcheck.notnull(out);
068      Argcheck.istrue(width > 0, "the specified width must be greater than 0");
069    
070      if (out instanceof PrintStream) 
071        this.out = (PrintStream) out;
072      else
073        this.out = new PrintStream(out);
074    
075      this.width = width; 
076      tempBuf = new byte[width];
077      tempBufLimit = width - 1;
078      if (dbg) System.out.println("Constructed HexOutputStream, out="+out);
079      } 
080      
081    /** 
082    Be careful with this call, if the underlying stream
083    is say, <tt>System.out</tt>, then calling this method will 
084    close <tt>System.out</tt>
085    **/
086    public void close() {
087      out.close();
088      }
089      
090    public void flush() {
091      out.flush();
092      }
093    
094    public void write(int b) 
095      {
096      tempBuf[tempBufPtr++] = (byte) b;
097    
098      if (tempBufPtr == tempBufLimit) {
099        write(tempBuf, 0, tempBufLimit);
100        tempBufPtr = 0;
101        }
102      }
103    
104    public void write(byte[] buf) 
105      {
106      if (buf == null) {
107        out.println("null");
108        return;
109        }
110      write(buf, 0, buf.length);
111      }
112    
113    public void write(byte[] buf, int off, int len) 
114      {
115      if (buf == null) {
116        out.println("null");
117        return;
118        }
119      
120      //faster if we use a buffer as opposed to multiple println()
121      //calls 
122      StringBuffer strbuf = new StringBuffer(buf.length * 3);
123        
124      int totalLen = len - off ;
125    
126      //the absolute index into the buffer of the last character
127      int end = off + len; 
128    
129      int linesToPrint = totalLen / width;
130      int lineNumPrintWidth = Integer.toString(totalLen).length(); 
131      int byteCount = 0;
132      
133      if (dbg) System.out.println("width="+width+"; totallen="+totalLen+"; end="+end+"; linesToPrint="+linesToPrint+"; lineNumPrintWidth="+lineNumPrintWidth);
134    
135      for (int n = off; n < totalLen; n += width)
136        {
137        if (linenumbers && linesToPrint > 0) {
138          strbuf.append(StringUtil.fixedWidth(
139                  String.valueOf(byteCount), 
140                  lineNumPrintWidth, HAlign.RIGHT, '0'));
141          byteCount += width;
142          }
143        
144        strbuf.append("| ");
145        // k cannot exceed lesser of [width (like 16) or the
146        // number of actual characters left in this buffer]
147        int k, c = 0; 
148          
149        int kmax  = n + width;  
150        //k is an absolute index into buf
151        for (k = n; k < kmax && k < end; k++) 
152          {
153          c = buf[k] & 0xFF;  //byte value is now unsigned
154          
155          if (showHex) 
156            {
157            /**
158            //This works but let's see if we can use a 
159            //faster custom impl.
160            if (c < 16) {
161              strbuf.append('0');  //we want 0A not A
162              }
163            strbuf.append(Integer.toHexString(c));
164            **/
165              //->the custom impl.
166            strbuf.append(toHex(c));
167            }
168          else { 
169            strbuf.append(StringUtil.fixedWidth(
170                    Integer.toString(c, baseValue),     
171                    baseMaxWidth, HAlign.RIGHT, '0'));
172            } 
173            
174          strbuf.append(sp1);           
175          }
176                    
177        int pad = 0;
178        if (k == end) { //loop finished before width, pad to align
179          pad = n + width - k;
180          
181          // add 1 to baseMaxWidth to account for space between
182          // each digit group
183      
184          strbuf.append(StringUtil.repeat(' ', 
185                      (baseMaxWidth+1) * pad));  
186          }
187        
188        strbuf.append("| ");
189    
190        //we can repeat the loop, but timewise it seems to be 
191        //about the same as creating 2 string buf's and 
192        //populating them within 1 loop. 2 loop is slower
193        //but 2 bufs is also slow, when appending buf2 to buf1
194        //an arraycopy is done internally inside of stringbuffer
195      
196        for (k = n; k < kmax && k < end; k++) 
197          {
198          c = buf[k] & 0xFF;  //byte value is now unsigned
199          if (c < 32 || c >= 127)
200            strbuf.append('.');     
201          else
202            strbuf.append((char)c);
203          }
204      
205        if (k == end) { //loop finished before width, pad to align
206          strbuf.append(StringUtil.repeat(" ", pad));  
207          }
208    
209        strbuf.append(" |");
210        strbuf.append(nl);
211        }
212        
213      out.print(strbuf.toString()); 
214      if (autoflush) {
215        out.flush();  
216        }
217      }
218    
219    
220    /** 
221    Sets this stream to flush it's contents after every
222    write statement. By default, this is <tt>true</tt>.
223    **/
224    public void setAutoFlush(boolean val) {
225      autoflush = val;
226      }
227    
228    /** 
229    <tt>true/false</tt> enables or disables showing line numbers at
230    the beginning of each line, <i>if</i> there is more than 1 line in
231    the output. The line number prefix is a running counter of the
232    number of bytes displayed so far. This counter is reset after at
233    the beginning of each write method call.
234    **/
235    public void showLineNumbers(boolean val) {
236      linenumbers = val;
237      }
238    
239    /** 
240    Shows output in the specified base (instead of the default hex).
241    To redundantly set to hex, specify <tt>16</tt>. To show decimal,
242    specify <tt>10</tt>.
243    
244    @param  base  the base to show each byte in. Must be between
245            2 and 36 (inclusive)
246    @throws IllegalArgumentException 
247            if the specified base is not between [2, 36]
248    **/
249    public void setBase(int base) 
250      {
251      if (base < 2 || base > 36)
252        throw new IllegalArgumentException("The base must be between 2 and 36 (both inclusive)");
253      baseValue = base;   
254      baseMaxWidth = calcBaseMaxWidth(base); 
255      if (baseValue != 16) {
256        showHex = false;
257        }
258      }
259    
260    
261    // essentially we need to ask the question:
262    // is the number greater than range [0 - base] ? if yes:
263    // then is the number greater than range [0 - base * base] ? 
264    // and so on
265    int calcBaseMaxWidth(int base) {
266      int n = 0;
267      int start = 255;
268      do {
269        start = start / base;
270        n++;
271        }
272      while ( start > 0);
273      return n;
274      }
275    
276    
277    //Fast hex conversion
278    
279    static final char[] hex = "0123456789abcdef".toCharArray();
280    
281    static final char[] toHex(final int c) //the lower 8 bits of c are hexed
282      {
283      final char[] buf = new char[2];
284      if (c < 16) {
285        buf[0] = '0';  //we want 0A not A
286        }
287      else {
288        buf[0] = hex[ (c >> 4) & 0x0F ];
289        }
290      buf[1] = hex[ c & 0x0f ];
291      return buf;
292      }
293    
294    /**
295    Utility method to convert a byte buffer to a hexadecimal string.
296    useful for md5 and other hashes.
297    */
298    public static final String toHex(final byte[] buf)
299      {
300      Argcheck.notnull(buf, "buf was null");
301      final StringBuilder tmp = new StringBuilder();
302      for (int n = 0; n < buf.length; n++)
303        {
304        int c = buf[n] & 0xFF;  //byte value is now unsigned
305        tmp.append(toHex(c));   
306        }
307      return tmp.toString();
308      }
309    
310    /** 
311    Unit Test: 
312    <pre>
313    Usage: java HexOutputStream <options>
314    options:
315      -file filename 
316      or
317      -data string
318    </pre>
319    **/
320    public static void main(String[] args) throws Exception
321      {
322      int cols = -1;
323      HexOutputStream hexout = null;
324      Args myargs = new Args(args);
325      myargs.setUsage("Usage: java HexOutputStream <options>\noptions:\n\t-file filename or -data string\r\n\t-cols num (optional number of display columns)\n\t-base num (optional base 2..36 to show bytes in)");
326    
327      if (myargs.getFlagCount() == 0)
328        myargs.showError();
329        
330      byte[] buf = null;
331    
332      if (myargs.flagExists("file")) {
333        String filename = myargs.get("file");
334        buf = IOUtil.fileToByteArray(filename);
335        }
336      else {  
337        String temp = myargs.getRequired("data");
338        if (temp != null)
339          buf = temp.getBytes();
340        }
341    
342      if (myargs.flagExists("cols"))
343        cols = Integer.parseInt(myargs.get("cols"));
344    
345      if (cols >= 0)
346        hexout= new HexOutputStream(System.out, cols);
347      else
348        hexout = new HexOutputStream(System.out);
349      
350      if (myargs.flagExists("base"))
351        hexout.setBase(Byte.parseByte(myargs.get("base")));
352    
353      long start = -1;
354      if (dbg) start = System.currentTimeMillis();
355      hexout.write(buf);
356      if (dbg) System.out.println("Time: " + (System.currentTimeMillis() - start) + " ms");
357      hexout.close();
358      }
359    
360    }          //~class HexOutputStream