tikhomirov@22: /* tikhomirov@533: * Copyright (c) 2010-2013 TMate Software Ltd tikhomirov@73: * tikhomirov@73: * This program is free software; you can redistribute it and/or modify tikhomirov@73: * it under the terms of the GNU General Public License as published by tikhomirov@73: * the Free Software Foundation; version 2 of the License. tikhomirov@73: * tikhomirov@73: * This program is distributed in the hope that it will be useful, tikhomirov@73: * but WITHOUT ANY WARRANTY; without even the implied warranty of tikhomirov@73: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the tikhomirov@73: * GNU General Public License for more details. tikhomirov@73: * tikhomirov@73: * For information on how to redistribute this software under tikhomirov@73: * the terms of a license other than GNU General Public License tikhomirov@102: * contact TMate Software at support@hg4j.com tikhomirov@22: */ tikhomirov@73: package org.tmatesoft.hg.internal; tikhomirov@0: tikhomirov@0: import java.io.BufferedInputStream; tikhomirov@392: import java.io.ByteArrayInputStream; tikhomirov@0: import java.io.DataInput; tikhomirov@0: import java.io.DataInputStream; tikhomirov@0: import java.io.File; tikhomirov@0: import java.io.FileInputStream; tikhomirov@392: import java.io.IOException; tikhomirov@392: import java.io.UnsupportedEncodingException; tikhomirov@0: import java.math.BigInteger; tikhomirov@376: import java.nio.ByteBuffer; tikhomirov@376: import java.nio.channels.FileChannel; tikhomirov@533: import java.util.regex.Matcher; tikhomirov@533: import java.util.regex.Pattern; tikhomirov@583: import java.util.zip.DataFormatException; tikhomirov@0: import java.util.zip.Inflater; tikhomirov@0: tikhomirov@0: /** tikhomirov@73: * Utility to test/debug/troubleshoot tikhomirov@73: * tikhomirov@73: * @author Artem Tikhomirov tikhomirov@73: * @author TMate Software Ltd. tikhomirov@0: */ tikhomirov@73: public class RevlogDump { tikhomirov@0: tikhomirov@73: /** tikhomirov@73: * Takes 3 command line arguments - tikhomirov@73: * repository path, tikhomirov@73: * path to index file (i.e. store/data/hello.c.i) in the repository (relative) tikhomirov@73: * and "dumpData" whether to print actual content or just revlog headers tikhomirov@73: */ tikhomirov@0: public static void main(String[] args) throws Exception { tikhomirov@73: String repo = "/temp/hg/hello/.hg/"; tikhomirov@73: String filename = "store/00changelog.i"; tikhomirov@22: // String filename = "store/data/hello.c.i"; tikhomirov@4: // String filename = "store/data/docs/readme.i"; tikhomirov@583: // System.out.println(escape("abc\0def\nzxc\tmnb")); tikhomirov@392: boolean dumpDataFull = true; tikhomirov@392: boolean dumpDataStats = false; tikhomirov@88: if (args.length > 1) { tikhomirov@73: repo = args[0]; tikhomirov@73: filename = args[1]; tikhomirov@392: dumpDataFull = args.length > 2 ? "dumpData".equals(args[2]) : false; tikhomirov@392: dumpDataStats = args.length > 2 ? "dumpDataStats".equals(args[2]) : false; tikhomirov@73: } tikhomirov@392: final boolean needRevData = dumpDataFull || dumpDataStats; tikhomirov@0: // tikhomirov@583: RevlogReader rr = new RevlogReader(new File(repo, filename)).needData(needRevData); tikhomirov@583: rr.init(needRevData); tikhomirov@583: System.out.printf("%#8x, inline: %b\n", rr.versionField, rr.inlineData); tikhomirov@73: System.out.println("Index Offset Flags Packed Actual Base Rev Link Rev Parent1 Parent2 nodeid"); tikhomirov@583: ByteBuffer data = null; tikhomirov@583: while (rr.hasMore()) { tikhomirov@583: rr.readNext(); tikhomirov@583: System.out.printf("%4d:%14d %6X %10d %10d %10d %10d %8d %8d %040x\n", rr.entryIndex, rr.offset, rr.flags, rr.compressedLen, rr.actualLen, rr.baseRevision, rr.linkRevision, rr.parent1Revision, rr.parent2Revision, new BigInteger(rr.nodeid)); tikhomirov@392: if (needRevData) { tikhomirov@583: String resultString; tikhomirov@583: if (rr.getDataLength() == 0) { tikhomirov@376: resultString = ""; tikhomirov@3: } else { tikhomirov@583: data = ensureCapacity(data, rr.getDataLength()); tikhomirov@583: rr.getData(data); tikhomirov@583: data.flip(); tikhomirov@583: resultString = buildString(data, rr.isPatch(), dumpDataFull); tikhomirov@3: } tikhomirov@583: if (resultString.endsWith("\n")) { tikhomirov@583: System.out.print(resultString); tikhomirov@583: } else { tikhomirov@583: System.out.println(resultString); tikhomirov@583: } tikhomirov@0: } tikhomirov@0: } tikhomirov@583: rr.done(); tikhomirov@0: } tikhomirov@392: tikhomirov@583: private static ByteBuffer ensureCapacity(ByteBuffer src, int requiredCap) { tikhomirov@583: if (src == null || src.capacity() < requiredCap) { tikhomirov@583: return ByteBuffer.allocate((1 + requiredCap) * 3 / 2); tikhomirov@583: } tikhomirov@583: src.clear(); tikhomirov@583: return src; tikhomirov@583: } tikhomirov@583: tikhomirov@583: private static String buildString(ByteBuffer data, boolean isPatch, boolean completeDataDump) throws IOException, UnsupportedEncodingException { tikhomirov@392: if (isPatch) { tikhomirov@583: DataInputStream dis = new DataInputStream(new ByteArrayInputStream(data.array(), data.arrayOffset(), data.remaining())); tikhomirov@392: StringBuilder sb = new StringBuilder(); tikhomirov@392: sb.append(":\n"); tikhomirov@392: while (dis.available() > 0) { tikhomirov@392: int s = dis.readInt(); tikhomirov@392: int e = dis.readInt(); tikhomirov@392: int l = dis.readInt(); tikhomirov@392: sb.append(String.format("%d..%d, %d", s, e, l)); tikhomirov@392: if (completeDataDump) { tikhomirov@392: byte[] src = new byte[l]; tikhomirov@392: dis.read(src, 0, l); tikhomirov@392: sb.append(":"); tikhomirov@533: sb.append(escape(new String(src, 0, l, "UTF-8"))); tikhomirov@392: } else { tikhomirov@392: dis.skipBytes(l); tikhomirov@392: } tikhomirov@392: sb.append('\n'); tikhomirov@392: } tikhomirov@392: return sb.toString(); tikhomirov@392: } else { tikhomirov@392: if (completeDataDump) { tikhomirov@583: return escape(new String(data.array(), data.arrayOffset(), data.remaining(), "UTF-8")); tikhomirov@392: } tikhomirov@583: return String.format(":%d bytes", data.remaining()); tikhomirov@392: } tikhomirov@392: } tikhomirov@533: tikhomirov@533: private static Pattern controlCharPattern = Pattern.compile("\\p{Cntrl}"); tikhomirov@533: // \p{Cntrl} A control character: [\x00-\x1F\x7F] tikhomirov@533: private static String[] replacements = new String[33]; tikhomirov@533: static { tikhomirov@533: for (int i = 0; i < 32; i++) { tikhomirov@533: // no idea why need FOUR backslashes to get only one in printout tikhomirov@533: replacements[i] = String.format("\\\\%X", i); tikhomirov@533: } tikhomirov@533: replacements[32] = String.format("\\\\%X", 127); tikhomirov@533: } tikhomirov@533: // handy to get newline-separated data printed on newlines. tikhomirov@533: // set to false for non-printable data (e.g. binaries, where \n doesn't make sense) tikhomirov@533: private static boolean leaveNewlineInData = true; tikhomirov@533: tikhomirov@533: private static String escape(CharSequence possiblyWithBinary) { tikhomirov@533: Matcher m = controlCharPattern.matcher(possiblyWithBinary); tikhomirov@533: StringBuffer rv = new StringBuffer(); tikhomirov@533: while (m.find()) { tikhomirov@533: char c = m.group().charAt(0); tikhomirov@533: if (leaveNewlineInData && c == '\n') { tikhomirov@533: continue; tikhomirov@533: } tikhomirov@533: int x = (int) c; tikhomirov@533: m.appendReplacement(rv, replacements[x == 127 ? 32 : x]); tikhomirov@533: } tikhomirov@533: m.appendTail(rv); tikhomirov@533: return rv.toString(); tikhomirov@533: } tikhomirov@583: tikhomirov@583: public static class RevlogReader { tikhomirov@583: tikhomirov@583: private final File file; tikhomirov@583: private boolean needRevData; tikhomirov@583: private DataInputStream dis; tikhomirov@583: private boolean inlineData; tikhomirov@583: public int versionField; tikhomirov@583: private FileChannel dataStream; tikhomirov@583: public int entryIndex; tikhomirov@583: private byte[] data; tikhomirov@583: private int dataOffset, dataLen; tikhomirov@583: public long offset; tikhomirov@583: public int flags; tikhomirov@583: public int baseRevision; tikhomirov@583: public int linkRevision; tikhomirov@583: public int parent1Revision; tikhomirov@583: public int parent2Revision; tikhomirov@583: public int compressedLen; tikhomirov@583: public int actualLen; tikhomirov@583: public byte[] nodeid = new byte[21]; // need 1 byte in the front to be 0 to avoid negative BigInts tikhomirov@583: tikhomirov@583: public RevlogReader(File f) { tikhomirov@583: assert f.getName().endsWith(".i"); tikhomirov@583: file = f; tikhomirov@583: } tikhomirov@583: tikhomirov@583: // affects #readNext() tikhomirov@583: public RevlogReader needData(boolean needData) { tikhomirov@583: needRevData = needData; tikhomirov@583: return this; tikhomirov@583: } tikhomirov@583: tikhomirov@583: public void init(boolean mayRequireData) throws IOException { tikhomirov@583: dis = new DataInputStream(new BufferedInputStream(new FileInputStream(file))); tikhomirov@583: DataInput di = dis; tikhomirov@583: dis.mark(10); tikhomirov@583: versionField = di.readInt(); tikhomirov@583: dis.reset(); tikhomirov@583: final int INLINEDATA = 1 << 16; tikhomirov@583: inlineData = (versionField & INLINEDATA) != 0; tikhomirov@583: tikhomirov@583: dataStream = null; tikhomirov@583: if (!inlineData && mayRequireData) { tikhomirov@583: String fname = file.getAbsolutePath(); tikhomirov@583: dataStream = new FileInputStream(new File(fname.substring(0, fname.length()-2) + ".d")).getChannel(); tikhomirov@583: } tikhomirov@583: tikhomirov@583: entryIndex = -1; tikhomirov@583: } tikhomirov@583: tikhomirov@583: public void startFrom(int startEntryIndex) throws IOException { tikhomirov@583: if (dis == null) { tikhomirov@583: throw new IllegalStateException("Call #init() first"); tikhomirov@583: } tikhomirov@583: if (entryIndex != -1 && startEntryIndex != 0) { tikhomirov@583: throw new IllegalStateException("Can't seek once iteration has started"); tikhomirov@583: } tikhomirov@583: if (dataStream == null) { tikhomirov@583: throw new IllegalStateException("Sorry, initial seek is now supported for separate .i/.d only"); tikhomirov@583: } tikhomirov@583: long newPos = startEntryIndex * Internals.REVLOGV1_RECORD_SIZE, actualSkip; tikhomirov@583: do { tikhomirov@583: actualSkip = dis.skip(newPos); tikhomirov@583: if (actualSkip <= 0) { tikhomirov@583: throw new IllegalStateException(String.valueOf(actualSkip)); tikhomirov@583: } tikhomirov@583: newPos -= actualSkip; tikhomirov@583: } while (newPos > 0); tikhomirov@583: entryIndex = startEntryIndex - 1; tikhomirov@583: } tikhomirov@583: tikhomirov@583: public boolean hasMore() throws IOException { tikhomirov@583: return dis.available() > 0; tikhomirov@583: } tikhomirov@583: tikhomirov@583: public void readNext() throws IOException, DataFormatException { tikhomirov@583: entryIndex++; tikhomirov@583: DataInput di = dis; tikhomirov@583: long l = di.readLong(); tikhomirov@583: offset = entryIndex == 0 ? 0 : (l >>> 16); tikhomirov@583: flags = (int) (l & 0x0FFFF); tikhomirov@583: compressedLen = di.readInt(); tikhomirov@583: actualLen = di.readInt(); tikhomirov@583: baseRevision = di.readInt(); tikhomirov@583: linkRevision = di.readInt(); tikhomirov@583: parent1Revision = di.readInt(); tikhomirov@583: parent2Revision = di.readInt(); tikhomirov@583: di.readFully(nodeid, 1, 20); tikhomirov@583: dis.skipBytes(12); tikhomirov@583: // CAN'T USE skip() here without extra precautions. E.g. I ran into situation when tikhomirov@583: // buffer was 8192 and BufferedInputStream was at position 8182 before attempt to skip(12). tikhomirov@583: // BIS silently skips available bytes and leaves me two extra bytes that ruin the rest of the code. tikhomirov@583: data = new byte[compressedLen]; tikhomirov@583: if (inlineData) { tikhomirov@583: di.readFully(data); tikhomirov@583: } else if (needRevData) { tikhomirov@583: dataStream.position(offset); tikhomirov@583: dataStream.read(ByteBuffer.wrap(data)); tikhomirov@583: } tikhomirov@583: if (needRevData) { tikhomirov@583: if (compressedLen == 0) { tikhomirov@583: data = null; tikhomirov@583: dataOffset = dataLen = 0; tikhomirov@583: } else { tikhomirov@583: if (data[0] == 0x78 /* 'x' */) { tikhomirov@583: Inflater zlib = new Inflater(); tikhomirov@583: zlib.setInput(data, 0, compressedLen); tikhomirov@583: byte[] result = new byte[actualLen * 3]; tikhomirov@583: int resultLen = zlib.inflate(result); tikhomirov@583: zlib.end(); tikhomirov@583: data = result; tikhomirov@583: dataOffset = 0; tikhomirov@583: dataLen = resultLen; tikhomirov@583: } else if (data[0] == 0x75 /* 'u' */) { tikhomirov@583: dataOffset = 1; tikhomirov@583: dataLen = data.length - 1; tikhomirov@583: } else { tikhomirov@583: dataOffset = 0; tikhomirov@583: dataLen = data.length; tikhomirov@583: } tikhomirov@583: } tikhomirov@583: } tikhomirov@583: } tikhomirov@583: tikhomirov@583: public int getDataLength() { tikhomirov@583: // NOT actualLen - there are empty patch revisions (dataLen == 0, but actualLen == previous length) tikhomirov@583: // NOT compressedLen - zip data is uncompressed tikhomirov@583: return dataLen; tikhomirov@583: } tikhomirov@583: tikhomirov@583: public void getData(ByteBuffer bb) { tikhomirov@583: assert bb.remaining() >= dataLen; tikhomirov@583: bb.put(data, dataOffset, dataLen); tikhomirov@583: } tikhomirov@583: tikhomirov@583: public boolean isPatch() { tikhomirov@583: assert entryIndex != -1; tikhomirov@583: return baseRevision != entryIndex; tikhomirov@583: } tikhomirov@583: tikhomirov@583: public boolean isInline() { tikhomirov@583: assert dis != null; tikhomirov@583: return inlineData; tikhomirov@583: } tikhomirov@583: tikhomirov@583: public void done() throws IOException { tikhomirov@583: dis.close(); tikhomirov@583: dis = null; tikhomirov@583: if (dataStream != null) { tikhomirov@583: dataStream.close(); tikhomirov@583: dataStream = null; tikhomirov@583: } tikhomirov@583: } tikhomirov@583: } tikhomirov@0: }