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
006package fc.io;
007
008import java.io.*;
009
010import fc.util.*;
011
012/** 
013Writes supplied bytes in hex/ascii form. Useful for hex dumps and
014debugging. Each <tt>write(..)</tt> method call is independent of previous
015invokations and prints data seperately from previous lines. This stream
016can also optionally print in other bases instead of hex.
017<p>
018In addition to it's streamy goodness, this class also provided some misc.
019static utility functions such as {@link #toHex}
020
021@author hursh jain
022**/
023public final class HexOutputStream extends FilterOutputStream
024{
025static final boolean dbg = false;
026
027final PrintStream   out;
028final String    sp1  = " ";
029final String    sp2  = "  ";
030final String    sp3  = "   ";
031final String    nl   = IOUtil.LINE_SEP;
032
033boolean     showHex = true;
034int       baseValue;
035
036//max digits needed to print 0..255 for any given base
037int       baseMaxWidth = 2; //default for hex  
038
039boolean     autoflush = true;
040boolean     linenumbers = true;
041int       width;
042byte[]      tempBuf;  //to collect write(int) calls
043int       tempBufPtr;
044int       tempBufLimit;
045
046/** 
047Constructs a new HexOutputStream with a default width of
04816 hex numbers (and corresponding ascii values) per line.
049
050@param   out the underlying outputstream to send the data to
051**/
052public HexOutputStream(OutputStream out) 
053  {
054  this (out, 16);
055  }
056
057/** 
058Constructs 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**/ 
063public 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/** 
082Be careful with this call, if the underlying stream
083is say, <tt>System.out</tt>, then calling this method will 
084close <tt>System.out</tt>
085**/
086public void close() {
087  out.close();
088  }
089  
090public void flush() {
091  out.flush();
092  }
093
094public 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
104public void write(byte[] buf) 
105  {
106  if (buf == null) {
107    out.println("null");
108    return;
109    }
110  write(buf, 0, buf.length);
111  }
112
113public 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/** 
221Sets this stream to flush it's contents after every
222write statement. By default, this is <tt>true</tt>.
223**/
224public void setAutoFlush(boolean val) {
225  autoflush = val;
226  }
227
228/** 
229<tt>true/false</tt> enables or disables showing line numbers at
230the beginning of each line, <i>if</i> there is more than 1 line in
231the output. The line number prefix is a running counter of the
232number of bytes displayed so far. This counter is reset after at
233the beginning of each write method call.
234**/
235public void showLineNumbers(boolean val) {
236  linenumbers = val;
237  }
238
239/** 
240Shows output in the specified base (instead of the default hex).
241To redundantly set to hex, specify <tt>16</tt>. To show decimal,
242specify <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**/
249public 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
265int 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
279static final char[] hex = "0123456789abcdef".toCharArray();
280
281static 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/**
295Utility method to convert a byte buffer to a hexadecimal string.
296useful for md5 and other hashes.
297*/
298public 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/** 
311Unit Test: 
312<pre>
313Usage: java HexOutputStream <options>
314options:
315  -file filename 
316  or
317  -data string
318</pre>
319**/
320public 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