/*
* $Id$
*
* Copyright (C) 2003-2015 JNode.org
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library; If not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package org.jnode.fs.ntfs;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import org.jnode.fs.ntfs.attribute.AttributeListAttribute;
import org.jnode.fs.ntfs.attribute.AttributeListEntry;
import org.jnode.fs.ntfs.attribute.NTFSAttribute;
import org.jnode.fs.ntfs.attribute.NTFSNonResidentAttribute;
import org.jnode.fs.ntfs.attribute.NTFSResidentAttribute;
import org.jnode.util.NumberUtils;
/**
* MFT file record structure.
*
* @author Chira
* @author Ewout Prangsma (epr@users.sourceforge.net)
* @author Daniel Noll (daniel@noll.id.au) (new attribute iteration support)
*/
public class FileRecord extends NTFSRecord {
/**
* The volume this record is a part of.
*/
private final NTFSVolume volume;
/**
* The cluster size for the volume containing this record.
*/
private final int clusterSize;
/**
* Index of the file record within the MFT.
*/
private long referenceNumber;
/**
* Cached attribute list attribute.
*/
protected AttributeListAttribute attributeListAttribute;
/**
* The stored attributes.
*/
protected List<NTFSAttribute> storedAttributeList;
/**
* A cached copy of the full list of attributes.
*/
protected List<NTFSAttribute> attributeList;
/**
* Cached standard information attribute.
*/
private StandardInformationAttribute standardInformationAttribute;
/**
* Cached file name attribute.
*/
private FileNameAttribute fileNameAttribute;
/**
* Initialize this instance.
*
* @param volume reference to the NTFS volume.
* @param referenceNumber the reference number of the file within the MFT.
* @param buffer data buffer.
* @param offset offset into the buffer.
*/
public FileRecord(NTFSVolume volume, long referenceNumber, byte[] buffer, int offset) throws IOException {
this(volume, volume.getBootRecord().getBytesPerSector(), volume.getClusterSize(), true, referenceNumber,
buffer, offset);
}
/**
* Initialize this instance.
*
* @param volume reference to the NTFS volume.
* @param bytesPerSector the number of bytes-per-sector in this volume.
* @param clusterSize the cluster size for the volume containing this record.
* @param strictFixUp indicates whether an exception should be throw if fix-up values don't match.
* @param referenceNumber the reference number of the file within the MFT.
* @param buffer data buffer.
* @param offset offset into the buffer.
*/
public FileRecord(NTFSVolume volume, int bytesPerSector, int clusterSize, boolean strictFixUp, long referenceNumber,
byte[] buffer, int offset) throws IOException {
super(bytesPerSector, strictFixUp, buffer, offset);
this.volume = volume;
this.clusterSize = clusterSize;
this.referenceNumber = referenceNumber;
storedAttributeList = readStoredAttributes();
// Linux NTFS docs say there can only be one of these, so I'll believe them.
attributeListAttribute = (AttributeListAttribute) findStoredAttributeByType(NTFSAttribute.Types.ATTRIBUTE_LIST);
}
/**
* Checks if the record appears to be valid.
*
* @throws IOException if an error occurs.
*/
public void checkIfValid() throws IOException {
// check for the magic number to see if we have a filerecord
if (getMagic() != Magic.FILE) {
log.debug("Invalid magic number found for FILE record: " + getMagic() + " -- dumping buffer");
for (int off = 0; off < getBuffer().length; off += 32) {
StringBuilder builder = new StringBuilder();
for (int i = off; i < off + 32 && i < getBuffer().length; i++) {
String hex = Integer.toHexString(getBuffer()[i]);
while (hex.length() < 2) {
hex = '0' + hex;
}
builder.append(' ').append(hex);
}
log.debug(builder.toString());
}
throw new IOException("Invalid magic found: " + getMagic());
}
// This additional sanity check is possible if the record also contains the MFT number.
// Helps catch bugs where a record is being read from the wrong offset.
final long storedReferenceNumber = getStoredReferenceNumber();
if (storedReferenceNumber >= 0 && referenceNumber != storedReferenceNumber) {
throw new IOException("Stored reference number " + getStoredReferenceNumber()
+ " does not match reference number " + referenceNumber);
}
}
/**
* The volume this record is a part of
*/
public NTFSVolume getVolume() {
return volume;
}
/**
* The cluster size for the volume containing this record.
*/
public int getClusterSize() {
return clusterSize;
}
/**
* Gets the allocated size of the FILE record in bytes.
*
* @return Returns the allocated size.
*/
public long getAllocatedSize() {
return getUInt32(0x1C);
}
/**
* Gets the reference number of the base record. For continuation MFT entries this will reference the main record.
* For main records this should match {@link #referenceNumber}.
*
* @return Returns the base reference number.
*/
public long getBaseReferenceNumber() {
return getUInt48(0x20);
}
/**
* Gets the real size of the FILE record in bytes.
*
* @return Returns the realSize.
*/
public long getRealSize() {
return getUInt32(0x18);
}
/**
* Is this record in use?
*
* @return {@code true} if the record is in use.
*/
public boolean isInUse() {
return (getFlags() & 0x01) != 0;
}
/**
* Is this a directory?
*
* @return {@code true} if the record is a directory.
*/
public boolean isDirectory() {
return (getFlags() & 0x02) != 0;
}
/**
* Gets the hard link count.
*
* @return Returns the hardLinkCount.
*/
public int getHardLinkCount() {
return getUInt16(0x12);
}
/**
* Gets the byte offset to the first attribute in this mft record from the start of the mft record.
*
* @return the first attribute offset.
*/
public int getFirstAttributeOffset() {
return getUInt16(0x14);
}
/**
* Gets the flags.
*
* @return Returns the flags.
*/
public int getFlags() {
return getUInt16(0x16);
}
/**
* Gets the Next Attribute Id.
*
* @return Returns the nextAttributeID.
*/
public int getNextAttributeID() {
return getUInt16(0x28);
}
/**
* Gets the $LogFile sequence number.
*
* @return the $LogFile sequence number.
*/
public long getLsn() {
return getInt64(0x08);
}
/**
* Gets the number of times this mft record has been reused.
*
* @return Returns the sequenceNumber.
*/
public int getSequenceNumber() {
return getUInt16(0x10);
}
/**
* Gets the reference number of this record within the MFT. This value is not actually stored in the record, but
* passed in from the outside.
*
* @return the reference number.
*/
public long getReferenceNumber() {
return referenceNumber;
}
/**
* @return Returns the updateSequenceOffset.
*/
public int getUpdateSequenceOffset() {
return getUInt16(0x4);
}
/**
* Gets the stored reference number. This can be compared against the reference number to confirm that the correct
* file record was returned, however it is not available on all versions of NTFS, and even on recent versions some
* MFT records lack it.
*
* @return the stored file reference number, or {@code -1} if it is not stored.
*/
public long getStoredReferenceNumber() {
// Expected to be 0x2A pre-XP.
if (getUpdateSequenceOffset() >= 0x30) {
return getUInt32(0x2C);
} else {
return -1;
}
}
/**
* Gets the name of this file.
*
* @return the filename.
*/
public String getFileName() {
final FileNameAttribute fnAttr = getFileNameAttribute();
if (fnAttr != null) {
return fnAttr.getFileName();
} else {
return null;
}
}
/**
* Gets the standard information attribute for this file record.
*
* @return the standard information attribute.
*/
public StandardInformationAttribute getStandardInformationAttribute() {
if (standardInformationAttribute == null) {
standardInformationAttribute =
(StandardInformationAttribute) findAttributeByType(NTFSAttribute.Types.STANDARD_INFORMATION);
}
return standardInformationAttribute;
}
/**
* Gets the file name attribute for this file record.
*
* @return the file name attribute.
*/
public FileNameAttribute getFileNameAttribute() {
if (fileNameAttribute == null) {
Iterator<NTFSAttribute> iterator = findAttributesByType(NTFSAttribute.Types.FILE_NAME);
// Search for a Win32 file name if possible
while (iterator.hasNext()) {
NTFSAttribute attribute = iterator.next();
if (fileNameAttribute == null ||
fileNameAttribute.getNameSpace() != FileNameAttribute.NameSpace.WIN32) {
fileNameAttribute = (FileNameAttribute) attribute;
}
}
}
return fileNameAttribute;
}
/**
* Gets the attributes stored in this file record.
*
* @return an iterator over attributes stored in this file record.
*/
public List<NTFSAttribute> getAllStoredAttributes() {
return storedAttributeList;
}
/**
* Finds a single stored attribute by ID.
*
* @param id the ID.
* @return the attribute found, or {@code null} if not found.
*/
private NTFSAttribute findStoredAttributeByID(int id) {
for (NTFSAttribute attr : storedAttributeList) {
if (attr != null && attr.getAttributeID() == id) {
return attr;
}
}
return null;
}
/**
* Finds a single stored attribute by type.
*
* @param typeID the type ID
* @return the attribute found, or {@code null} if not found.
* @see NTFSAttribute.Types
*/
private NTFSAttribute findStoredAttributeByType(int typeID) {
for (NTFSAttribute attr : storedAttributeList) {
if (attr != null && attr.getAttributeType() == typeID) {
return attr;
}
}
return null;
}
/**
* Gets a collection of all attributes in this file record, including any attributes
* which are stored in other file records referenced from an $ATTRIBUTE_LIST attribute.
*
* @return a collection of all attributes.
*/
public synchronized List<NTFSAttribute> getAllAttributes() {
if (attributeList == null) {
attributeList = new ArrayList<NTFSAttribute>();
try {
if (attributeListAttribute == null) {
log.debug("All attributes stored");
attributeList = new ArrayList<NTFSAttribute>(getAllStoredAttributes());
} else {
log.debug("Attributes in attribute list");
readAttributeListAttributes();
}
} catch (Exception e) {
log.error("Error getting attributes for entry: " + this, e);
}
}
return attributeList;
}
/**
* Gets the first attribute in this filerecord with a given type.
*
* @param attrTypeID the type ID of the attribute we're looking for.
* @return the attribute.
*/
public NTFSAttribute findAttributeByType(int attrTypeID) {
log.debug("findAttributeByType(0x" + NumberUtils.hex(attrTypeID, 4) + ")");
for (NTFSAttribute attr : getAllAttributes()) {
if (attr.getAttributeType() == attrTypeID) {
log.debug("findAttributeByType(0x" + NumberUtils.hex(attrTypeID, 4) + ") found");
return attr;
}
}
log.debug("findAttributeByType(0x" + NumberUtils.hex(attrTypeID, 4) + ") not found");
return null;
}
/**
* Gets attributes in this file record with a given type.
*
* @param attrTypeID the type ID of the attribute we're looking for.
* @return an iterator for the matching the attributes.
*/
public Iterator<NTFSAttribute> findAttributesByType(final int attrTypeID) {
log.debug("findAttributesByType(0x" + NumberUtils.hex(attrTypeID, 4) + ")");
return new FilteredAttributeIterator(getAllAttributes().iterator()) {
@Override
protected boolean matches(NTFSAttribute attr) {
return attr.getAttributeType() == attrTypeID;
}
};
}
/**
* Gets attributes in this file record with a given type and name.
*
* @param attrTypeID the type ID of the attribute we're looking for.
* @param name the name to look for.
* @return an iterator for the matching the attributes.
*/
public Iterator<NTFSAttribute> findAttributesByTypeAndName(final int attrTypeID, final String name) {
log.debug("findAttributesByTypeAndName(0x" + NumberUtils.hex(attrTypeID, 4) + "," + name + ")");
return new FilteredAttributeIterator(getAllAttributes().iterator()) {
@Override
protected boolean matches(NTFSAttribute attr) {
if (attr.getAttributeType() == attrTypeID) {
String attrName = attr.getAttributeName();
if (name == null ? attrName == null : name.equals(attrName)) {
log.debug("findAttributesByTypeAndName(0x" + NumberUtils.hex(attrTypeID, 4) + "," + name
+ ") found");
return true;
}
}
return false;
}
};
}
/**
* Gets the total size used for the given attribute. Often the directory index entry and the FileRecord will have
* stale values for the file length, so checking the length of the {@link NTFSAttribute.Types#DATA} attribute is the
* most reliable way to get the actual file length.
*
* @param attrTypeID the type of attribute to get the size for, e.g. {@link NTFSAttribute.Types#DATA}.
* @param name the name of the attribute or {@code null} for no name.
* @return the total size of the attribute.
*/
public long getAttributeTotalSize(int attrTypeID, String name) {
Iterator<NTFSAttribute> attributes = findAttributesByTypeAndName(attrTypeID, name);
if (!attributes.hasNext()) {
throw new IllegalStateException("Failed to find an attribute with type: " + attrTypeID + " and name: '" +
name + "'");
} else {
NTFSAttribute attribute = attributes.next();
if (attribute.isResident()) {
// If the attribute is resident it should be the only attribute of that type present, so just return
// the length
return ((NTFSResidentAttribute) attribute).getAttributeLength();
} else {
// The total length seems to be stored in the first attribute of a certain type. E.g. if there are two
// DATA attributes each with data runs, the first one has the total length, and the intermediate ones
// seem to contain the length of that particular attribute. So here just return the length of the first
// attribute
return ((NTFSNonResidentAttribute) attribute).getAttributeActualSize();
}
}
}
/**
* Reads data from the file.
*
* @param fileOffset the offset into the file.
* @param dest the destination byte array into which to copy the file data.
* @param off the offset into the destination byte array.
* @param len the number of bytes of data to read.
* @throws IOException if an error occurs reading from the filesystem.
*/
public void readData(long fileOffset, byte[] dest, int off, int len) throws IOException {
// Explicitly look for the attribute with no name, to avoid getting alternate streams.
readData(NTFSAttribute.Types.DATA, null, fileOffset, dest, off, len, true);
}
/**
* Reads data from the file.
*
* @param attributeType the attribute type to read from.
* @param streamName the stream name to read from, or {@code null} to read from the default stream.
* @param fileOffset the offset into the file.
* @param dest the destination byte array into which to copy the file data.
* @param off the offset into the destination byte array.
* @param len the number of bytes of data to read.
* @param limitToInitialised {@code true} if the data read in should be limited to the initalised part of the
* attribute.
* @throws IOException if an error occurs reading from the filesystem.
*/
public void readData(int attributeType, String streamName, long fileOffset, byte[] dest, int off, int len,
boolean limitToInitialised)
throws IOException {
if (log.isDebugEnabled()) {
log.debug("readData: offset " + fileOffset + " attr:" + attributeType + " stream: " + streamName +
" length " + len + ", file record = " + this);
}
if (len == 0) {
return;
}
final Iterator<NTFSAttribute> dataAttrs = findAttributesByTypeAndName(attributeType, streamName);
if (!dataAttrs.hasNext()) {
throw new IOException(attributeType + " attribute not found, file record = " + this);
}
NTFSAttribute attr = dataAttrs.next();
if (attr.isResident()) {
if (dataAttrs.hasNext()) {
throw new IOException("Resident attribute should be by itself, file record = " + this);
}
final NTFSResidentAttribute resData = (NTFSResidentAttribute) attr;
final int attrLength = resData.getAttributeLength();
if (attrLength < len) {
throw new IOException("File data(" + attrLength + "b) is not large enough to read:" + len + "b");
}
resData.getData(resData.getAttributeOffset() + (int) fileOffset, dest, off, len);
if (log.isDebugEnabled()) {
log.debug("readData: read from resident data");
}
return;
}
// At this point we know that at least the first attribute is non-resident...
// Grab the initialised size (if that is itself initialised)
long initialisedSize = ((NTFSNonResidentAttribute) attr).getAttributeInitializedSize();
if (initialisedSize == 0)
{
limitToInitialised = false;
}
// calculate start and end cluster
final int clusterSize = getClusterSize();
final long startCluster = fileOffset / clusterSize;
final long endCluster = (fileOffset + len - 1) / clusterSize;
final int nrClusters = (int) (endCluster - startCluster + 1);
final byte[] tmp = new byte[nrClusters * clusterSize];
long clusterOffset = 0;
long clusterWithinNresData = startCluster;
int readClusters = 0;
while (true) {
if (attr.isResident()) {
throw new IOException("Resident attribute should be by itself, file record = " + this);
}
final NTFSNonResidentAttribute nresData = (NTFSNonResidentAttribute) attr;
readClusters += nresData.readVCN(clusterWithinNresData, tmp, 0, nrClusters);
if (readClusters > 0) {
// If if the data is past the 'initialised' part of the attribute. If it is uninitialised then it must
// be read as zeros. Annoyingly the initialised portion isn't even cluster aligned...
long endOffset = (clusterOffset + clusterWithinNresData + nrClusters) * clusterSize;
if (endOffset > initialisedSize && limitToInitialised) {
int delta = (int)(endOffset - initialisedSize);
int startIndex = Math.max((int)(tmp.length - delta), 0);
if (startIndex < tmp.length) {
Arrays.fill(tmp, startIndex, tmp.length, (byte) 0);
}
}
}
if (readClusters == nrClusters) {
// Already done.
break;
}
// When there are multiple attributes, the data in each one claims to start at VCN 0.
// Clearly this is not the case, so we need to offset when we read.
clusterWithinNresData -= nresData.getNumberOfVCNs();
clusterOffset += nresData.getNumberOfVCNs();
if (dataAttrs.hasNext()) {
attr = dataAttrs.next();
} else {
break;
}
}
if (log.isDebugEnabled()) {
log.debug("readData: read " + readClusters + " from non-resident attributes");
}
if (readClusters != nrClusters) {
throw new IOException("Requested " + nrClusters + " clusters but only read " + readClusters +
", file offset = " + fileOffset + ", file record = " + this);
}
System.arraycopy(tmp, (int) (fileOffset % clusterSize), dest, off, len);
}
@Override
public String toString() {
if (isInUse()) {
return String.format("FileRecord [%d fileName='%s']", referenceNumber, getFileName());
} else {
return String.format("FileRecord [%d unused]", referenceNumber);
}
}
/**
* Reads in all attributes referenced by the attribute-list attribute.
*/
private synchronized void readAttributeListAttributes() {
Iterator<AttributeListEntry> entryIterator;
try {
entryIterator = attributeListAttribute.getAllEntries();
} catch (Exception e) {
throw new IllegalStateException("Error getting attributes from attribute list, file record " +
FileRecord.this, e);
}
Map<Integer, NTFSNonResidentAttribute> compressedByType =
new LinkedHashMap<Integer, NTFSNonResidentAttribute>();
while (entryIterator.hasNext()) {
AttributeListEntry entry = entryIterator.next();
try {
// If it's resident (i.e. in the current file record) then we don't need to
// look it up, and doing so would risk infinite recursion.
NTFSAttribute attribute;
if (entry.getFileReferenceNumber() == referenceNumber) {
attribute = findStoredAttributeByID(entry.getAttributeID());
} else {
log.debug("Looking up MFT entry for: " + entry.getFileReferenceNumber());
// When reading the MFT itself don't attempt to check the index is in range (we won't know the total
// MFT length yet)
MasterFileTable mft = getVolume().getMFT();
FileRecord holdingRecord = getReferenceNumber() == MasterFileTable.SystemFiles.MFT
? mft.getRecordUnchecked(entry.getFileReferenceNumber())
: mft.getRecord(entry.getFileReferenceNumber());
attribute = holdingRecord.findStoredAttributeByID(entry.getAttributeID());
if (attribute == null) {
log.error(String.format("Failed to find an attribute matching entry '%s' in the holding record", entry));
continue;
} else if (!attribute.isResident() && attribute.isCompressedAttribute() &&
compressedByType.containsKey(attribute.getAttributeType())) {
// Get the fallback compression unit
NTFSNonResidentAttribute firstAttribute = compressedByType.get(attribute.getAttributeType());
int fallbackCompressionUnit = 1 << firstAttribute.getStoredCompressionUnitSize();
// Re-read the attribute with the fallback compression unit
attribute = NTFSAttribute.getAttribute(attribute.getFileRecord(), attribute.getOffset(),
fallbackCompressionUnit);
}
}
// Record the first compressed attribute of each type
if (!attribute.isResident() && attribute.isCompressedAttribute() &&
!compressedByType.containsKey(attribute.getAttributeType())) {
compressedByType.put(attribute.getAttributeType(), (NTFSNonResidentAttribute) attribute);
}
if (log.isDebugEnabled()) {
log.debug("Attribute: " + attribute);
}
attributeList.add(attribute);
} catch (Exception e) {
throw new IllegalStateException("Error getting MFT or FileRecord for attribute in list, ref = 0x" +
Long.toHexString(entry.getFileReferenceNumber()), e);
}
}
}
/**
* Reads in the stored attributes.
*
* @return the stored attributes.
*/
private List<NTFSAttribute> readStoredAttributes() {
List<NTFSAttribute> attributes = new ArrayList<NTFSAttribute>();
int offset = getFirstAttributeOffset();
while (true) {
int type = getUInt32AsInt(offset);
if (type == 0xFFFFFFFF) {
// Normal end of list condition.
break;
} else {
NTFSAttribute attribute = NTFSAttribute.getAttribute(FileRecord.this, offset);
if (log.isDebugEnabled()) {
log.debug("Attribute: " + attribute.toDebugString());
}
int offsetToNextOffset = getUInt32AsInt(offset + 0x04);
if (offsetToNextOffset <= 0) {
log.debug("Non-positive offset, preventing infinite loop. Data on disk may be corrupt. "
+ "referenceNumber = " + referenceNumber);
break;
} else {
offset += offsetToNextOffset;
attributes.add(attribute);
}
}
}
return attributes;
}
/**
* An iterator for filtering another iterator.
*/
private abstract class FilteredAttributeIterator implements Iterator<NTFSAttribute> {
private Iterator<NTFSAttribute> attributes;
private NTFSAttribute cached;
private boolean hasCached;
private FilteredAttributeIterator(Iterator<NTFSAttribute> attributes) {
this.attributes = attributes;
}
@Override
public boolean hasNext() {
if (hasCached) {
return true;
} else {
nextMatch();
return hasCached;
}
}
@Override
public NTFSAttribute next() {
if (hasNext()) {
hasCached = false;
return cached;
}
throw new NoSuchElementException();
}
/**
* Gets the next matching attribute.
*
* @return the next match.
*/
private NTFSAttribute nextMatch() {
while (attributes.hasNext()) {
NTFSAttribute attribute = attributes.next();
if (matches(attribute)) {
hasCached = true;
cached = attribute;
return attribute;
}
}
hasCached = false;
return null;
}
/**
* Implemented by subclasses to perform matching logic.
*
* @param attr the attribute.
* @return {@code true} if it matches, {@code false} otherwise.
*/
protected abstract boolean matches(NTFSAttribute attr);
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
}