blob: 673f47dad1787c00779223040c780d05dcffedbe [file] [log] [blame]
/*-
* Copyright (C) 2002, 2018, Oracle and/or its affiliates. All rights reserved.
*
* This file was distributed by Oracle as part of a version of Oracle Berkeley
* DB Java Edition made available at:
*
* http://www.oracle.com/technetwork/database/database-technologies/berkeleydb/downloads/index.html
*
* Please see the LICENSE file included in the top-level directory of the
* appropriate version of Oracle Berkeley DB Java Edition for a copy of the
* license and additional information.
*/
package com.sleepycat.je.utilint;
import java.nio.ByteBuffer;
import java.util.Arrays;
import com.sleepycat.je.DbInternal;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentFailureException;
import com.sleepycat.je.dbi.EnvironmentImpl;
import com.sleepycat.je.log.ChecksumException;
import com.sleepycat.je.log.ChecksumValidator;
import com.sleepycat.je.log.FileHeader;
import com.sleepycat.je.log.LogEntryHeader;
import com.sleepycat.je.log.LogEntryType;
import com.sleepycat.je.log.entry.LogEntry;
import com.sleepycat.je.util.LogVerificationException;
/**
* Verifies the checksums in the contents of a log file in a JE {@code
* Environment}.
*
* <p>The caller supplies the contents of the log file by passing arrays of
* bytes in a series of calls to the {@link #verify} method, which verifies the
* checksums for log records, and by calling the {@link #verifyAtEof} when the
* entire contents are complete, to detect incomplete entries at the end of the
* file. The primary intended use of this class is to verify the contents of
* log files that are being copied as part of a programmatic backup. It is
* critical that invalid files are not added to a backup set, since then both
* the live environment and the backup will be invalid.
*
* @see com.sleepycat.je.util.LogVerificationInputStream
*/
public class LogVerifier {
private static final byte FILE_HEADER_TYPE_NUM =
LogEntryType.LOG_FILE_HEADER.getTypeNum();
private final EnvironmentImpl envImpl;
private final String fileName;
private final long fileNum;
/* Stream verification state information. */
private enum State {
INIT, FIXED_HEADER, VARIABLE_HEADER, ITEM, FILE_HEADER_ITEM, INVALID
}
private State state;
private long entryStart;
private long prevEntryStart;
private final ChecksumValidator validator;
private final ByteBuffer headerBuf;
private LogEntryHeader header;
private int itemPosition;
private int logVersion;
/**
* Creates a log verifier.
*
* @param env the {@code Environment} associated with the log
*
* @param fileName the file name of the log, for reporting in the {@code
* LogVerificationException}. This should be a simple file name of the
* form {@code NNNNNNNN.jdb}, where NNNNNNNN is the file number in
* hexadecimal format.
*
* @throws EnvironmentFailureException if an unexpected, internal or
* environment-wide failure occurs
*/
public LogVerifier(final Environment env, final String fileName) {
this(DbInternal.getNonNullEnvImpl(env), fileName);
}
/**
* Creates a log verifier.
*
* @param envImpl the {@code EnvironmentImpl} associated with the log
*
* @param fileName the file name of the log, for reporting in the {@code
* LogVerificationException}. This should be a simple file name of the
* form {@code NNNNNNNN.jdb}, where NNNNNNNN is the file number in
* hexadecimal format.
*
* @throws EnvironmentFailureException if an unexpected, internal or
* environment-wide failure occurs
*/
public LogVerifier(final EnvironmentImpl envImpl, final String fileName) {
this(envImpl, fileName, -1L);
}
/**
* <p>Creates a log verifier for use with an internal environment. If
* {@code fileNum} is less than zero, it is derived from {@code fileName}.
*
* @param envImpl the {@code EnvironmentImpl} associated with the log
*
* @param fileName the file name of the log, for reporting in the {@code
* LogVerificationException}. This should be a simple file name of the
* form {@code NNNNNNNN.jdb}, where NNNNNNNN is the file number in
* hexadecimal format.
*
* @param fileNum the file number
*/
public LogVerifier(final EnvironmentImpl envImpl,
final String fileName,
final long fileNum) {
this.envImpl = envImpl;
this.fileName = fileName;
this.fileNum = (fileNum >= 0) ?
fileNum : envImpl.getFileManager().getNumFromName(fileName);
state = State.INIT;
entryStart = 0L;
prevEntryStart = 0L;
validator = new ChecksumValidator(envImpl);
/*
* The headerBuf is used to hold the fixed entry header, variable entry
* header portion, and file header entry.
*/
headerBuf = ByteBuffer.allocate
(Math.max(LogEntryHeader.MAX_HEADER_SIZE, FileHeader.entrySize()));
/* Initial log version for reading the file header. */
logVersion = LogEntryType.UNKNOWN_FILE_HEADER_VERSION;
}
/**
* Verifies the next portion of the log file.
*
* @param buf the buffer containing the log file bytes
*
* @param off the start offset of the log file bytes in the buffer
*
* @param len the number of log file bytes in the buffer
*
* @throws LogVerificationException if a checksum cannot be verified or a
* log entry is determined to be invalid by examining its contents
*
* @throws EnvironmentFailureException if an unexpected, internal or
* environment-wide failure occurs
*/
public void verify(final byte[] buf, final int off, final int len)
throws LogVerificationException {
final int endOffset = off + len;
int curOffset = off;
while (curOffset < endOffset) {
final int remaining = endOffset - curOffset;
switch (state) {
case INIT:
processInit();
break;
case FIXED_HEADER:
curOffset = processFixedHeader(buf, curOffset, remaining);
break;
case VARIABLE_HEADER:
curOffset = processVariableHeader(buf, curOffset, remaining);
break;
case FILE_HEADER_ITEM:
curOffset = processFileHeaderItem(buf, curOffset, remaining);
break;
case ITEM:
curOffset = processItem(buf, curOffset, remaining);
break;
case INVALID:
throw newVerifyException
("May not read after LogVerificationException is thrown");
default:
assert false;
}
}
}
/**
* Checks that the log file ends with a complete log entry, after having
* completed verifying the log file contents through calls to {@link
* #verify}.
*
* @throws LogVerificationException if the stream does not end with a
* complete log entry
*
* @throws EnvironmentFailureException if an unexpected, internal or
* environment-wide failure occurs
*/
public void verifyAtEof()
throws LogVerificationException {
/* State should be INIT at EOF. */
if (state == State.INIT) {
return;
}
/* Ignore partial entry at end of last log file. */
if (fileNum == envImpl.getFileManager().getLastFileNum()) {
return;
}
/* Report partial entry at end of any other file. */
throw newVerifyException("Entry is incomplete");
}
/**
* Initializes all state variables before the start of a log entry. Moves
* the state to FIXED_HEADER, the first part of a log entry.
*/
private void processInit() {
validator.reset();
headerBuf.clear();
header = null;
itemPosition = 0;
state = State.FIXED_HEADER;
}
/**
* Processes the fixed initial portion of a log entry. After all bytes for
* the fixed portion are read, moves the state to VARIABLE_HEADER if the
* header contains a variable portion, or to ITEM if it does not.
*/
private int processFixedHeader(final byte[] buf,
final int curOffset,
final int remaining)
throws LogVerificationException {
assert header == null;
final int maxSize = LogEntryHeader.MIN_HEADER_SIZE;
final int processSize =
Math.min(remaining, maxSize - headerBuf.position());
headerBuf.put(buf, curOffset, processSize);
assert headerBuf.position() <= maxSize;
if (headerBuf.position() == maxSize) {
headerBuf.flip();
try {
header = new LogEntryHeader(
headerBuf, logVersion, DbLsn.makeLsn(fileNum, entryStart));
} catch (ChecksumException e) {
throw newVerifyException(
"Invalid header bytes=" +
Arrays.toString(headerBuf.array()),
e);
}
if (header.getPrevOffset() != prevEntryStart) {
throw newVerifyException(
"Header prevOffset=0x" +
Long.toHexString(header.getPrevOffset()) +
" but prevEntryStart=0x" +
Long.toHexString(prevEntryStart));
}
/* If the header is invisible, turn off the invisible bit. */
if (header.isInvisible()) {
LogEntryHeader.turnOffInvisible(headerBuf, 0);
}
/* Do not validate the bytes of the checksum itself. */
if (header.hasChecksum()) {
validator.update(headerBuf.array(),
LogEntryHeader.CHECKSUM_BYTES,
maxSize - LogEntryHeader.CHECKSUM_BYTES);
}
if (header.isVariableLength()) {
headerBuf.clear();
state = State.VARIABLE_HEADER;
} else if (header.getType() == FILE_HEADER_TYPE_NUM) {
headerBuf.clear();
state = State.FILE_HEADER_ITEM;
} else {
state = State.ITEM;
}
}
return curOffset + processSize;
}
/**
* Processes the variable portion of a log entry. After all bytes for the
* variable portion are read, moves the state to ITEM.
*/
private int processVariableHeader(final byte[] buf,
final int curOffset,
final int remaining) {
assert header != null;
assert header.isVariableLength();
final int maxSize = header.getVariablePortionSize();
final int processSize =
Math.min(remaining, maxSize - headerBuf.position());
headerBuf.put(buf, curOffset, processSize);
assert headerBuf.position() <= maxSize;
if (headerBuf.position() == maxSize) {
headerBuf.flip();
header.readVariablePortion(headerBuf);
if (header.hasChecksum()) {
validator.update(headerBuf.array(), 0, maxSize);
}
if (header.getType() == FILE_HEADER_TYPE_NUM) {
headerBuf.clear();
state = State.FILE_HEADER_ITEM;
} else {
state = State.ITEM;
}
}
return curOffset + processSize;
}
private int processFileHeaderItem(final byte[] buf,
final int curOffset,
final int remaining)
throws LogVerificationException {
assert header != null;
assert logVersion == LogEntryType.UNKNOWN_FILE_HEADER_VERSION;
final int maxSize = FileHeader.entrySize();
final int processSize =
Math.min(remaining, maxSize - headerBuf.position());
headerBuf.put(buf, curOffset, processSize);
assert headerBuf.position() <= maxSize;
if (headerBuf.position() == maxSize) {
if (header.hasChecksum()) {
validator.update(headerBuf.array(), 0, maxSize);
try {
validator.validate(
header.getChecksum(), fileNum, entryStart);
} catch (ChecksumException e) {
throw newVerifyException(e);
}
}
headerBuf.flip();
LogEntry fileHeaderEntry =
LogEntryType.LOG_FILE_HEADER.getNewLogEntry();
fileHeaderEntry.readEntry(envImpl, header, headerBuf);
FileHeader fileHeaderItem =
(FileHeader) fileHeaderEntry.getMainItem();
/* Log version in the file header applies to all other entries. */
logVersion = fileHeaderItem.getLogVersion();
prevEntryStart = entryStart;
entryStart += header.getSize() + maxSize;
state = State.INIT;
}
return curOffset + processSize;
}
/**
* Processes the item portion of a log entry. After all bytes for the item
* are read, moves the state back to INIT and bumps the entryStart.
*/
private int processItem(final byte[] buf,
final int curOffset,
final int remaining)
throws LogVerificationException {
assert header != null;
final int maxSize = header.getItemSize();
final int processSize = Math.min(remaining, maxSize - itemPosition);
if (header.hasChecksum()) {
validator.update(buf, curOffset, processSize);
}
itemPosition += processSize;
assert itemPosition <= maxSize;
if (itemPosition == maxSize) {
if (header.hasChecksum()) {
try {
validator.validate(
header.getChecksum(), fileNum, entryStart);
} catch (ChecksumException e) {
/*
LogEntryType lastEntryType =
LogEntryType.findType(header.getType());
System.out.println();
System.out.println(
"Checksum error in logrec of tyoe " +
lastEntryType.toStringNoVersion() +
" log version: " + logVersion);
System.out.println();
*/
throw newVerifyException(e);
}
}
prevEntryStart = entryStart;
entryStart += header.getSize() + maxSize;
state = State.INIT;
}
return curOffset + processSize;
}
private LogVerificationException newVerifyException(String reason) {
return newVerifyException(reason, null);
}
private LogVerificationException newVerifyException(Throwable cause) {
return newVerifyException(cause.toString(), cause);
}
private LogVerificationException newVerifyException(String reason,
Throwable cause) {
state = State.INVALID;
final String logEntrySize;
if (header != null) {
logEntrySize =
String.valueOf(header.getSize() + header.getItemSize());
} else {
logEntrySize = "unknown";
}
return new LogVerificationException
("Log is invalid, fileName: " + fileName +
" fileNumber: 0x" + Long.toHexString(fileNum) +
" logEntryOffset: 0x" + Long.toHexString(entryStart) +
" logEntrySize: " + logEntrySize +
" verifyState: " + state +
" reason: " + reason,
cause);
}
}