package org.jnode.fs.ntfs.logfile; import org.jnode.fs.ntfs.NTFSStructure; /** * $LogFile log record. * * @author Luke Quinane */ public class LogRecord extends NTFSStructure { /** * The length of the fixed part of the record. */ public static int HEADER_SIZE = 0x58; /** * The position inside the structure from which {@link #getClientDataLength()} is calculated. */ public static int LENGTH_CALCULATION_OFFSET = 0x30; /** * The record type value for a check point record. */ public static int RECORD_TYPE_CHECKPOINT = 0x2; /** * The flag that indicates the record crosses a page boundary. */ public static int FLAG_CROSSES_PAGE = 0x1; /** * The 'LCNs to follow' value that indicates that there is a subsequent record. */ public static int LCN_FOLLOWING_RECORD = 0x1; /** * The page size for log pages. */ private final int pageSize; /** * The offset in the page to the log record data. */ private final int logPageDataOffset; /** * Creates a new log file record. * @param buffer the buffer. * @param offset the offset in the buffer to create the record at. * @param pageSize the page size. * @param logPageDataOffset the offset in the page to the log record data. */ public LogRecord(byte[] buffer, int offset, int pageSize, int logPageDataOffset) { super(buffer, offset); this.pageSize = pageSize; this.logPageDataOffset = logPageDataOffset; } /** * Checks if the record appears to be valid. * * @return {@code true} if valid. */ public boolean isValid() { return getLsn() != 0; } /** * Gets the log file sequence number for this record. * * @return the LSN. */ public long getLsn() { return getInt64(0x00); } /** * Gets the client previous log file sequence number. * * @return the LSN. */ public long getClientPreviousLsn() { return getInt64(0x08); } /** * Gets the client undo next log file sequence number. * * @return the LSN. */ public long getClientUndoNextLsn() { return getInt64(0x10); } /** * Gets the client data length. * * @return the length. */ public long getClientDataLength() { return getUInt32(0x18); } /** * Gets the sequence number or client index. * * @return the value. */ public int getClientId() { return getUInt16(0x1c); } /** * Gets the record type. * * @return the value. */ public long getRecordType() { return getUInt32(0x20); } /** * Gets the transaction ID. * * @return the transaction ID. */ public long getTransactionId() { return getUInt32(0x24); } /** * Gets the log record flags. * * @return the log record flags. */ public int getFlags() { return getUInt16(0x28); } /** * Gets an unsigned 16-bit integer which may or may not cross the log file page boundary. * * @param offset the offset to the field in this structure. * @return the value. */ protected int getUInt16AcrossPages(int offset) { if (getCrossesPage()) { int offsetWithinPage = getOffset() % pageSize + offset; if (offsetWithinPage + 2 > pageSize) { return getUInt16(offsetWithinPage + logPageDataOffset); } } return getUInt16(offset); } /** * Gets an unsigned 32-bit integer which may or may not cross the log file page boundary. * * @param offset the offset to the field in this structure. * @return the value. */ protected long getUInt32AcrossPages(int offset) { if (getCrossesPage()) { int offsetWithinPage = getOffset() % pageSize + offset; if (offsetWithinPage + 4 > pageSize) { return getUInt32(offsetWithinPage + logPageDataOffset); } } return getUInt32(offset); } /** * Copy (byte-array) data from a given offset which may or may not cross the log file page boundary. * * @param offset the offset to read from in this structure. * @param dst the destination to write to. * @param dstOffset the offset to write from. * @param length the length. */ public final void getDataAcrossPages(int offset, byte[] dst, int dstOffset, int length) { if (getCrossesPage()) { int baseOffset = getOffset() + offset; int offsetWithinPage = baseOffset % pageSize; int pageOffset = baseOffset / pageSize; while (length > 0) { if (pageOffset > getBuffer().length) { // Wrap back around to the start of the 'normal' area pageOffset = LogFile.NORMAL_AREA_START * pageSize; } int endOffset = Math.min(offsetWithinPage + length, pageSize); int readLength = endOffset - offsetWithinPage; getData(pageOffset + offsetWithinPage, dst, dstOffset, readLength); length -= readLength; offsetWithinPage += readLength; dstOffset += readLength; if (offsetWithinPage >= pageSize || readLength == 0) { offsetWithinPage = logPageDataOffset; pageOffset += pageSize; } } } getData(offset, dst, dstOffset, length); } /** * Indicates whether this log record crosses a page boundary. * * @return {@code true} if it crosses a page boundary. */ public boolean getCrossesPage() { return (getFlags() & FLAG_CROSSES_PAGE) == FLAG_CROSSES_PAGE; } /** * Gets the redo operation. * * @return the redo operation. */ public int getRedoOperation() { return getUInt16AcrossPages(0x30); } /** * Gets the undo operation. * * @return the undo operation. */ public int getUndoOperation() { return getUInt16AcrossPages(0x32); } /** * Gets the redo offset. * * @return the redo offset. */ public int getRedoOffset() { return getUInt16AcrossPages(0x34); } /** * Gets the redo length. * * @return the redo length. */ public int getRedoLength() { return getUInt16AcrossPages(0x36); } /** * Gets the undo offset. * * @return the undo offset. */ public int getUndoOffset() { return getUInt16AcrossPages(0x38); } /** * Gets the undo length. * * @return the undo length. */ public int getUndoLength() { return getUInt16AcrossPages(0x3a); } /** * Gets the target attribute. * * @return the attribute. */ public int getTargetAttribute() { return getUInt16AcrossPages(0x3c); } /** * Gets the number of LCN list entries to follow. * * @return the number. */ public int getLcnsToFollow() { return getUInt16AcrossPages(0x3e); } /** * Gets the record offset. * * @return the offset. */ public int getRecordOffset() { return getUInt16AcrossPages(0x40); } /** * Gets the attribute offset. * * @return the offset. */ public int getAttributeOffset() { return getUInt16AcrossPages(0x42); } /** * Gets the MFT cluster index. * * @return the value. */ public int getMftClusterIndex() { return getUInt16AcrossPages(0x44); } /** * Gets the target VCN. * * @return the target VCN. */ public long getTargetVcn() { return getUInt32AcrossPages(0x48); } /** * Gets the target LCN. * * @return the target LCN. */ public long getTargetLcn() { return getUInt32AcrossPages(0x50); } /** * Gets the redo data for the record. * * @param buffer the buffer to write into. */ public void getRedoData(byte[] buffer) { getDataAcrossPages(0x30 + getRedoOffset(), buffer, 0, getRedoLength()); } /** * Gets the undo data for the record. * * @param buffer the buffer to write into. */ public void getUndoData(byte[] buffer) { getDataAcrossPages(0x30 + getUndoOffset(), buffer, 0, getUndoLength()); } @Override public String toString() { String type = ""; if (getRecordType() == RECORD_TYPE_CHECKPOINT) { type = "checkpoint"; } else { OperationCode undoCode = OperationCode.fromCode(getUndoOperation()); if (undoCode != null) { type += undoCode.name(); } else { type += "unknown: " + getUndoOperation(); } type += " --- "; OperationCode redoCode = OperationCode.fromCode(getRedoOperation()); if (redoCode != null) { type += redoCode.name(); } else { type += "unknown: " + getRedoOperation(); } } return String.format("log-record:[%d - %d %s]", getLsn(), getTransactionId(), type); } /** * Gets a debug string for this instance. * * @return the debug string. */ public String toDebugString() { StringBuilder builder = new StringBuilder("Log Record:[\n"); builder.append("lsn: " + getLsn() + "\n"); builder.append("prev-lsn: " + getClientPreviousLsn() + "\n"); builder.append("undo-lsn: " + getClientUndoNextLsn() + "\n"); builder.append("data-length: " + getClientDataLength() + "\n"); builder.append("client-id: " + getClientId() + "\n"); builder.append("record-type: " + getRecordType() + "\n"); builder.append("transaction-id: " + getTransactionId() + "\n"); builder.append("flags: " + getFlags() + "\n"); builder.append("redo: " + getRedoOperation() + "\n"); builder.append("undo: " + getUndoOperation() + "\n"); builder.append("redo-offset: " + getRedoOffset() + "\n"); builder.append("redo-length: " + getRedoLength() + "\n"); builder.append("undo-offset: " + getUndoOffset() + "\n"); builder.append("undo-length: " + getUndoLength() + "\n"); builder.append("target-attribute: " + getTargetAttribute() + "\n"); builder.append("lcns-to-follow: " + getLcnsToFollow() + "\n"); builder.append("record-offset: " + getRecordOffset() + "\n"); builder.append("attribute-offset: " + getAttributeOffset() + "\n"); builder.append("MFT-cluster-index: " + getMftClusterIndex() + "\n"); builder.append("target-vcn: " + getTargetVcn() + "\n"); builder.append("target-lcn: " + getTargetLcn() + "]"); return builder.toString(); } }