/* Copyright (C) 2014 Tal Aloni <tal.aloni.il@gmail.com>. All rights reserved. * * You can redistribute this program and/or modify it under the terms of * the GNU Lesser Public License as published by the Free Software Foundation, * either version 3 of the License, or (at your option) any later version. */ using System; using System.Collections.Generic; using System.IO; using System.Text; using Utilities; namespace DiskAccessLibrary.FileSystems.NTFS { public class FileRecordSegment { public const string ValidSignature = "FILE"; public const int EndMarkerLength = 4; public const int NTFS30UpdateSequenceArrayOffset = 0x2A; // NTFS 3.0 and earlier (up to Windows 2000) public const int NTFS31UpdateSequenceArrayOffset = 0x30; // NTFS 3.1 and later (XP and later) [Flags] public enum FileRecordFlags : ushort { None = 0x0000, InUse = 0x0001, IsDirectory = 0x0002, IsMetaFile = 0x0004, HasViewIndex = 0x0008, } /* Start of header */ /* Start of MULTI_SECTOR_HEADER */ public string Signature = ValidSignature; // ushort UpdateSequenceArrayOffset; // ushort UpdateSequenceArraySize; // number of (2 byte) words /* End of MULTI_SECTOR_HEADER */ public ulong LogFileSequenceNumber; public ushort SequenceNumber; // This value is incremented each time that a file record segment is freed public ushort HardLinkCount; // ushort FirstAttributeOffset; private FileRecordFlags m_flags; // uint SegmentRealSize; // uint SegmentAllocatedSize; private ulong BaseFileRecordSegmentNumber; // If this is the base file record, the value is 0 public ushort NextAttributeId; // Starting from 0 // 2 zeros - padding public uint MftSegmentNumberXP; // Self-reference (on XP+) public ushort UpdateSequenceNumber; // a.k.a. USN /* End of header */ private long m_mftSegmentNumber; // We use our own segment number to support NTFS 3.0 (note that MftSegmentNumberXP is UInt32, which is another reason to avoid it) private List<AttributeRecord> m_immediateAttributes = new List<AttributeRecord>(); // Attribute records that are stored in the base file record public FileRecordSegment(byte[] buffer, int bytesPerSector, long segmentNumber) : this(buffer, 0, bytesPerSector, segmentNumber) { } public FileRecordSegment(byte[] buffer, int offset, int bytesPerSector, long segmentNumber) { Signature = ByteReader.ReadAnsiString(buffer, offset + 0x00, 4); ushort updateSequenceArrayOffset = LittleEndianConverter.ToUInt16(buffer, offset + 0x04); ushort updateSequenceArraySize = LittleEndianConverter.ToUInt16(buffer, offset + 0x06); LogFileSequenceNumber = LittleEndianConverter.ToUInt64(buffer, offset + 0x08); SequenceNumber = LittleEndianConverter.ToUInt16(buffer, offset + 0x10); HardLinkCount = LittleEndianConverter.ToUInt16(buffer, offset + 0x12); ushort firstAttributeOffset = LittleEndianConverter.ToUInt16(buffer, offset + 0x14); m_flags = (FileRecordFlags)LittleEndianConverter.ToUInt16(buffer, offset + 0x16); uint segmentRealSize = LittleEndianConverter.ToUInt32(buffer, offset + 0x18); uint segmentAllocatedSize = LittleEndianConverter.ToUInt32(buffer, offset + 0x1C); BaseFileRecordSegmentNumber = LittleEndianConverter.ToUInt64(buffer, offset + 0x20); NextAttributeId = LittleEndianConverter.ToUInt16(buffer, offset + 0x28); // 2 zeros - padding MftSegmentNumberXP = LittleEndianConverter.ToUInt32(buffer, offset + 0x2C); // There is an UpdateSequenceNumber for the FileRecordSegment, // and an entry in the UpdateSequenceArray for each sector of the record // The last two bytes of each sector contains this entry for integrity-check purposes int position = offset + updateSequenceArrayOffset; UpdateSequenceNumber = LittleEndianConverter.ToUInt16(buffer, position); position += 2; // This stores the data that was supposed to be placed at the end of each sector, and was replaced with an UpdateSequenceNumber List<byte[]> updateSequenceReplacementData = new List<byte[]>(); for (int index = 0; index < updateSequenceArraySize - 1; index++) { byte[] endOfSectorBytes = new byte[2]; endOfSectorBytes[0] = buffer[position + 0]; endOfSectorBytes[1] = buffer[position + 1]; updateSequenceReplacementData.Add(endOfSectorBytes); position += 2; } MultiSectorHelper.DecodeSegmentBuffer(buffer, offset, UpdateSequenceNumber, updateSequenceReplacementData); // read attributes position = offset + firstAttributeOffset; while (!IsEndMarker(buffer, position)) { AttributeRecord attribute = AttributeRecord.FromBytes(buffer, position); m_immediateAttributes.Add(attribute); position += (int)attribute.StoredRecordLength; if (position > buffer.Length) { throw new InvalidDataException("Improper attribute length"); } } m_mftSegmentNumber = segmentNumber; } /// <param name="segmentLength">This refers to the maximum length of FileRecord as defined in the Volume's BootRecord</param> public byte[] GetBytes(int segmentLength, int bytesPerCluster, ushort minorNTFSVersion) { int strideCount = segmentLength / MultiSectorHelper.BytesPerStride; ushort updateSequenceArraySize = (ushort)(1 + strideCount); ushort updateSequenceArrayOffset; if (minorNTFSVersion == 0) { updateSequenceArrayOffset = NTFS30UpdateSequenceArrayOffset; } else { updateSequenceArrayOffset = NTFS31UpdateSequenceArrayOffset; } ushort firstAttributeOffset = GetFirstAttributeOffset(segmentLength, minorNTFSVersion); byte[] buffer = new byte[segmentLength]; ByteWriter.WriteAnsiString(buffer, 0, Signature, 4); LittleEndianWriter.WriteUInt16(buffer, 0x04, updateSequenceArrayOffset); LittleEndianWriter.WriteUInt16(buffer, 0x06, updateSequenceArraySize); LittleEndianWriter.WriteUInt64(buffer, 0x08, LogFileSequenceNumber); LittleEndianWriter.WriteUInt16(buffer, 0x10, SequenceNumber); LittleEndianWriter.WriteUInt16(buffer, 0x12, HardLinkCount); LittleEndianWriter.WriteUInt16(buffer, 0x14, firstAttributeOffset); LittleEndianWriter.WriteUInt16(buffer, 0x16, (ushort)m_flags); LittleEndianWriter.WriteInt32(buffer, 0x1C, segmentLength); LittleEndianWriter.WriteUInt64(buffer, 0x20, BaseFileRecordSegmentNumber); LittleEndianWriter.WriteUInt16(buffer, 0x28, NextAttributeId); if (minorNTFSVersion == 1) { LittleEndianWriter.WriteUInt32(buffer, 0x2C, MftSegmentNumberXP); } // write attributes int position = firstAttributeOffset; foreach (AttributeRecord attribute in m_immediateAttributes) { byte[] attributeBytes = attribute.GetBytes(bytesPerCluster); ByteWriter.WriteBytes(buffer, position, attributeBytes); position += attributeBytes.Length; } byte[] marker = GetEndMarker(); ByteWriter.WriteBytes(buffer, position, marker); position += marker.Length; position += 4; // record (length) is aligned to 8-byte boundary uint segmentRealSize = (uint)position; LittleEndianWriter.WriteUInt32(buffer, 0x18, segmentRealSize); // write UpdateSequenceNumber and UpdateSequenceReplacementData List<byte[]> updateSequenceReplacementData = MultiSectorHelper.EncodeSegmentBuffer(buffer, 0, segmentLength, UpdateSequenceNumber); position = updateSequenceArrayOffset; LittleEndianWriter.WriteUInt16(buffer, position, UpdateSequenceNumber); position += 2; foreach (byte[] endOfSectorBytes in updateSequenceReplacementData) { ByteWriter.WriteBytes(buffer, position, endOfSectorBytes); position += 2; } return buffer; } public AttributeRecord GetImmediateAttributeRecord(AttributeType type) { foreach (AttributeRecord attribute in m_immediateAttributes) { if (attribute.AttributeType == type) { return attribute; } } return null; } /// <summary> /// Indicates that the file / directory wasn't deleted /// </summary> public bool IsInUse { get { return (m_flags & FileRecordFlags.InUse) != 0; } set { m_flags &= ~FileRecordFlags.InUse; } } public bool IsDirectory { get { return (m_flags & FileRecordFlags.IsDirectory) != 0; //return (GetAttributeRecord(AttributeType.IndexRoot) != null); } } public List<AttributeRecord> ImmediateAttributes { get { return m_immediateAttributes; } } public bool IsBaseFileRecord { get { // If this is the base file record, the value is 0 // http://msdn.microsoft.com/en-us/library/bb470124%28v=vs.85%29.aspx return (BaseFileRecordSegmentNumber == 0); } } public bool HasAttributeList { get { AttributeRecord attributeList = GetImmediateAttributeRecord(AttributeType.AttributeList); return (attributeList != null); } } /* public uint RecordRealSize { get { return m_recordRealSize; } }*/ public override bool Equals(object obj) { if (obj is FileRecordSegment) { return ((FileRecordSegment)obj).MftSegmentNumber == MftSegmentNumber; } return base.Equals(obj); } public override int GetHashCode() { return MftSegmentNumber.GetHashCode(); } public long MftSegmentNumber { get { return m_mftSegmentNumber; } } public static bool IsEndMarker(byte[] buffer, int offset) { uint type = LittleEndianConverter.ToUInt32(buffer, offset + 0x00); return (type == 0xFFFFFFFF); } /// <summary> /// Get file record end marker /// </summary> public static byte[] GetEndMarker() { byte[] buffer = new byte[4]; Array.Copy(LittleEndianConverter.GetBytes(0xFFFFFFFF), buffer, 4); return buffer; } public static ushort GetFirstAttributeOffset(int segmentLength, ushort minorNTFSVersion) { int strideCount = segmentLength / MultiSectorHelper.BytesPerStride; ushort updateSequenceArraySize = (ushort)(1 + strideCount); ushort updateSequenceArrayOffset; if (minorNTFSVersion == 0) { updateSequenceArrayOffset = NTFS30UpdateSequenceArrayOffset; } else { updateSequenceArrayOffset = NTFS31UpdateSequenceArrayOffset; } // aligned to 8 byte boundary // Note: I had an issue with 4 byte boundary under Windows 7 using disk with 2048 bytes per sector. // Windows used an 8 byte boundary. ushort firstAttributeOffset = (ushort)(Math.Ceiling((double)(updateSequenceArrayOffset + updateSequenceArraySize * 2) / 8) * 8); return firstAttributeOffset; } public static bool ContainsFileRecordSegment(byte[] recordBytes) { return ContainsFileRecordSegment(recordBytes, 0); } public static bool ContainsFileRecordSegment(byte[] recordBytes, int offset) { string fileSignature = ByteReader.ReadAnsiString(recordBytes, offset, 4); return (fileSignature == ValidSignature); } public static bool ContainsMftSegmentNumber(List<FileRecordSegment> list, long mftSegmentNumber) { foreach (FileRecordSegment segment in list) { if (segment.MftSegmentNumber == mftSegmentNumber) { return true; } } return false; } } }