package org.jnode.fs.ntfs.logfile;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.apache.log4j.Logger;
import org.jnode.fs.ntfs.FileRecord;
import org.jnode.fs.ntfs.NTFSVolume;
import org.jnode.fs.ntfs.attribute.NTFSAttribute;
import org.jnode.fs.util.FSUtils;
import org.jnode.util.LittleEndian;
/**
* $LogFile
*
* @author Luke Quinane
*/
public class LogFile {
/**
* My logger
*/
protected static final Logger log = Logger.getLogger(LogFile.class);
/**
* The start of the normal area (in pages).
*/
public static final int NORMAL_AREA_START = 4;
/**
* The list of open log clients.
*/
private final List<LogClientRecord> logClients = new ArrayList<LogClientRecord>();
/**
* The offset to the oldest page.
*/
private final int oldestPageOffset;
/**
* The map of offset to record page headers.
*/
private Map<Integer, RecordPageHeader> offsetPageMap = new LinkedHashMap<Integer, RecordPageHeader>();
/**
* The map of LSN to log record.
*/
private Map<Long, LogRecord> lsnLogRecordMap;
/**
* The restart page header.
*/
private final RestartPageHeader restartPageHeader;
/**
* The restart area.
*/
private final RestartArea restartArea;
/**
* The $LogFile page size.
*/
private final int logPageSize;
/**
* The $LogFile size.
*/
private final long logFileLength;
/**
* The buffer that holds the $LogFile data.
*/
private final byte[] logFileBuffer;
/**
* Indicates whether the $LogFile contents need to be checked when reading MFT data.
*/
private boolean cleanlyShutdown = true;
/**
* Creates a new instance.
*
* @param fileRecord the file record to read the $LogFile data from.
* @throws IOException if an error occurs.
*/
public LogFile(FileRecord fileRecord) throws IOException {
// Read in the log file data
logFileLength = fileRecord.getAttributeTotalSize(NTFSAttribute.Types.DATA, null);
logFileBuffer = new byte[(int) logFileLength];
fileRecord.readData(0, logFileBuffer, 0, (int) logFileLength);
// Read in the restart area info
restartPageHeader = getNewestRestartPageHeader(fileRecord.getVolume(), logFileBuffer);
int restartAreaOffset = restartPageHeader.getOffset() + restartPageHeader.getRestartOffset();
logPageSize = restartPageHeader.getLogPageSize();
restartArea = new RestartArea(logFileBuffer, restartAreaOffset);
if ((restartArea.getFlags() & RestartArea.VOLUME_CLEANLY_UNMOUNTED) != RestartArea.VOLUME_CLEANLY_UNMOUNTED) {
log.info("Volume not cleanly unmounted");
cleanlyShutdown = false;
} else {
log.info("Volume marked as cleanly unmounted");
}
// Read in any open log client records
int logClientCount = restartArea.getLogClients();
if (logClientCount != RestartArea.LOGFILE_NO_CLIENT) {
log.info(String.format("Found %d open log clients", logClientCount));
int logClientOffset = restartAreaOffset + restartArea.getClientArrayOffset();
LogClientRecord logClientRecord = new LogClientRecord(logFileBuffer, logClientOffset);
logClients.add(logClientRecord);
for (int i = 1; i <= logClientCount; i++) {
logClientOffset = restartAreaOffset + logClientRecord.getNextClientOffset();
logClientRecord = new LogClientRecord(logFileBuffer, logClientOffset);
logClients.add(logClientRecord);
}
}
oldestPageOffset = findOldestPageOffset(fileRecord.getVolume());
}
/**
* Parses the log records.
*/
private void parseRecords() {
if (lsnLogRecordMap != null) {
// Already parsed
return;
}
lsnLogRecordMap = new LinkedHashMap<Long, LogRecord>();
// The first whole record in the oldest page can start mid-page, so just skip all records in the first page and
// use the last record to calculate the offset to the first record in the next page.
int offset = oldestPageOffset;
RecordPageHeader oldestPage = offsetPageMap.get(offset);
long recordOffset = oldestPage.getNextRecordOffset();
recordOffset = FSUtils.roundUpToBoundary(8, recordOffset);
LogRecord lastRecordOnFirstPage;
long lastRecordLength;
if (recordOffset + LogRecord.LENGTH_CALCULATION_OFFSET > logPageSize) {
// The first record we hit has overflowed to the next page
offset += logPageSize;
recordOffset = restartArea.getLogPageDataOffset();
lastRecordOnFirstPage = new LogRecord(logFileBuffer, (int) (offset + recordOffset), logPageSize,
restartArea.getLogPageDataOffset());
lastRecordLength = lastRecordOnFirstPage.getClientDataLength();
} else {
// Last record on this page with no overflow
lastRecordOnFirstPage = new LogRecord(logFileBuffer, (int) (offset + recordOffset), logPageSize,
restartArea.getLogPageDataOffset());
lastRecordLength = lastRecordOnFirstPage.getClientDataLength();
offset += logPageSize;
lastRecordLength -= logPageSize - restartArea.getLogPageDataOffset();
}
recordOffset = getNextRecordOffset(lastRecordOnFirstPage, recordOffset);
long lastLsn = offsetPageMap.get(offset).getLastLsnOrFileOffset();
int logPageCount = (int) ((logFileLength - NORMAL_AREA_START * logPageSize) / logPageSize);
// Read in each log page
for (int pageNumber = 1; pageNumber < logPageCount; pageNumber++) {
RecordPageHeader pageHeader = offsetPageMap.get(offset);
if (pageHeader != null && pageHeader.isValid()) {
if (pageHeader.getLastLsnOrFileOffset() < lastLsn ||
pageHeader.getLastLsnOrFileOffset() - lastLsn > 0x8000) {
// This page doesn't seem to continue on from the last page, so reset the offsets
log.info(String.format("$LogFile discontinuous at 0x%x [%d -> %d]", offset, lastLsn,
pageHeader.getLastLsnOrFileOffset()));
recordOffset = 0;
lastRecordLength = 0;
}
// Check if this page is filled with data from a previous record
if (lastRecordLength > logPageSize - restartArea.getLogPageDataOffset()) {
recordOffset -= logPageSize - restartArea.getLogPageDataOffset();
lastRecordLength -= logPageSize - restartArea.getLogPageDataOffset();
} else {
// Ensure that the record offset is within the page and beyond the page header
if (recordOffset < logPageSize) {
recordOffset = restartArea.getLogPageDataOffset();
} else {
recordOffset = recordOffset % logPageSize;
recordOffset += restartArea.getLogPageDataOffset();
}
long lastRecordInPage = Math.min(pageHeader.getNextRecordOffset(), logPageSize);
if (lastRecordInPage == 0) {
lastRecordInPage = logPageSize;
}
// Read in the page's log records
while (recordOffset <= lastRecordInPage) {
if (recordOffset + LogRecord.LENGTH_CALCULATION_OFFSET > logPageSize) {
// No more room for records in this page, move to the next page
recordOffset = 0;
break;
}
// Get the offset to the next record in the buffer rounded up to an 8-byte boundary
recordOffset = FSUtils.roundUpToBoundary(8, recordOffset);
LogRecord logRecord = new LogRecord(logFileBuffer, (int) (offset + recordOffset), logPageSize,
restartArea.getLogPageDataOffset());
recordOffset = getNextRecordOffset(logRecord, recordOffset);
long lsn = logRecord.getLsn();
if (logRecord.isValid() && lsn > 0 && lsn <= pageHeader.getLastLsnOrFileOffset()) {
lsnLogRecordMap.put(lsn, logRecord);
lastRecordLength = logRecord.getClientDataLength();
// Account for the portion of the record on this page
int start = (logRecord.getOffset() % logPageSize) + LogRecord.LENGTH_CALCULATION_OFFSET;
lastRecordLength -= logPageSize - start;
} else {
if (lsn <= 0 || lsn > pageHeader.getLastLsnOrFileOffset()) {
log.warn("Log record seems to be invalid: " + logRecord.toDebugString());
}
// Seems to be the end of valid records for this page
lastRecordLength = 0;
break;
}
}
}
lastLsn = pageHeader.getLastLsnOrFileOffset();
} else {
lastLsn = 0;
}
offset += logPageSize;
if (offset >= logFileLength) {
// Wrap around to the start of the 'normal' area.
offset = NORMAL_AREA_START * logPageSize;
}
}
}
/**
* Gets the next record offset.
*
* @param logRecord the current log record.
* @param recordOffset the offset to the current record.
* @return the offset to the next record.
*/
private long getNextRecordOffset(LogRecord logRecord, long recordOffset) {
if (logRecord.isValid()) {
return recordOffset + LogRecord.LENGTH_CALCULATION_OFFSET + (int) logRecord.getClientDataLength();
} else {
// Seems to be the end of valid records for this page
return restartArea.getLogPageDataOffset();
}
}
/**
* Finds the offset to the oldest page, i.e. the one with the lowest LSN.
*
* @param volume the volume that holds the log file.
* @return the offset to the oldest page.
* @throws IOException if an error occurs.
*/
private int findOldestPageOffset(NTFSVolume volume) throws IOException {
TreeMap<Long, RecordPageHeader> lsnPageMap = new TreeMap<Long, RecordPageHeader>();
Map<RecordPageHeader, Integer> pageOffsetMap = new HashMap<RecordPageHeader, Integer>();
// Read in all the page header records. The first two pages are the restart area, and the next two pages are the
// buffer page area, so start reading in page headers from the fifth page. This is the start of the 'normal
// area'.
for (int offset = 4 * logPageSize; offset < logFileLength; offset += logPageSize) {
int magic = LittleEndian.getInt32(logFileBuffer, offset);
if (magic != RecordPageHeader.Magic.RCRD) {
// Bad page magic, possibly an uninitialised page
continue;
}
RecordPageHeader pageHeader = new RecordPageHeader(volume, logFileBuffer, offset);
offsetPageMap.put(offset, pageHeader);
// If the last-end-LSN is zero then the page only contains data from the log record on the last page. I.e.
// it has no new entries, so skip it
if (pageHeader.isValid() && pageHeader.getLastEndLsn() != 0) {
lsnPageMap.put(pageHeader.getLastEndLsn(), pageHeader);
pageOffsetMap.put(pageHeader, offset);
}
}
RecordPageHeader oldestPage = lsnPageMap.firstEntry().getValue();
return pageOffsetMap.get(oldestPage);
}
/**
* Gets the restart page header that corresponds to the restart page with the highest current LSN.
*
* @param volume the volume that holds the log file.
* @param buffer the buffer to read from.
* @return the header.
* @throws IOException if an error occurs.
*/
private RestartPageHeader getNewestRestartPageHeader(NTFSVolume volume, byte[] buffer) throws IOException {
RestartPageHeader restartPageHeader1 = new RestartPageHeader(volume, buffer, 0);
if (!restartPageHeader1.isValid()) {
throw new IllegalStateException("Restart header has invalid magic: " + restartPageHeader1.getMagic());
} else if (restartPageHeader1.getMagic() == RestartPageHeader.Magic.CHKD) {
log.warn("First $LogFile restart header has check disk magic");
}
RestartPageHeader restartPageHeader2 = new RestartPageHeader(volume, buffer, restartPageHeader1.getLogPageSize());
if (!restartPageHeader2.isValid()) {
throw new IllegalStateException("Second restart header has invalid magic: " + restartPageHeader2.getMagic());
} else if (restartPageHeader2.getMagic() == RestartPageHeader.Magic.CHKD) {
log.warn("Second $LogFile restart header has check disk magic");
}
int restartAreaOffset1 = restartPageHeader1.getRestartOffset();
int restartAreaOffset2 = restartPageHeader2.getRestartOffset();
RestartArea restartArea1 = new RestartArea(buffer, restartAreaOffset1);
RestartArea restartArea2 = new RestartArea(buffer, restartPageHeader1.getLogPageSize() + restartAreaOffset2);
// Pick the restart page with the highest current LSN
if (restartArea1.getCurrentLsn() >= restartArea2.getCurrentLsn()) {
return restartPageHeader1;
} else {
return restartPageHeader2;
}
}
/**
* Checks whether the log file seems to be cleanly shutdown.
*
* @return {@code true} if cleanly shutdown.
*/
public boolean isCleanlyShutdown() {
return cleanlyShutdown;
}
/**
* Gets the log file records for this log file.
*
* @return the records.
*/
public Collection<LogRecord> getLogRecords() {
parseRecords();
return lsnLogRecordMap.values();
}
/**
* Gets a mapping of LSN to log record.
*
* @return the map.
*/
public Map<Long, LogRecord> getLsnLogRecordMap() {
parseRecords();
return Collections.unmodifiableMap(lsnLogRecordMap);
}
/**
* Dumps out a chain of log records.
*
* @param lsn the LSN to start from.
* @return the dumped out chain.
*/
public String dumpLogChain(long lsn) {
parseRecords();
List<LogRecord> records = new ArrayList<LogRecord>();
LogRecord midRecord = lsnLogRecordMap.get(lsn);
records.add(midRecord);
LogRecord current = midRecord;
while (current.getClientPreviousLsn() != 0) {
current = lsnLogRecordMap.get(current.getClientPreviousLsn());
records.add(0, current);
}
int midIndex = records.size() - 1;
current = midRecord;
while (current.getClientUndoNextLsn() != 0) {
current = lsnLogRecordMap.get(current.getClientUndoNextLsn());
records.add(current);
}
StringBuilder builder = new StringBuilder();
builder.append("[");
for (int i = 0; i < records.size(); i++) {
LogRecord record = records.get(i);
if (i < midIndex) {
builder.append("<");
} else if (i == midIndex) {
builder.append("=");
} else {
builder.append(">");
}
builder.append(record);
builder.append("\n");
}
builder.append("]");
return builder.toString();
}
}