ReftableReader.java
/*
* Copyright (C) 2017, Google Inc.
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
* under the terms of the Eclipse Distribution License v1.0 which
* accompanies this distribution, is reproduced below, and is
* available at http://www.eclipse.org/org/documents/edl-v10.php
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* - Neither the name of the Eclipse Foundation, Inc. nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.eclipse.jgit.internal.storage.reftable;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jgit.internal.storage.reftable.BlockReader.decodeBlockLen;
import static org.eclipse.jgit.internal.storage.reftable.ReftableConstants.FILE_BLOCK_TYPE;
import static org.eclipse.jgit.internal.storage.reftable.ReftableConstants.FILE_FOOTER_LEN;
import static org.eclipse.jgit.internal.storage.reftable.ReftableConstants.FILE_HEADER_LEN;
import static org.eclipse.jgit.internal.storage.reftable.ReftableConstants.INDEX_BLOCK_TYPE;
import static org.eclipse.jgit.internal.storage.reftable.ReftableConstants.LOG_BLOCK_TYPE;
import static org.eclipse.jgit.internal.storage.reftable.ReftableConstants.REF_BLOCK_TYPE;
import static org.eclipse.jgit.internal.storage.reftable.ReftableConstants.VERSION_1;
import static org.eclipse.jgit.internal.storage.reftable.ReftableConstants.isFileHeaderMagic;
import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.zip.CRC32;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.internal.storage.io.BlockSource;
import org.eclipse.jgit.internal.storage.reftable.BlockWriter.LogEntry;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.ReflogEntry;
import org.eclipse.jgit.util.LongList;
import org.eclipse.jgit.util.LongMap;
import org.eclipse.jgit.util.NB;
/**
* Reads a reftable formatted file.
* <p>
* {@code ReftableReader} is not thread-safe. Concurrent readers need their own
* instance to read from the same file.
*/
public class ReftableReader extends Reftable {
private final BlockSource src;
private int blockSize = -1;
private long minUpdateIndex;
private long maxUpdateIndex;
private long refEnd;
private long objPosition;
private long objEnd;
private long logPosition;
private long logEnd;
private int objIdLen;
private long refIndexPosition = -1;
private long objIndexPosition = -1;
private long logIndexPosition = -1;
private BlockReader refIndex;
private BlockReader objIndex;
private BlockReader logIndex;
private LongMap<BlockReader> indexCache;
/**
* Initialize a new reftable reader.
*
* @param src
* the file content to read.
*/
public ReftableReader(BlockSource src) {
this.src = src;
}
/**
* Get the block size in bytes chosen for this file by the writer.
*
* @return the block size in bytes chosen for this file by the writer. Most
* reads from the
* {@link org.eclipse.jgit.internal.storage.io.BlockSource} will be
* aligned to the block size.
* @throws java.io.IOException
* file cannot be read.
*/
public int blockSize() throws IOException {
if (blockSize == -1) {
readFileHeader();
}
return blockSize;
}
/**
* Get the minimum update index for log entries that appear in this
* reftable.
*
* @return the minimum update index for log entries that appear in this
* reftable. This should be 1 higher than the prior reftable's
* {@code maxUpdateIndex} if this table is used in a stack.
* @throws java.io.IOException
* file cannot be read.
*/
public long minUpdateIndex() throws IOException {
if (blockSize == -1) {
readFileHeader();
}
return minUpdateIndex;
}
/**
* Get the maximum update index for log entries that appear in this
* reftable.
*
* @return the maximum update index for log entries that appear in this
* reftable. This should be 1 higher than the prior reftable's
* {@code maxUpdateIndex} if this table is used in a stack.
* @throws java.io.IOException
* file cannot be read.
*/
public long maxUpdateIndex() throws IOException {
if (blockSize == -1) {
readFileHeader();
}
return maxUpdateIndex;
}
/** {@inheritDoc} */
@Override
public RefCursor allRefs() throws IOException {
if (blockSize == -1) {
readFileHeader();
}
long end = refEnd > 0 ? refEnd : (src.size() - FILE_FOOTER_LEN);
src.adviseSequentialRead(0, end);
RefCursorImpl i = new RefCursorImpl(end, null, false);
i.block = readBlock(0, end);
return i;
}
/** {@inheritDoc} */
@Override
public RefCursor seekRef(String refName) throws IOException {
initRefIndex();
byte[] key = refName.getBytes(UTF_8);
RefCursorImpl i = new RefCursorImpl(refEnd, key, false);
i.block = seek(REF_BLOCK_TYPE, key, refIndex, 0, refEnd);
return i;
}
/** {@inheritDoc} */
@Override
public RefCursor seekRefsWithPrefix(String prefix) throws IOException {
initRefIndex();
byte[] key = prefix.getBytes(UTF_8);
RefCursorImpl i = new RefCursorImpl(refEnd, key, true);
i.block = seek(REF_BLOCK_TYPE, key, refIndex, 0, refEnd);
return i;
}
/** {@inheritDoc} */
@Override
public RefCursor byObjectId(AnyObjectId id) throws IOException {
initObjIndex();
ObjCursorImpl i = new ObjCursorImpl(refEnd, id);
if (objIndex != null) {
i.initSeek();
} else {
i.initScan();
}
return i;
}
/** {@inheritDoc} */
@Override
public LogCursor allLogs() throws IOException {
initLogIndex();
if (logPosition > 0) {
src.adviseSequentialRead(logPosition, logEnd);
LogCursorImpl i = new LogCursorImpl(logEnd, null);
i.block = readBlock(logPosition, logEnd);
return i;
}
return new EmptyLogCursor();
}
/** {@inheritDoc} */
@Override
public LogCursor seekLog(String refName, long updateIndex)
throws IOException {
initLogIndex();
if (logPosition > 0) {
byte[] key = LogEntry.key(refName, updateIndex);
byte[] match = refName.getBytes(UTF_8);
LogCursorImpl i = new LogCursorImpl(logEnd, match);
i.block = seek(LOG_BLOCK_TYPE, key, logIndex, logPosition, logEnd);
return i;
}
return new EmptyLogCursor();
}
private BlockReader seek(byte blockType, byte[] key, BlockReader idx,
long startPos, long endPos) throws IOException {
if (idx != null) {
// Walk through a possibly multi-level index to a leaf block.
BlockReader block = idx;
do {
if (block.seekKey(key) > 0) {
return null;
}
long pos = block.readPositionFromIndex();
block = readBlock(pos, endPos);
} while (block.type() == INDEX_BLOCK_TYPE);
block.seekKey(key);
return block;
}
return binarySearch(blockType, key, startPos, endPos);
}
private BlockReader binarySearch(byte blockType, byte[] key,
long startPos, long endPos) throws IOException {
if (blockSize == 0) {
BlockReader b = readBlock(startPos, endPos);
if (blockType != b.type()) {
return null;
}
b.seekKey(key);
return b;
}
int low = (int) (startPos / blockSize);
int end = blocksIn(startPos, endPos);
BlockReader block = null;
do {
int mid = (low + end) >>> 1;
block = readBlock(((long) mid) * blockSize, endPos);
if (blockType != block.type()) {
return null;
}
int cmp = block.seekKey(key);
if (cmp < 0) {
end = mid;
} else if (cmp == 0) {
break;
} else /* if (cmp > 0) */ {
low = mid + 1;
}
} while (low < end);
return block;
}
private void readFileHeader() throws IOException {
readHeaderOrFooter(0, FILE_HEADER_LEN);
}
private void readFileFooter() throws IOException {
int ftrLen = FILE_FOOTER_LEN;
byte[] ftr = readHeaderOrFooter(src.size() - ftrLen, ftrLen);
CRC32 crc = new CRC32();
crc.update(ftr, 0, ftrLen - 4);
if (crc.getValue() != NB.decodeUInt32(ftr, ftrLen - 4)) {
throw new IOException(JGitText.get().invalidReftableCRC);
}
refIndexPosition = NB.decodeInt64(ftr, 24);
long p = NB.decodeInt64(ftr, 32);
objPosition = p >>> 5;
objIdLen = (int) (p & 0x1f);
objIndexPosition = NB.decodeInt64(ftr, 40);
logPosition = NB.decodeInt64(ftr, 48);
logIndexPosition = NB.decodeInt64(ftr, 56);
if (refIndexPosition > 0) {
refEnd = refIndexPosition;
} else if (objPosition > 0) {
refEnd = objPosition;
} else if (logPosition > 0) {
refEnd = logPosition;
} else {
refEnd = src.size() - ftrLen;
}
if (objPosition > 0) {
if (objIndexPosition > 0) {
objEnd = objIndexPosition;
} else if (logPosition > 0) {
objEnd = logPosition;
} else {
objEnd = src.size() - ftrLen;
}
}
if (logPosition > 0) {
if (logIndexPosition > 0) {
logEnd = logIndexPosition;
} else {
logEnd = src.size() - ftrLen;
}
}
}
private byte[] readHeaderOrFooter(long pos, int len) throws IOException {
ByteBuffer buf = src.read(pos, len);
if (buf.position() != len) {
throw new IOException(JGitText.get().shortReadOfBlock);
}
byte[] tmp = new byte[len];
buf.flip();
buf.get(tmp);
if (!isFileHeaderMagic(tmp, 0, len)) {
throw new IOException(JGitText.get().invalidReftableFile);
}
int v = NB.decodeInt32(tmp, 4);
int version = v >>> 24;
if (VERSION_1 != version) {
throw new IOException(MessageFormat.format(
JGitText.get().unsupportedReftableVersion,
Integer.valueOf(version)));
}
if (blockSize == -1) {
blockSize = v & 0xffffff;
}
minUpdateIndex = NB.decodeInt64(tmp, 8);
maxUpdateIndex = NB.decodeInt64(tmp, 16);
return tmp;
}
private void initRefIndex() throws IOException {
if (refIndexPosition < 0) {
readFileFooter();
}
if (refIndex == null && refIndexPosition > 0) {
refIndex = readIndex(refIndexPosition);
}
}
private void initObjIndex() throws IOException {
if (objIndexPosition < 0) {
readFileFooter();
}
if (objIndex == null && objIndexPosition > 0) {
objIndex = readIndex(objIndexPosition);
}
}
private void initLogIndex() throws IOException {
if (logIndexPosition < 0) {
readFileFooter();
}
if (logIndex == null && logIndexPosition > 0) {
logIndex = readIndex(logIndexPosition);
}
}
private BlockReader readIndex(long pos) throws IOException {
int sz = readBlockLen(pos);
BlockReader i = new BlockReader();
i.readBlock(src, pos, sz);
i.verifyIndex();
return i;
}
private int readBlockLen(long pos) throws IOException {
int sz = pos == 0 ? FILE_HEADER_LEN + 4 : 4;
ByteBuffer tmp = src.read(pos, sz);
if (tmp.position() < sz) {
throw new IOException(JGitText.get().invalidReftableFile);
}
byte[] buf;
if (tmp.hasArray() && tmp.arrayOffset() == 0) {
buf = tmp.array();
} else {
buf = new byte[sz];
tmp.flip();
tmp.get(buf);
}
if (pos == 0 && buf[FILE_HEADER_LEN] == FILE_BLOCK_TYPE) {
return FILE_HEADER_LEN;
}
int p = pos == 0 ? FILE_HEADER_LEN : 0;
return decodeBlockLen(NB.decodeInt32(buf, p));
}
private BlockReader readBlock(long pos, long end) throws IOException {
if (indexCache != null) {
BlockReader b = indexCache.get(pos);
if (b != null) {
return b;
}
}
int sz = blockSize;
if (sz == 0) {
sz = readBlockLen(pos);
} else if (pos + sz > end) {
sz = (int) (end - pos); // last block may omit padding.
}
BlockReader b = new BlockReader();
b.readBlock(src, pos, sz);
if (b.type() == INDEX_BLOCK_TYPE && !b.truncated()) {
if (indexCache == null) {
indexCache = new LongMap<>();
}
indexCache.put(pos, b);
}
return b;
}
private int blocksIn(long pos, long end) {
int blocks = (int) ((end - pos) / blockSize);
return end % blockSize == 0 ? blocks : (blocks + 1);
}
/**
* Get size of the reftable, in bytes.
*
* @return size of the reftable, in bytes.
* @throws java.io.IOException
* size cannot be obtained.
*/
public long size() throws IOException {
return src.size();
}
/** {@inheritDoc} */
@Override
public void close() throws IOException {
src.close();
}
private class RefCursorImpl extends RefCursor {
private final long scanEnd;
private final byte[] match;
private final boolean prefix;
private Ref ref;
BlockReader block;
RefCursorImpl(long scanEnd, byte[] match, boolean prefix) {
this.scanEnd = scanEnd;
this.match = match;
this.prefix = prefix;
}
@Override
public boolean next() throws IOException {
for (;;) {
if (block == null || block.type() != REF_BLOCK_TYPE) {
return false;
} else if (!block.next()) {
long pos = block.endPosition();
if (pos >= scanEnd) {
return false;
}
block = readBlock(pos, scanEnd);
continue;
}
block.parseKey();
if (match != null && !block.match(match, prefix)) {
block.skipValue();
return false;
}
ref = block.readRef(minUpdateIndex);
if (!includeDeletes && wasDeleted()) {
continue;
}
return true;
}
}
@Override
public Ref getRef() {
return ref;
}
@Override
public void close() {
// Do nothing.
}
}
private class LogCursorImpl extends LogCursor {
private final long scanEnd;
private final byte[] match;
private String refName;
private long updateIndex;
private ReflogEntry entry;
BlockReader block;
LogCursorImpl(long scanEnd, byte[] match) {
this.scanEnd = scanEnd;
this.match = match;
}
@Override
public boolean next() throws IOException {
for (;;) {
if (block == null || block.type() != LOG_BLOCK_TYPE) {
return false;
} else if (!block.next()) {
long pos = block.endPosition();
if (pos >= scanEnd) {
return false;
}
block = readBlock(pos, scanEnd);
continue;
}
block.parseKey();
if (match != null && !block.match(match, false)) {
block.skipValue();
return false;
}
refName = block.name();
updateIndex = block.readLogUpdateIndex();
entry = block.readLogEntry();
if (entry == null && !includeDeletes) {
continue;
}
return true;
}
}
@Override
public String getRefName() {
return refName;
}
@Override
public long getUpdateIndex() {
return updateIndex;
}
@Override
public ReflogEntry getReflogEntry() {
return entry;
}
@Override
public void close() {
// Do nothing.
}
}
static final LongList EMPTY_LONG_LIST = new LongList(0);
private class ObjCursorImpl extends RefCursor {
private final long scanEnd;
private final ObjectId match;
private Ref ref;
private int listIdx;
private LongList blockPos;
private BlockReader block;
ObjCursorImpl(long scanEnd, AnyObjectId id) {
this.scanEnd = scanEnd;
this.match = id.copy();
}
void initSeek() throws IOException {
byte[] rawId = new byte[OBJECT_ID_LENGTH];
match.copyRawTo(rawId, 0);
byte[] key = Arrays.copyOf(rawId, objIdLen);
BlockReader b = objIndex;
do {
if (b.seekKey(key) > 0) {
blockPos = EMPTY_LONG_LIST;
return;
}
long pos = b.readPositionFromIndex();
b = readBlock(pos, objEnd);
} while (b.type() == INDEX_BLOCK_TYPE);
b.seekKey(key);
while (b.next()) {
b.parseKey();
if (b.match(key, false)) {
blockPos = b.readBlockPositionList();
if (blockPos == null) {
initScan();
return;
}
break;
}
b.skipValue();
}
if (blockPos == null) {
blockPos = EMPTY_LONG_LIST;
}
if (blockPos.size() > 0) {
long pos = blockPos.get(listIdx++);
block = readBlock(pos, scanEnd);
}
}
void initScan() throws IOException {
block = readBlock(0, scanEnd);
}
@Override
public boolean next() throws IOException {
for (;;) {
if (block == null || block.type() != REF_BLOCK_TYPE) {
return false;
} else if (!block.next()) {
long pos;
if (blockPos != null) {
if (listIdx >= blockPos.size()) {
return false;
}
pos = blockPos.get(listIdx++);
} else {
pos = block.endPosition();
}
if (pos >= scanEnd) {
return false;
}
block = readBlock(pos, scanEnd);
continue;
}
block.parseKey();
ref = block.readRef(minUpdateIndex);
ObjectId id = ref.getObjectId();
if (id != null && match.equals(id)
&& (includeDeletes || !wasDeleted())) {
return true;
}
}
}
@Override
public Ref getRef() {
return ref;
}
@Override
public void close() {
// Do nothing.
}
}
}