Merge "midx.MultiPackIndexPrettyPrinter: pretty printer to debug multi pack index"
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexPrettyPrinter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexPrettyPrinter.java
new file mode 100644
index 0000000..e8428b8
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexPrettyPrinter.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2025, Google Inc.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.internal.storage.midx;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.CHUNK_LOOKUP_WIDTH;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.util.NB;
+
+/**
+ * Prints a multipack index file in a human-readable format.
+ */
+@SuppressWarnings("nls")
+public class MultiPackIndexPrettyPrinter {
+
+	/**
+	 * Writes to out, in human-readable format, the multipack index in rawMidx
+	 *
+	 * @param rawMidx the bytes of a multipack index
+	 * @param out a writer
+	 */
+	public static void prettyPrint(byte[] rawMidx, PrintWriter out) {
+		// Header (12 bytes)
+		out.println("[ 0] Magic: " + new String(rawMidx, 0, 4, UTF_8));
+		out.println("[ 4] Version number: " + (int) rawMidx[4]);
+		out.println("[ 5] OID version: " + (int) rawMidx[5]);
+		int chunkCount = rawMidx[6];
+		out.println("[ 6] # of chunks: " + chunkCount);
+		out.println("[ 7] # of bases: " + (int) rawMidx[7]);
+		int numberOfPacks = NB.decodeInt32(rawMidx, 8);
+		out.println("[ 8] # of packs: " + numberOfPacks);
+
+		// Chunk lookup table
+		List<ChunkSegment> chunkSegments = new ArrayList<>();
+		int current = printChunkLookup(out, rawMidx, chunkCount, chunkSegments);
+
+		for (int i = 0; i < chunkSegments.size() - 1; i++) {
+			ChunkSegment segment = chunkSegments.get(i);
+			if (current != segment.startOffset()) {
+				throw new IllegalStateException(String.format(
+						"We are at byte %d, but segment should start at %d",
+						current, segment.startOffset()));
+			}
+			out.printf("Starting chunk: %s @ %d%n", segment.chunkName(),
+					segment.startOffset());
+			switch (segment.chunkName()) {
+				case "OIDF" -> current = printOIDF(out, rawMidx, current);
+				case "OIDL" -> current = printOIDL(out, rawMidx, current,
+						chunkSegments.get(i + 1).startOffset);
+				case "OOFF" -> current = printOOFF(out, rawMidx, current,
+						chunkSegments.get(i + 1).startOffset);
+				case "PNAM" -> current = printPNAM(out, rawMidx, current,
+						chunkSegments.get(i + 1).startOffset);
+				case "RIDX" -> current = printRIDX(out, rawMidx, current,
+						chunkSegments.get(i + 1).startOffset);
+				default -> {
+					out.printf(
+							"Skipping %s (don't know how to print it yet)%n",
+							segment.chunkName());
+					current = (int) chunkSegments.get(i + 1).startOffset();
+				}
+			}
+		}
+		// Checksum is a SHA-1, use ObjectId to parse it
+		out.printf("[ %d] Checksum %s%n", current,
+				ObjectId.fromRaw(rawMidx, current).name());
+		out.printf("Total size: " + (current + 20));
+	}
+
+	private static int printChunkLookup(PrintWriter out, byte[] rawMidx, int chunkCount,
+			List<ChunkSegment> chunkSegments) {
+		out.println("Starting chunk lookup @ 12");
+		int current = 12;
+		for (int i = 0; i < chunkCount; i++) {
+			String chunkName = new String(rawMidx, current, 4, UTF_8);
+			long offset = NB.decodeInt64(rawMidx, current + 4);
+			out.printf("[ %d] |%8s|%8d|%n", current, chunkName, offset);
+			current += CHUNK_LOOKUP_WIDTH;
+			chunkSegments.add(new ChunkSegment(chunkName, offset));
+		}
+		String chunkName = "0000";
+		long offset = NB.decodeInt64(rawMidx, current + 4);
+		out.printf("[ %d] |%8s|%8d|%n", current, chunkName, offset);
+		current += CHUNK_LOOKUP_WIDTH;
+		chunkSegments.add(new ChunkSegment(chunkName, offset));
+		return current;
+	}
+
+	private static int printOIDF(PrintWriter out, byte[] rawMidx, int start) {
+		int current = start;
+		for (short i = 0; i < 256; i++) {
+			out.printf("[ %d] (%02X) %d%n", current, i,
+					NB.decodeInt32(rawMidx, current));
+			current += 4;
+		}
+		return current;
+	}
+
+	private static int printOIDL(PrintWriter out, byte[] rawMidx, int start, long end) {
+		int i = start;
+		while (i < end) {
+			out.printf("[ %d] %s%n", i,
+					ObjectId.fromRaw(rawMidx, i).name());
+			i += 20;
+		}
+		return i;
+	}
+
+	private static int printOOFF(PrintWriter out, byte[] rawMidx, int start, long end) {
+		int i = start;
+		while (i < end) {
+			out.printf("[ %d] %d %d%n", i, NB.decodeInt32(rawMidx, i),
+					NB.decodeInt32(rawMidx, i + 4));
+			i += 8;
+		}
+		return i;
+	}
+
+	private static int printRIDX(PrintWriter out, byte[] rawMidx, int start, long end) {
+		int i = start;
+		while (i < end) {
+			out.printf("[ %d] %d%n", i, NB.decodeInt32(rawMidx, i));
+			i += 4;
+		}
+		return (int) end;
+	}
+
+	private static int printPNAM(PrintWriter out, byte[] rawMidx, int start, long end) {
+		int nameStart = start;
+		for (int i = start; i < end; i++) {
+			if (rawMidx[i] == 0) {
+				out
+						.println(new String(rawMidx, nameStart, i - nameStart));
+				nameStart = i + 1;
+			}
+		}
+		return (int) end;
+	}
+
+	private record ChunkSegment(String chunkName, long startOffset) {
+	}
+}