package org.apache.poi.hssf.dev;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import org.apache.poi.hssf.record.*;
import org.apache.poi.hssf.record.RecordInputStream.LeftoverDataException;
import org.apache.poi.hssf.record.chart.*;
import org.apache.poi.hssf.record.pivottable.DataItemRecord;
import org.apache.poi.hssf.record.pivottable.ExtendedPivotTableViewFieldsRecord;
import org.apache.poi.hssf.record.pivottable.PageItemRecord;
import org.apache.poi.hssf.record.pivottable.StreamIDRecord;
import org.apache.poi.hssf.record.pivottable.ViewDefinitionRecord;
import org.apache.poi.hssf.record.pivottable.ViewFieldsRecord;
import org.apache.poi.hssf.record.pivottable.ViewSourceRecord;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.util.HexDump;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.LittleEndian;
import org.apache.poi.util.POILogFactory;
import org.apache.poi.util.POILogger;
import org.apache.poi.util.StringUtil;
import org.apache.poi.util.SuppressForbidden;
public final class BiffViewer {
private static final char[] NEW_LINE_CHARS = System.getProperty("line.separator").toCharArray();
private static final POILogger logger = POILogFactory.getLogger(BiffViewer.class);
private BiffViewer() {
}
private static void createRecords(InputStream is, PrintWriter ps, BiffRecordListener recListener, boolean dumpInterpretedRecords)
throws org.apache.poi.util.RecordFormatException {
RecordInputStream recStream = new RecordInputStream(is);
while (true) {
boolean hasNext;
try {
hasNext = recStream.hasNextRecord();
} catch (LeftoverDataException e) {
logger.log(POILogger.ERROR, "Discarding " + recStream.remaining() + " bytes and continuing", e);
recStream.readRemainder();
hasNext = recStream.hasNextRecord();
}
if (!hasNext) {
break;
}
recStream.nextRecord();
if (recStream.getSid() == 0) {
continue;
}
Record record;
if (dumpInterpretedRecords) {
record = createRecord (recStream);
if (record.getSid() == ContinueRecord.sid) {
continue;
}
for (String header : recListener.getRecentHeaders()) {
ps.println(header);
}
ps.print(record);
} else {
recStream.readRemainder();
}
ps.println();
}
}
private static Record createRecord(RecordInputStream in) {
switch (in.getSid()) {
case AreaFormatRecord.sid: return new AreaFormatRecord(in);
case AreaRecord.sid: return new AreaRecord(in);
case ArrayRecord.sid: return new ArrayRecord(in);
case AxisLineFormatRecord.sid: return new AxisLineFormatRecord(in);
case AxisOptionsRecord.sid: return new AxisOptionsRecord(in);
case AxisParentRecord.sid: return new AxisParentRecord(in);
case AxisRecord.sid: return new AxisRecord(in);
case AxisUsedRecord.sid: return new AxisUsedRecord(in);
case AutoFilterInfoRecord.sid: return new AutoFilterInfoRecord(in);
case BOFRecord.sid: return new BOFRecord(in);
case BackupRecord.sid: return new BackupRecord(in);
case BarRecord.sid: return new BarRecord(in);
case BeginRecord.sid: return new BeginRecord(in);
case BlankRecord.sid: return new BlankRecord(in);
case BookBoolRecord.sid: return new BookBoolRecord(in);
case BoolErrRecord.sid: return new BoolErrRecord(in);
case BottomMarginRecord.sid: return new BottomMarginRecord(in);
case BoundSheetRecord.sid: return new BoundSheetRecord(in);
case CFHeaderRecord.sid: return new CFHeaderRecord(in);
case CFHeader12Record.sid: return new CFHeader12Record(in);
case CFRuleRecord.sid: return new CFRuleRecord(in);
case CFRule12Record.sid: return new CFRule12Record(in);
case CalcCountRecord.sid: return new CalcCountRecord(in);
case CalcModeRecord.sid: return new CalcModeRecord(in);
case CategorySeriesAxisRecord.sid:return new CategorySeriesAxisRecord(in);
case ChartFormatRecord.sid: return new ChartFormatRecord(in);
case ChartRecord.sid: return new ChartRecord(in);
case CodepageRecord.sid: return new CodepageRecord(in);
case ColumnInfoRecord.sid: return new ColumnInfoRecord(in);
case ContinueRecord.sid: return new ContinueRecord(in);
case CountryRecord.sid: return new CountryRecord(in);
case DBCellRecord.sid: return new DBCellRecord(in);
case DSFRecord.sid: return new DSFRecord(in);
case DatRecord.sid: return new DatRecord(in);
case DataFormatRecord.sid: return new DataFormatRecord(in);
case DateWindow1904Record.sid: return new DateWindow1904Record(in);
case DConRefRecord.sid: return new DConRefRecord(in);
case DefaultColWidthRecord.sid: return new DefaultColWidthRecord(in);
case DefaultDataLabelTextPropertiesRecord.sid: return new DefaultDataLabelTextPropertiesRecord(in);
case DefaultRowHeightRecord.sid: return new DefaultRowHeightRecord(in);
case DeltaRecord.sid: return new DeltaRecord(in);
case DimensionsRecord.sid: return new DimensionsRecord(in);
case DrawingGroupRecord.sid: return new DrawingGroupRecord(in);
case DrawingRecordForBiffViewer.sid: return new DrawingRecordForBiffViewer(in);
case DrawingSelectionRecord.sid: return new DrawingSelectionRecord(in);
case DVRecord.sid: return new DVRecord(in);
case DVALRecord.sid: return new DVALRecord(in);
case EOFRecord.sid: return new EOFRecord(in);
case EndRecord.sid: return new EndRecord(in);
case ExtSSTRecord.sid: return new ExtSSTRecord(in);
case ExtendedFormatRecord.sid: return new ExtendedFormatRecord(in);
case ExternSheetRecord.sid: return new ExternSheetRecord(in);
case ExternalNameRecord.sid: return new ExternalNameRecord(in);
case FeatRecord.sid: return new FeatRecord(in);
case FeatHdrRecord.sid: return new FeatHdrRecord(in);
case FilePassRecord.sid: return new FilePassRecord(in);
case FileSharingRecord.sid: return new FileSharingRecord(in);
case FnGroupCountRecord.sid: return new FnGroupCountRecord(in);
case FontBasisRecord.sid: return new FontBasisRecord(in);
case FontIndexRecord.sid: return new FontIndexRecord(in);
case FontRecord.sid: return new FontRecord(in);
case FooterRecord.sid: return new FooterRecord(in);
case FormatRecord.sid: return new FormatRecord(in);
case FormulaRecord.sid: return new FormulaRecord(in);
case FrameRecord.sid: return new FrameRecord(in);
case GridsetRecord.sid: return new GridsetRecord(in);
case GutsRecord.sid: return new GutsRecord(in);
case HCenterRecord.sid: return new HCenterRecord(in);
case HeaderRecord.sid: return new HeaderRecord(in);
case HideObjRecord.sid: return new HideObjRecord(in);
case HorizontalPageBreakRecord.sid: return new HorizontalPageBreakRecord(in);
case HyperlinkRecord.sid: return new HyperlinkRecord(in);
case IndexRecord.sid: return new IndexRecord(in);
case InterfaceEndRecord.sid: return InterfaceEndRecord.create(in);
case InterfaceHdrRecord.sid: return new InterfaceHdrRecord(in);
case IterationRecord.sid: return new IterationRecord(in);
case LabelRecord.sid: return new LabelRecord(in);
case LabelSSTRecord.sid: return new LabelSSTRecord(in);
case LeftMarginRecord.sid: return new LeftMarginRecord(in);
case LegendRecord.sid: return new LegendRecord(in);
case LineFormatRecord.sid: return new LineFormatRecord(in);
case LinkedDataRecord.sid: return new LinkedDataRecord(in);
case MMSRecord.sid: return new MMSRecord(in);
case MergeCellsRecord.sid: return new MergeCellsRecord(in);
case MulBlankRecord.sid: return new MulBlankRecord(in);
case MulRKRecord.sid: return new MulRKRecord(in);
case NameRecord.sid: return new NameRecord(in);
case NameCommentRecord.sid: return new NameCommentRecord(in);
case NoteRecord.sid: return new NoteRecord(in);
case NumberRecord.sid: return new NumberRecord(in);
case ObjRecord.sid: return new ObjRecord(in);
case ObjectLinkRecord.sid: return new ObjectLinkRecord(in);
case PaletteRecord.sid: return new PaletteRecord(in);
case PaneRecord.sid: return new PaneRecord(in);
case PasswordRecord.sid: return new PasswordRecord(in);
case PasswordRev4Record.sid: return new PasswordRev4Record(in);
case PlotAreaRecord.sid: return new PlotAreaRecord(in);
case PlotGrowthRecord.sid: return new PlotGrowthRecord(in);
case PrecisionRecord.sid: return new PrecisionRecord(in);
case PrintGridlinesRecord.sid: return new PrintGridlinesRecord(in);
case PrintHeadersRecord.sid: return new PrintHeadersRecord(in);
case PrintSetupRecord.sid: return new PrintSetupRecord(in);
case ProtectRecord.sid: return new ProtectRecord(in);
case ProtectionRev4Record.sid: return new ProtectionRev4Record(in);
case RKRecord.sid: return new RKRecord(in);
case RecalcIdRecord.sid: return new RecalcIdRecord(in);
case RefModeRecord.sid: return new RefModeRecord(in);
case RefreshAllRecord.sid: return new RefreshAllRecord(in);
case RightMarginRecord.sid: return new RightMarginRecord(in);
case RowRecord.sid: return new RowRecord(in);
case SCLRecord.sid: return new SCLRecord(in);
case SSTRecord.sid: return new SSTRecord(in);
case SaveRecalcRecord.sid: return new SaveRecalcRecord(in);
case SelectionRecord.sid: return new SelectionRecord(in);
case SeriesIndexRecord.sid: return new SeriesIndexRecord(in);
case SeriesListRecord.sid: return new SeriesListRecord(in);
case SeriesRecord.sid: return new SeriesRecord(in);
case SeriesTextRecord.sid: return new SeriesTextRecord(in);
case SeriesToChartGroupRecord.sid:return new SeriesToChartGroupRecord(in);
case SharedFormulaRecord.sid: return new SharedFormulaRecord(in);
case SheetPropertiesRecord.sid: return new SheetPropertiesRecord(in);
case StringRecord.sid: return new StringRecord(in);
case StyleRecord.sid: return new StyleRecord(in);
case SupBookRecord.sid: return new SupBookRecord(in);
case TabIdRecord.sid: return new TabIdRecord(in);
case TableStylesRecord.sid: return new TableStylesRecord(in);
case TableRecord.sid: return new TableRecord(in);
case TextObjectRecord.sid: return new TextObjectRecord(in);
case TextRecord.sid: return new TextRecord(in);
case TickRecord.sid: return new TickRecord(in);
case TopMarginRecord.sid: return new TopMarginRecord(in);
case UncalcedRecord.sid: return new UncalcedRecord(in);
case UnitsRecord.sid: return new UnitsRecord(in);
case UseSelFSRecord.sid: return new UseSelFSRecord(in);
case VCenterRecord.sid: return new VCenterRecord(in);
case ValueRangeRecord.sid: return new ValueRangeRecord(in);
case VerticalPageBreakRecord.sid: return new VerticalPageBreakRecord(in);
case WSBoolRecord.sid: return new WSBoolRecord(in);
case WindowOneRecord.sid: return new WindowOneRecord(in);
case WindowProtectRecord.sid: return new WindowProtectRecord(in);
case WindowTwoRecord.sid: return new WindowTwoRecord(in);
case WriteAccessRecord.sid: return new WriteAccessRecord(in);
case WriteProtectRecord.sid: return new WriteProtectRecord(in);
case CatLabRecord.sid: return new CatLabRecord(in);
case ChartEndBlockRecord.sid: return new ChartEndBlockRecord(in);
case ChartEndObjectRecord.sid: return new ChartEndObjectRecord(in);
case ChartFRTInfoRecord.sid: return new ChartFRTInfoRecord(in);
case ChartStartBlockRecord.sid: return new ChartStartBlockRecord(in);
case ChartStartObjectRecord.sid: return new ChartStartObjectRecord(in);
case StreamIDRecord.sid: return new StreamIDRecord(in);
case ViewSourceRecord.sid: return new ViewSourceRecord(in);
case PageItemRecord.sid: return new PageItemRecord(in);
case ViewDefinitionRecord.sid: return new ViewDefinitionRecord(in);
case ViewFieldsRecord.sid: return new ViewFieldsRecord(in);
case DataItemRecord.sid: return new DataItemRecord(in);
case ExtendedPivotTableViewFieldsRecord.sid: return new ExtendedPivotTableViewFieldsRecord(in);
}
return new UnknownRecord(in);
}
private static final class CommandArgs {
private final boolean _biffhex;
private final boolean _noint;
private final boolean _out;
private final boolean _rawhex;
private final boolean _noHeader;
private final File _file;
private CommandArgs(boolean biffhex, boolean noint, boolean out, boolean rawhex, boolean noHeader, File file) {
_biffhex = biffhex;
_noint = noint;
_out = out;
_rawhex = rawhex;
_file = file;
_noHeader = noHeader;
}
public static CommandArgs parse(String[] args) throws CommandParseException {
int nArgs = args.length;
boolean biffhex = false;
boolean noint = false;
boolean out = false;
boolean rawhex = false;
boolean noheader = false;
File file = null;
for (int i=0; i<nArgs; i++) {
String arg = args[i];
if (arg.startsWith("--")) {
if ("--biffhex".equals(arg)) {
biffhex = true;
} else if ("--noint".equals(arg)) {
noint = true;
} else if ("--out".equals(arg)) {
out = true;
} else if ("--escher".equals(arg)) {
System.setProperty("poi.deserialize.escher", "true");
} else if ("--rawhex".equals(arg)) {
rawhex = true;
} else if ("--noheader".equals(arg)) {
noheader = true;
} else {
throw new CommandParseException("Unexpected option '" + arg + "'");
}
continue;
}
file = new File(arg);
if (!file.exists()) {
throw new CommandParseException("Specified file '" + arg + "' does not exist");
}
if (i+1<nArgs) {
throw new CommandParseException("File name must be the last arg");
}
}
if (file == null) {
throw new CommandParseException("Biff viewer needs a filename");
}
return new CommandArgs(biffhex, noint, out, rawhex, noheader, file);
}
boolean shouldDumpBiffHex() {
return _biffhex;
}
boolean shouldDumpRecordInterpretations() {
return !_noint;
}
boolean shouldOutputToFile() {
return _out;
}
boolean shouldOutputRawHexOnly() {
return _rawhex;
}
boolean suppressHeader() {
return _noHeader;
}
public File getFile() {
return _file;
}
}
private static final class CommandParseException extends Exception {
CommandParseException(String msg) {
super(msg);
}
}
public static void main(String[] args) throws IOException, CommandParseException {
CommandArgs cmdArgs = CommandArgs.parse(args);
PrintWriter pw;
if (cmdArgs.shouldOutputToFile()) {
OutputStream os = new FileOutputStream(cmdArgs.getFile().getAbsolutePath() + ".out");
pw = new PrintWriter(new OutputStreamWriter(os, StringUtil.UTF8));
} else {
pw = new PrintWriter(new OutputStreamWriter(System.out, Charset.defaultCharset()));
}
POIFSFileSystem fs = null;
InputStream is = null;
try {
fs = new POIFSFileSystem(cmdArgs.getFile(), true);
is = getPOIFSInputStream(fs);
if (cmdArgs.shouldOutputRawHexOnly()) {
byte[] data = IOUtils.toByteArray(is);
HexDump.dump(data, 0, System.out, 0);
} else {
boolean dumpInterpretedRecords = cmdArgs.shouldDumpRecordInterpretations();
boolean dumpHex = cmdArgs.shouldDumpBiffHex();
runBiffViewer(pw, is, dumpInterpretedRecords, dumpHex, dumpInterpretedRecords,
cmdArgs.suppressHeader());
}
} finally {
IOUtils.closeQuietly(is);
IOUtils.closeQuietly(fs);
IOUtils.closeQuietly(pw);
}
}
static InputStream getPOIFSInputStream(POIFSFileSystem fs) throws IOException {
String workbookName = HSSFWorkbook.getWorkbookDirEntryName(fs.getRoot());
return fs.createDocumentInputStream(workbookName);
}
static void runBiffViewer(PrintWriter pw, InputStream is,
boolean dumpInterpretedRecords, boolean dumpHex, boolean zeroAlignHexDump,
boolean suppressHeader) {
BiffRecordListener recListener = new BiffRecordListener(dumpHex ? pw : null, zeroAlignHexDump, suppressHeader);
is = new BiffDumpingStream(is, recListener);
createRecords(is, pw, recListener, dumpInterpretedRecords);
}
private static final class BiffRecordListener implements IBiffRecordListener {
private final Writer _hexDumpWriter;
private List<String> ;
private final boolean _zeroAlignEachRecord;
private final boolean ;
private BiffRecordListener(Writer hexDumpWriter, boolean zeroAlignEachRecord, boolean noHeader) {
_hexDumpWriter = hexDumpWriter;
_zeroAlignEachRecord = zeroAlignEachRecord;
_noHeader = noHeader;
_headers = new ArrayList<>();
}
@Override
public void processRecord(int globalOffset, int recordCounter, int sid, int dataSize,
byte[] data) {
String header = formatRecordDetails(globalOffset, sid, dataSize, recordCounter);
if(!_noHeader) {
_headers.add(header);
}
Writer w = _hexDumpWriter;
if (w != null) {
try {
w.write(header);
w.write(NEW_LINE_CHARS);
hexDumpAligned(w, data, dataSize+4, globalOffset, _zeroAlignEachRecord);
w.flush();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private List<String> () {
List<String> result = _headers;
_headers = new ArrayList<>();
return result;
}
private static String formatRecordDetails(int globalOffset, int sid, int size, int recordCounter) {
return "Offset=" + HexDump.intToHex(globalOffset) + "(" + globalOffset + ")" +
" recno=" + recordCounter +
" sid=" + HexDump.shortToHex(sid) +
" size=" + HexDump.shortToHex(size) + "(" + size + ")";
}
}
private interface IBiffRecordListener {
void processRecord(int globalOffset, int recordCounter, int sid, int dataSize, byte[] data);
}
private static final class BiffDumpingStream extends InputStream {
private final DataInputStream _is;
private final IBiffRecordListener _listener;
private final byte[] _data;
private int _recordCounter;
private int _overallStreamPos;
private int _currentPos;
private int _currentSize;
private boolean _innerHasReachedEOF;
private BiffDumpingStream(InputStream is, IBiffRecordListener listener) {
_is = new DataInputStream(is);
_listener = listener;
_data = new byte[RecordInputStream.MAX_RECORD_DATA_SIZE + 4];
_recordCounter = 0;
_overallStreamPos = 0;
_currentSize = 0;
_currentPos = 0;
}
@Override
public int read() throws IOException {
if (_currentPos >= _currentSize) {
fillNextBuffer();
}
if (_currentPos >= _currentSize) {
return -1;
}
int result = _data[_currentPos] & 0x00FF;
_currentPos ++;
_overallStreamPos ++;
formatBufferIfAtEndOfRec();
return result;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (b == null || off < 0 || len < 0 || b.length < off+len) {
throw new IllegalArgumentException();
}
if (_currentPos >= _currentSize) {
fillNextBuffer();
}
if (_currentPos >= _currentSize) {
return -1;
}
final int result = Math.min(len, _currentSize - _currentPos);
System.arraycopy(_data, _currentPos, b, off, result);
_currentPos += result;
_overallStreamPos += result;
formatBufferIfAtEndOfRec();
return result;
}
@Override
@SuppressForbidden("just delegating the call")
public int available() throws IOException {
return _currentSize - _currentPos + _is.available();
}
private void fillNextBuffer() throws IOException {
if (_innerHasReachedEOF) {
return;
}
int b0 = _is.read();
if (b0 == -1) {
_innerHasReachedEOF = true;
return;
}
_data[0] = (byte) b0;
_is.readFully(_data, 1, 3);
int len = LittleEndian.getShort(_data, 2);
_is.readFully(_data, 4, len);
_currentPos = 0;
_currentSize = len + 4;
_recordCounter++;
}
private void formatBufferIfAtEndOfRec() {
if (_currentPos != _currentSize) {
return;
}
int dataSize = _currentSize-4;
int sid = LittleEndian.getShort(_data, 0);
int globalOffset = _overallStreamPos-_currentSize;
_listener.processRecord(globalOffset, _recordCounter, sid, dataSize, _data);
}
@Override
public void close() throws IOException {
_is.close();
}
}
private static final int DUMP_LINE_LEN = 16;
private static final char[] COLUMN_SEPARATOR = " | ".toCharArray();
private static void hexDumpAligned(Writer w, byte[] data, int dumpLen, int globalOffset,
boolean zeroAlignEachRecord) {
int baseDataOffset = 0;
int globalStart = globalOffset + baseDataOffset;
int globalEnd = globalOffset + baseDataOffset + dumpLen;
int startDelta = globalStart % DUMP_LINE_LEN;
int endDelta = globalEnd % DUMP_LINE_LEN;
if (zeroAlignEachRecord) {
endDelta -= startDelta;
if (endDelta < 0) {
endDelta += DUMP_LINE_LEN;
}
startDelta = 0;
}
int startLineAddr;
int endLineAddr;
if (zeroAlignEachRecord) {
endLineAddr = globalEnd - endDelta - (globalStart - startDelta);
startLineAddr = 0;
} else {
startLineAddr = globalStart - startDelta;
endLineAddr = globalEnd - endDelta;
}
int lineDataOffset = baseDataOffset - startDelta;
int lineAddr = startLineAddr;
if (startLineAddr == endLineAddr) {
hexDumpLine(w, data, lineAddr, lineDataOffset, startDelta, endDelta);
return;
}
hexDumpLine(w, data, lineAddr, lineDataOffset, startDelta, DUMP_LINE_LEN);
while (true) {
lineAddr += DUMP_LINE_LEN;
lineDataOffset += DUMP_LINE_LEN;
if (lineAddr >= endLineAddr) {
break;
}
hexDumpLine(w, data, lineAddr, lineDataOffset, 0, DUMP_LINE_LEN);
}
if (endDelta != 0) {
hexDumpLine(w, data, lineAddr, lineDataOffset, 0, endDelta);
}
}
private static void hexDumpLine(Writer w, byte[] data, int lineStartAddress, int lineDataOffset, int startDelta, int endDelta) {
final char[] buf = new char[8+2*COLUMN_SEPARATOR.length+DUMP_LINE_LEN*3-1+DUMP_LINE_LEN+NEW_LINE_CHARS.length];
if (startDelta >= endDelta) {
throw new IllegalArgumentException("Bad start/end delta");
}
int idx=0;
try {
writeHex(buf, idx, lineStartAddress, 8);
idx = arraycopy(COLUMN_SEPARATOR, buf, idx+8);
for (int i=0; i< DUMP_LINE_LEN; i++) {
if (i>0) {
buf[idx++] = ' ';
}
if (i >= startDelta && i < endDelta) {
writeHex(buf, idx, data[lineDataOffset+i], 2);
} else {
buf[idx] = ' ';
buf[idx+1] = ' ';
}
idx += 2;
}
idx = arraycopy(COLUMN_SEPARATOR, buf, idx);
for (int i=0; i< DUMP_LINE_LEN; i++) {
char ch = ' ';
if (i >= startDelta && i < endDelta) {
ch = getPrintableChar(data[lineDataOffset+i]);
}
buf[idx++] = ch;
}
idx = arraycopy(NEW_LINE_CHARS, buf, idx);
w.write(buf, 0, idx);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static int arraycopy(char[] in, char[] out, int pos) {
int idx = pos;
for (char c : in) {
out[idx++] = c;
}
return idx;
}
private static char getPrintableChar(byte b) {
char ib = (char) (b & 0x00FF);
if (ib < 32 || ib > 126) {
return '.';
}
return ib;
}
private static void writeHex(char[] buf, int startInBuf, int value, int nDigits) {
int acc = value;
for(int i=nDigits-1; i>=0; i--) {
int digit = acc & 0x0F;
buf[startInBuf+i] = (char) (digit < 10 ? ('0' + digit) : ('A' + digit - 10));
acc >>>= 4;
}
}
}