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