mirror of
https://github.com/ReneLergner/WPinternals.git
synced 2026-06-14 03:16:40 +10:00
1280 lines
45 KiB
C#
1280 lines
45 KiB
C#
//
|
|
// Copyright (c) 2008-2011, Kenneth Bell
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a
|
|
// copy of this software and associated documentation files (the "Software"),
|
|
// to deal in the Software without restriction, including without limitation
|
|
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
// and/or sell copies of the Software, and to permit persons to whom the
|
|
// Software is furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
// DEALINGS IN THE SOFTWARE.
|
|
//
|
|
|
|
namespace DiscUtils.Ntfs
|
|
{
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
|
|
internal class File
|
|
{
|
|
protected INtfsContext _context;
|
|
|
|
private MasterFileTable _mft;
|
|
private List<FileRecord> _records;
|
|
private ObjectCache<string, Index> _indexCache;
|
|
private List<NtfsAttribute> _attributes;
|
|
|
|
private bool _dirty;
|
|
|
|
public File(INtfsContext context, FileRecord baseRecord)
|
|
{
|
|
_context = context;
|
|
_mft = _context.Mft;
|
|
_records = new List<FileRecord>();
|
|
_records.Add(baseRecord);
|
|
_indexCache = new ObjectCache<string, Index>();
|
|
_attributes = new List<NtfsAttribute>();
|
|
|
|
LoadAttributes();
|
|
}
|
|
|
|
public uint IndexInMft
|
|
{
|
|
get { return _records[0].MasterFileTableIndex; }
|
|
}
|
|
|
|
public uint MaxMftRecordSize
|
|
{
|
|
get { return _records[0].AllocatedSize; }
|
|
}
|
|
|
|
public FileRecordReference MftReference
|
|
{
|
|
get { return _records[0].Reference; }
|
|
}
|
|
|
|
public List<string> Names
|
|
{
|
|
get
|
|
{
|
|
List<string> result = new List<string>();
|
|
|
|
if (IndexInMft == MasterFileTable.RootDirIndex)
|
|
{
|
|
result.Add(string.Empty);
|
|
}
|
|
else
|
|
{
|
|
foreach (StructuredNtfsAttribute<FileNameRecord> attr in GetAttributes(AttributeType.FileName))
|
|
{
|
|
string name = attr.Content.FileName;
|
|
|
|
Directory parentDir = _context.GetDirectoryByRef(attr.Content.ParentDirectory);
|
|
if (parentDir != null)
|
|
{
|
|
foreach (string dirName in parentDir.Names)
|
|
{
|
|
result.Add(Utilities.CombinePaths(dirName, name));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
public bool HasWin32OrDosName
|
|
{
|
|
get
|
|
{
|
|
foreach (StructuredNtfsAttribute<FileNameRecord> attr in GetAttributes(AttributeType.FileName))
|
|
{
|
|
FileNameRecord fnr = attr.Content;
|
|
if (fnr.FileNameNamespace != FileNameNamespace.Posix)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public bool MftRecordIsDirty
|
|
{
|
|
get
|
|
{
|
|
return _dirty;
|
|
}
|
|
}
|
|
|
|
public ushort HardLinkCount
|
|
{
|
|
get { return _records[0].HardLinkCount; }
|
|
set { _records[0].HardLinkCount = value; }
|
|
}
|
|
|
|
public IEnumerable<NtfsStream> AllStreams
|
|
{
|
|
get
|
|
{
|
|
foreach (var attr in _attributes)
|
|
{
|
|
yield return new NtfsStream(this, attr);
|
|
}
|
|
}
|
|
}
|
|
|
|
public DirectoryEntry DirectoryEntry
|
|
{
|
|
get
|
|
{
|
|
if (_context.GetDirectoryByRef == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
NtfsStream stream = GetStream(AttributeType.FileName, null);
|
|
if (stream == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
FileNameRecord record = stream.GetContent<FileNameRecord>();
|
|
|
|
// Root dir is stored without root directory flag set in FileNameRecord, simulate it.
|
|
if (_records[0].MasterFileTableIndex == MasterFileTable.RootDirIndex)
|
|
{
|
|
record.Flags |= FileAttributeFlags.Directory;
|
|
}
|
|
|
|
return new DirectoryEntry(_context.GetDirectoryByRef(record.ParentDirectory), MftReference, record);
|
|
}
|
|
}
|
|
|
|
public string BestName
|
|
{
|
|
get
|
|
{
|
|
NtfsAttribute[] attrs = GetAttributes(AttributeType.FileName);
|
|
|
|
string bestName = null;
|
|
|
|
if (attrs != null && attrs.Length != 0)
|
|
{
|
|
bestName = attrs[0].ToString();
|
|
|
|
for (int i = 1; i < attrs.Length; ++i)
|
|
{
|
|
string name = attrs[i].ToString();
|
|
|
|
if (Utilities.Is8Dot3(bestName))
|
|
{
|
|
bestName = name;
|
|
}
|
|
}
|
|
}
|
|
|
|
return bestName;
|
|
}
|
|
}
|
|
|
|
public bool IsDirectory
|
|
{
|
|
get { return (_records[0].Flags & FileRecordFlags.IsDirectory) != 0; }
|
|
}
|
|
|
|
public StandardInformation StandardInformation
|
|
{
|
|
get { return GetStream(AttributeType.StandardInformation, null).GetContent<StandardInformation>(); }
|
|
}
|
|
|
|
internal INtfsContext Context
|
|
{
|
|
get
|
|
{
|
|
return _context;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets an enumeration of all the attributes.
|
|
/// </summary>
|
|
internal IEnumerable<NtfsAttribute> AllAttributes
|
|
{
|
|
get { return _attributes; }
|
|
}
|
|
|
|
public static File CreateNew(INtfsContext context, FileAttributeFlags dirFlags)
|
|
{
|
|
return CreateNew(context, FileRecordFlags.None, dirFlags);
|
|
}
|
|
|
|
public static File CreateNew(INtfsContext context, FileRecordFlags flags, FileAttributeFlags dirFlags)
|
|
{
|
|
File newFile = context.AllocateFile(flags);
|
|
|
|
FileAttributeFlags fileFlags =
|
|
FileAttributeFlags.Archive
|
|
| FileRecord.ConvertFlags(flags)
|
|
| (dirFlags & FileAttributeFlags.Compressed);
|
|
|
|
AttributeFlags dataAttrFlags = AttributeFlags.None;
|
|
if ((dirFlags & FileAttributeFlags.Compressed) != 0)
|
|
{
|
|
dataAttrFlags |= AttributeFlags.Compressed;
|
|
}
|
|
|
|
StandardInformation.InitializeNewFile(newFile, fileFlags);
|
|
|
|
if (context.ObjectIds != null)
|
|
{
|
|
Guid newId = CreateNewGuid(context);
|
|
NtfsStream stream = newFile.CreateStream(AttributeType.ObjectId, null);
|
|
ObjectId objId = new ObjectId();
|
|
objId.Id = newId;
|
|
stream.SetContent(objId);
|
|
context.ObjectIds.Add(newId, newFile.MftReference, newId, Guid.Empty, Guid.Empty);
|
|
}
|
|
|
|
newFile.CreateAttribute(AttributeType.Data, dataAttrFlags);
|
|
|
|
newFile.UpdateRecordInMft();
|
|
|
|
return newFile;
|
|
}
|
|
|
|
public int MftRecordFreeSpace(AttributeType attrType, string attrName)
|
|
{
|
|
foreach (var record in _records)
|
|
{
|
|
if (record.GetAttribute(attrType, attrName) != null)
|
|
{
|
|
return _mft.RecordSize - record.Size;
|
|
}
|
|
}
|
|
|
|
throw new IOException("Attempt to determine free space for non-existent attribute");
|
|
}
|
|
|
|
public void Modified()
|
|
{
|
|
DateTime now = DateTime.UtcNow;
|
|
|
|
NtfsStream siStream = GetStream(AttributeType.StandardInformation, null);
|
|
StandardInformation si = siStream.GetContent<StandardInformation>();
|
|
si.LastAccessTime = now;
|
|
si.ModificationTime = now;
|
|
siStream.SetContent(si);
|
|
|
|
MarkMftRecordDirty();
|
|
}
|
|
|
|
public void Accessed()
|
|
{
|
|
DateTime now = DateTime.UtcNow;
|
|
|
|
NtfsStream siStream = GetStream(AttributeType.StandardInformation, null);
|
|
StandardInformation si = siStream.GetContent<StandardInformation>();
|
|
si.LastAccessTime = now;
|
|
siStream.SetContent(si);
|
|
|
|
MarkMftRecordDirty();
|
|
}
|
|
|
|
public void MarkMftRecordDirty()
|
|
{
|
|
_dirty = true;
|
|
}
|
|
|
|
public void UpdateRecordInMft()
|
|
{
|
|
if (_dirty)
|
|
{
|
|
if (NtfsTransaction.Current != null)
|
|
{
|
|
NtfsStream stream = GetStream(AttributeType.StandardInformation, null);
|
|
StandardInformation si = stream.GetContent<StandardInformation>();
|
|
si.MftChangedTime = NtfsTransaction.Current.Timestamp;
|
|
stream.SetContent(si);
|
|
}
|
|
|
|
bool fixesApplied = true;
|
|
while (fixesApplied)
|
|
{
|
|
fixesApplied = false;
|
|
|
|
for (int i = 0; i < _records.Count; ++i)
|
|
{
|
|
var record = _records[i];
|
|
|
|
bool fixedAttribute = true;
|
|
while (record.Size > _mft.RecordSize && fixedAttribute)
|
|
{
|
|
fixedAttribute = false;
|
|
|
|
if (!fixedAttribute && !record.IsMftRecord)
|
|
{
|
|
foreach (var attr in record.Attributes)
|
|
{
|
|
if (!attr.IsNonResident && !_context.AttributeDefinitions.MustBeResident(attr.AttributeType))
|
|
{
|
|
MakeAttributeNonResident(new AttributeReference(record.Reference, attr.AttributeId), (int)attr.DataLength);
|
|
fixedAttribute = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!fixedAttribute)
|
|
{
|
|
foreach (var attr in record.Attributes)
|
|
{
|
|
if (attr.AttributeType == AttributeType.IndexRoot
|
|
&& ShrinkIndexRoot(attr.Name))
|
|
{
|
|
fixedAttribute = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!fixedAttribute)
|
|
{
|
|
if (record.Attributes.Count == 1)
|
|
{
|
|
fixedAttribute = SplitAttribute(record);
|
|
}
|
|
else
|
|
{
|
|
if (_records.Count == 1)
|
|
{
|
|
CreateAttributeList();
|
|
}
|
|
|
|
fixedAttribute = ExpelAttribute(record);
|
|
}
|
|
}
|
|
|
|
fixesApplied |= fixedAttribute;
|
|
}
|
|
}
|
|
}
|
|
|
|
_dirty = false;
|
|
foreach (var record in _records)
|
|
{
|
|
_mft.WriteRecord(record);
|
|
}
|
|
}
|
|
}
|
|
|
|
public Index CreateIndex(string name, AttributeType attrType, AttributeCollationRule collRule)
|
|
{
|
|
Index.Create(attrType, collRule, this, name);
|
|
return GetIndex(name);
|
|
}
|
|
|
|
public Index GetIndex(string name)
|
|
{
|
|
Index idx = _indexCache[name];
|
|
|
|
if (idx == null)
|
|
{
|
|
idx = new Index(this, name, _context.BiosParameterBlock, _context.UpperCase);
|
|
_indexCache[name] = idx;
|
|
}
|
|
|
|
return idx;
|
|
}
|
|
|
|
public void Delete()
|
|
{
|
|
if (_records[0].HardLinkCount != 0)
|
|
{
|
|
throw new InvalidOperationException("Attempt to delete in-use file: " + ToString());
|
|
}
|
|
|
|
_context.ForgetFile(this);
|
|
|
|
NtfsStream objIdStream = GetStream(AttributeType.ObjectId, null);
|
|
if (objIdStream != null)
|
|
{
|
|
ObjectId objId = objIdStream.GetContent<ObjectId>();
|
|
Context.ObjectIds.Remove(objId.Id);
|
|
}
|
|
|
|
// Truncate attributes, allowing for truncation silently removing the AttributeList attribute
|
|
// in some cases (large file with all attributes first extent in the first MFT record). This
|
|
// releases all allocated clusters in most cases.
|
|
List<NtfsAttribute> truncateAttrs = new List<NtfsAttribute>(_attributes.Count);
|
|
foreach (var attr in _attributes)
|
|
{
|
|
if (attr.Type != AttributeType.AttributeList)
|
|
{
|
|
truncateAttrs.Add(attr);
|
|
}
|
|
}
|
|
|
|
foreach (NtfsAttribute attr in truncateAttrs)
|
|
{
|
|
attr.GetDataBuffer().SetCapacity(0);
|
|
}
|
|
|
|
// If the attribute list record remains, free any possible clusters it owns. We've now freed
|
|
// all clusters.
|
|
NtfsAttribute attrList = GetAttribute(AttributeType.AttributeList, null);
|
|
if (attrList != null)
|
|
{
|
|
attrList.GetDataBuffer().SetCapacity(0);
|
|
}
|
|
|
|
// Now go through the MFT records, freeing them up
|
|
foreach (var mftRecord in _records)
|
|
{
|
|
_context.Mft.RemoveRecord(mftRecord.Reference);
|
|
}
|
|
|
|
_attributes.Clear();
|
|
_records.Clear();
|
|
}
|
|
|
|
public bool StreamExists(AttributeType attrType, string name)
|
|
{
|
|
return GetStream(attrType, name) != null;
|
|
}
|
|
|
|
public NtfsStream GetStream(AttributeType attrType, string name)
|
|
{
|
|
foreach (NtfsStream stream in GetStreams(attrType, name))
|
|
{
|
|
return stream;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public IEnumerable<NtfsStream> GetStreams(AttributeType attrType, string name)
|
|
{
|
|
foreach (var attr in _attributes)
|
|
{
|
|
if (attr.Type == attrType && attr.Name == name)
|
|
{
|
|
yield return new NtfsStream(this, attr);
|
|
}
|
|
}
|
|
}
|
|
|
|
public NtfsStream CreateStream(AttributeType attrType, string name)
|
|
{
|
|
return new NtfsStream(this, CreateAttribute(attrType, name, AttributeFlags.None));
|
|
}
|
|
|
|
public NtfsStream CreateStream(AttributeType attrType, string name, long firstCluster, ulong numClusters, uint bytesPerCluster)
|
|
{
|
|
return new NtfsStream(this, CreateAttribute(attrType, name, AttributeFlags.None, firstCluster, numClusters, bytesPerCluster));
|
|
}
|
|
|
|
public SparseStream OpenStream(AttributeType attrType, string name, FileAccess access)
|
|
{
|
|
NtfsAttribute attr = GetAttribute(attrType, name);
|
|
if (attr != null)
|
|
{
|
|
return new FileStream(this, attr, access);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public void RemoveStream(NtfsStream stream)
|
|
{
|
|
RemoveAttribute(stream.Attribute);
|
|
}
|
|
|
|
public FileNameRecord GetFileNameRecord(string name, bool freshened)
|
|
{
|
|
NtfsAttribute[] attrs = GetAttributes(AttributeType.FileName);
|
|
StructuredNtfsAttribute<FileNameRecord> attr = null;
|
|
if (String.IsNullOrEmpty(name))
|
|
{
|
|
if (attrs.Length != 0)
|
|
{
|
|
attr = (StructuredNtfsAttribute<FileNameRecord>)attrs[0];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
foreach (StructuredNtfsAttribute<FileNameRecord> a in attrs)
|
|
{
|
|
if (_context.UpperCase.Compare(a.Content.FileName, name) == 0)
|
|
{
|
|
attr = a;
|
|
}
|
|
}
|
|
|
|
if (attr == null)
|
|
{
|
|
throw new FileNotFoundException("File name not found on file", name);
|
|
}
|
|
}
|
|
|
|
FileNameRecord fnr = attr == null ? new FileNameRecord() : new FileNameRecord(attr.Content);
|
|
|
|
if (freshened)
|
|
{
|
|
FreshenFileName(fnr, false);
|
|
}
|
|
|
|
return fnr;
|
|
}
|
|
|
|
public virtual void Dump(TextWriter writer, string indent)
|
|
{
|
|
writer.WriteLine(indent + "FILE (" + ToString() + ")");
|
|
writer.WriteLine(indent + " File Number: " + _records[0].MasterFileTableIndex);
|
|
|
|
_records[0].Dump(writer, indent + " ");
|
|
|
|
foreach (AttributeRecord attrRec in _records[0].Attributes)
|
|
{
|
|
NtfsAttribute.FromRecord(this, MftReference, attrRec).Dump(writer, indent + " ");
|
|
}
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
string bestName = BestName;
|
|
if (bestName == null)
|
|
{
|
|
return "?????";
|
|
}
|
|
else
|
|
{
|
|
return bestName;
|
|
}
|
|
}
|
|
|
|
internal void RemoveAttributeExtents(NtfsAttribute attr)
|
|
{
|
|
attr.GetDataBuffer().SetCapacity(0);
|
|
|
|
foreach (var extentRef in attr.Extents.Keys)
|
|
{
|
|
RemoveAttributeExtent(extentRef);
|
|
}
|
|
}
|
|
|
|
internal void RemoveAttributeExtent(AttributeReference extentRef)
|
|
{
|
|
FileRecord fileRec = GetFileRecord(extentRef.File);
|
|
if (fileRec != null)
|
|
{
|
|
fileRec.RemoveAttribute(extentRef.AttributeId);
|
|
|
|
// Remove empty non-primary MFT records
|
|
if (fileRec.Attributes.Count == 0 && fileRec.BaseFile.Value != 0)
|
|
{
|
|
RemoveFileRecord(extentRef.File);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets an attribute by reference.
|
|
/// </summary>
|
|
/// <param name="attrRef">Reference to the attribute.</param>
|
|
/// <returns>The attribute.</returns>
|
|
internal NtfsAttribute GetAttribute(AttributeReference attrRef)
|
|
{
|
|
foreach (var attr in _attributes)
|
|
{
|
|
if (attr.Reference.Equals(attrRef))
|
|
{
|
|
return attr;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the first (if more than one) instance of a named attribute.
|
|
/// </summary>
|
|
/// <param name="type">The attribute type.</param>
|
|
/// <param name="name">The attribute's name.</param>
|
|
/// <returns>The attribute of <c>null</c>.</returns>
|
|
internal NtfsAttribute GetAttribute(AttributeType type, string name)
|
|
{
|
|
foreach (var attr in _attributes)
|
|
{
|
|
if (attr.PrimaryRecord.AttributeType == type && attr.Name == name)
|
|
{
|
|
return attr;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all instances of an unnamed attribute.
|
|
/// </summary>
|
|
/// <param name="type">The attribute type.</param>
|
|
/// <returns>The attributes.</returns>
|
|
internal NtfsAttribute[] GetAttributes(AttributeType type)
|
|
{
|
|
List<NtfsAttribute> matches = new List<NtfsAttribute>();
|
|
|
|
foreach (var attr in _attributes)
|
|
{
|
|
if (attr.PrimaryRecord.AttributeType == type && string.IsNullOrEmpty(attr.Name))
|
|
{
|
|
matches.Add(attr);
|
|
}
|
|
}
|
|
|
|
return matches.ToArray();
|
|
}
|
|
|
|
internal void MakeAttributeNonResident(AttributeReference attrRef, int maxData)
|
|
{
|
|
NtfsAttribute attr = GetAttribute(attrRef);
|
|
if (attr.IsNonResident)
|
|
{
|
|
throw new InvalidOperationException("Attribute is already non-resident");
|
|
}
|
|
|
|
ushort id = _records[0].CreateNonResidentAttribute(attr.Type, attr.Name, attr.Flags);
|
|
AttributeRecord newAttrRecord = _records[0].GetAttribute(id);
|
|
|
|
IBuffer attrBuffer = attr.GetDataBuffer();
|
|
byte[] tempData = Utilities.ReadFully(attrBuffer, 0, (int)Math.Min(maxData, attrBuffer.Capacity));
|
|
|
|
RemoveAttributeExtents(attr);
|
|
attr.SetExtent(_records[0].Reference, newAttrRecord);
|
|
|
|
attr.GetDataBuffer().Write(0, tempData, 0, tempData.Length);
|
|
|
|
UpdateAttributeList();
|
|
}
|
|
|
|
internal void FreshenFileName(FileNameRecord fileName, bool updateMftRecord)
|
|
{
|
|
//
|
|
// Freshen the record from the definitive info in the other attributes
|
|
//
|
|
StandardInformation si = StandardInformation;
|
|
NtfsAttribute anonDataAttr = GetAttribute(AttributeType.Data, null);
|
|
|
|
fileName.CreationTime = si.CreationTime;
|
|
fileName.ModificationTime = si.ModificationTime;
|
|
fileName.MftChangedTime = si.MftChangedTime;
|
|
fileName.LastAccessTime = si.LastAccessTime;
|
|
fileName.Flags = si.FileAttributes;
|
|
|
|
if (_dirty && NtfsTransaction.Current != null)
|
|
{
|
|
fileName.MftChangedTime = NtfsTransaction.Current.Timestamp;
|
|
}
|
|
|
|
// Directories don't have directory flag set in StandardInformation, so set from MFT record
|
|
if ((_records[0].Flags & FileRecordFlags.IsDirectory) != 0)
|
|
{
|
|
fileName.Flags |= FileAttributeFlags.Directory;
|
|
}
|
|
|
|
if (anonDataAttr != null)
|
|
{
|
|
fileName.RealSize = (ulong)anonDataAttr.PrimaryRecord.DataLength;
|
|
fileName.AllocatedSize = (ulong)anonDataAttr.PrimaryRecord.AllocatedLength;
|
|
}
|
|
|
|
if (updateMftRecord)
|
|
{
|
|
foreach (NtfsStream stream in GetStreams(AttributeType.FileName, null))
|
|
{
|
|
FileNameRecord fnr = stream.GetContent<FileNameRecord>();
|
|
if (fnr.Equals(fileName))
|
|
{
|
|
fnr = new FileNameRecord(fileName);
|
|
fnr.Flags &= ~FileAttributeFlags.ReparsePoint;
|
|
stream.SetContent(fnr);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
internal long GetAttributeOffset(AttributeReference attrRef)
|
|
{
|
|
long recordOffset = _mft.GetRecordOffset(attrRef.File);
|
|
|
|
FileRecord frs = GetFileRecord(attrRef.File);
|
|
return recordOffset + frs.GetAttributeOffset(attrRef.AttributeId);
|
|
}
|
|
|
|
private static Guid CreateNewGuid(INtfsContext context)
|
|
{
|
|
Random rng = context.Options.RandomNumberGenerator;
|
|
if (rng != null)
|
|
{
|
|
byte[] buffer = new byte[16];
|
|
rng.NextBytes(buffer);
|
|
return new Guid(buffer);
|
|
}
|
|
else
|
|
{
|
|
return Guid.NewGuid();
|
|
}
|
|
}
|
|
|
|
private void LoadAttributes()
|
|
{
|
|
Dictionary<long, FileRecord> extraFileRecords = new Dictionary<long, FileRecord>();
|
|
|
|
AttributeRecord attrListRec = _records[0].GetAttribute(AttributeType.AttributeList);
|
|
if (attrListRec != null)
|
|
{
|
|
NtfsAttribute lastAttr = null;
|
|
|
|
StructuredNtfsAttribute<AttributeList> attrListAttr = (StructuredNtfsAttribute<AttributeList>)NtfsAttribute.FromRecord(this, MftReference, attrListRec);
|
|
var attrList = attrListAttr.Content;
|
|
_attributes.Add(attrListAttr);
|
|
|
|
foreach (var record in attrList)
|
|
{
|
|
FileRecord attrFileRecord = _records[0];
|
|
if (record.BaseFileReference.MftIndex != _records[0].MasterFileTableIndex)
|
|
{
|
|
if (!extraFileRecords.TryGetValue(record.BaseFileReference.MftIndex, out attrFileRecord))
|
|
{
|
|
attrFileRecord = _context.Mft.GetRecord(record.BaseFileReference);
|
|
if (attrFileRecord != null)
|
|
{
|
|
extraFileRecords[attrFileRecord.MasterFileTableIndex] = attrFileRecord;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (attrFileRecord != null)
|
|
{
|
|
AttributeRecord attrRec = attrFileRecord.GetAttribute(record.AttributeId);
|
|
|
|
if (attrRec != null)
|
|
{
|
|
if (record.StartVcn == 0)
|
|
{
|
|
lastAttr = NtfsAttribute.FromRecord(this, record.BaseFileReference, attrRec);
|
|
_attributes.Add(lastAttr);
|
|
}
|
|
else
|
|
{
|
|
lastAttr.AddExtent(record.BaseFileReference, attrRec);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (var extraFileRecord in extraFileRecords)
|
|
{
|
|
_records.Add(extraFileRecord.Value);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
foreach (var record in _records[0].Attributes)
|
|
{
|
|
_attributes.Add(NtfsAttribute.FromRecord(this, MftReference, record));
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool SplitAttribute(FileRecord record)
|
|
{
|
|
if (record.Attributes.Count != 1)
|
|
{
|
|
throw new InvalidOperationException("Attempting to split attribute in MFT record containing multiple attributes");
|
|
}
|
|
|
|
return SplitAttribute(record, (NonResidentAttributeRecord)record.FirstAttribute, false);
|
|
}
|
|
|
|
private bool SplitAttribute(FileRecord record, NonResidentAttributeRecord targetAttr, bool atStart)
|
|
{
|
|
if (targetAttr.DataRuns.Count <= 1)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
int splitIndex = 1;
|
|
if (!atStart)
|
|
{
|
|
List<DataRun> runs = targetAttr.DataRuns;
|
|
|
|
splitIndex = runs.Count - 1;
|
|
int saved = runs[splitIndex].Size;
|
|
while (splitIndex > 1 && record.Size - saved > record.AllocatedSize)
|
|
{
|
|
--splitIndex;
|
|
saved += runs[splitIndex].Size;
|
|
}
|
|
}
|
|
|
|
AttributeRecord newAttr = targetAttr.Split(splitIndex);
|
|
|
|
// Find a home for the new attribute record
|
|
FileRecord newAttrHome = null;
|
|
foreach (var targetRecord in _records)
|
|
{
|
|
if (!targetRecord.IsMftRecord && _mft.RecordSize - targetRecord.Size >= newAttr.Size)
|
|
{
|
|
targetRecord.AddAttribute(newAttr);
|
|
newAttrHome = targetRecord;
|
|
}
|
|
}
|
|
|
|
if (newAttrHome == null)
|
|
{
|
|
newAttrHome = _mft.AllocateRecord(_records[0].Flags & (~FileRecordFlags.InUse), record.IsMftRecord);
|
|
newAttrHome.BaseFile = record.BaseFile.IsNull ? record.Reference : record.BaseFile;
|
|
_records.Add(newAttrHome);
|
|
newAttrHome.AddAttribute(newAttr);
|
|
}
|
|
|
|
// Add the new attribute record as an extent on the attribute it split from
|
|
bool added = false;
|
|
foreach (var attr in _attributes)
|
|
{
|
|
foreach (var existingRecord in attr.Extents)
|
|
{
|
|
if (existingRecord.Key.File == record.Reference && existingRecord.Key.AttributeId == targetAttr.AttributeId)
|
|
{
|
|
attr.AddExtent(newAttrHome.Reference, newAttr);
|
|
added = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (added)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
UpdateAttributeList();
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool ExpelAttribute(FileRecord record)
|
|
{
|
|
if (record.MasterFileTableIndex == MasterFileTable.MftIndex)
|
|
{
|
|
// Special case for MFT - can't fully expel attributes, instead split most of the data runs off.
|
|
List<AttributeRecord> attrs = record.Attributes;
|
|
for (int i = attrs.Count - 1; i >= 0; --i)
|
|
{
|
|
AttributeRecord attr = attrs[i];
|
|
if (attr.AttributeType == AttributeType.Data)
|
|
{
|
|
if (SplitAttribute(record, (NonResidentAttributeRecord)attr, true))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
List<AttributeRecord> attrs = record.Attributes;
|
|
for (int i = attrs.Count - 1; i >= 0; --i)
|
|
{
|
|
AttributeRecord attr = attrs[i];
|
|
if (attr.AttributeType > AttributeType.AttributeList)
|
|
{
|
|
foreach (var targetRecord in _records)
|
|
{
|
|
if (_mft.RecordSize - targetRecord.Size >= attr.Size)
|
|
{
|
|
MoveAttribute(record, attr, targetRecord);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
FileRecord newFileRecord = _mft.AllocateRecord(FileRecordFlags.None, record.IsMftRecord);
|
|
newFileRecord.BaseFile = record.Reference;
|
|
_records.Add(newFileRecord);
|
|
MoveAttribute(record, attr, newFileRecord);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void MoveAttribute(FileRecord record, AttributeRecord attrRec, FileRecord targetRecord)
|
|
{
|
|
AttributeReference oldRef = new AttributeReference(record.Reference, attrRec.AttributeId);
|
|
|
|
record.RemoveAttribute(attrRec.AttributeId);
|
|
targetRecord.AddAttribute(attrRec);
|
|
|
|
AttributeReference newRef = new AttributeReference(targetRecord.Reference, attrRec.AttributeId);
|
|
|
|
foreach (var attr in _attributes)
|
|
{
|
|
attr.ReplaceExtent(oldRef, newRef, attrRec);
|
|
}
|
|
|
|
UpdateAttributeList();
|
|
}
|
|
|
|
private void CreateAttributeList()
|
|
{
|
|
ushort id = _records[0].CreateAttribute(AttributeType.AttributeList, null, false, AttributeFlags.None);
|
|
|
|
StructuredNtfsAttribute<AttributeList> newAttr = (StructuredNtfsAttribute<AttributeList>)NtfsAttribute.FromRecord(this, MftReference, _records[0].GetAttribute(id));
|
|
_attributes.Add(newAttr);
|
|
UpdateAttributeList();
|
|
}
|
|
|
|
private void UpdateAttributeList()
|
|
{
|
|
if (_records.Count > 1)
|
|
{
|
|
AttributeList attrList = new AttributeList();
|
|
|
|
foreach (var attr in _attributes)
|
|
{
|
|
if (attr.Type != AttributeType.AttributeList)
|
|
{
|
|
foreach (var extent in attr.Extents)
|
|
{
|
|
attrList.Add(AttributeListRecord.FromAttribute(extent.Value, extent.Key.File));
|
|
}
|
|
}
|
|
}
|
|
|
|
StructuredNtfsAttribute<AttributeList> alAttr;
|
|
alAttr = (StructuredNtfsAttribute<AttributeList>)GetAttribute(AttributeType.AttributeList, null);
|
|
alAttr.Content = attrList;
|
|
alAttr.Save();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new unnamed attribute.
|
|
/// </summary>
|
|
/// <param name="type">The type of the new attribute.</param>
|
|
/// <param name="flags">The flags of the new attribute.</param>
|
|
/// <returns>The new attribute.</returns>
|
|
private NtfsAttribute CreateAttribute(AttributeType type, AttributeFlags flags)
|
|
{
|
|
return CreateAttribute(type, null, flags);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new attribute.
|
|
/// </summary>
|
|
/// <param name="type">The type of the new attribute.</param>
|
|
/// <param name="name">The name of the new attribute.</param>
|
|
/// <param name="flags">The flags of the new attribute.</param>
|
|
/// <returns>The new attribute.</returns>
|
|
private NtfsAttribute CreateAttribute(AttributeType type, string name, AttributeFlags flags)
|
|
{
|
|
bool indexed = _context.AttributeDefinitions.IsIndexed(type);
|
|
ushort id = _records[0].CreateAttribute(type, name, indexed, flags);
|
|
|
|
AttributeRecord newAttrRecord = _records[0].GetAttribute(id);
|
|
NtfsAttribute newAttr = NtfsAttribute.FromRecord(this, MftReference, newAttrRecord);
|
|
|
|
_attributes.Add(newAttr);
|
|
UpdateAttributeList();
|
|
|
|
MarkMftRecordDirty();
|
|
|
|
return newAttr;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new attribute at a fixed cluster.
|
|
/// </summary>
|
|
/// <param name="type">The type of the new attribute.</param>
|
|
/// <param name="name">The name of the new attribute.</param>
|
|
/// <param name="flags">The flags of the new attribute.</param>
|
|
/// <param name="firstCluster">The first cluster to assign to the attribute.</param>
|
|
/// <param name="numClusters">The number of sequential clusters to assign to the attribute.</param>
|
|
/// <param name="bytesPerCluster">The number of bytes in each cluster.</param>
|
|
/// <returns>The new attribute.</returns>
|
|
private NtfsAttribute CreateAttribute(AttributeType type, string name, AttributeFlags flags, long firstCluster, ulong numClusters, uint bytesPerCluster)
|
|
{
|
|
bool indexed = _context.AttributeDefinitions.IsIndexed(type);
|
|
ushort id = _records[0].CreateNonResidentAttribute(type, name, flags, firstCluster, numClusters, bytesPerCluster);
|
|
|
|
AttributeRecord newAttrRecord = _records[0].GetAttribute(id);
|
|
NtfsAttribute newAttr = NtfsAttribute.FromRecord(this, MftReference, newAttrRecord);
|
|
|
|
_attributes.Add(newAttr);
|
|
UpdateAttributeList();
|
|
MarkMftRecordDirty();
|
|
return newAttr;
|
|
}
|
|
|
|
private void RemoveAttribute(NtfsAttribute attr)
|
|
{
|
|
if (attr != null)
|
|
{
|
|
if (attr.PrimaryRecord.AttributeType == AttributeType.IndexRoot)
|
|
{
|
|
_indexCache.Remove(attr.PrimaryRecord.Name);
|
|
}
|
|
|
|
RemoveAttributeExtents(attr);
|
|
|
|
_attributes.Remove(attr);
|
|
|
|
UpdateAttributeList();
|
|
}
|
|
}
|
|
|
|
private bool ShrinkIndexRoot(string indexName)
|
|
{
|
|
NtfsAttribute attr = GetAttribute(AttributeType.IndexRoot, indexName);
|
|
|
|
// Nothing to win, can't make IndexRoot smaller than this
|
|
// 8 = min size of entry that points to IndexAllocation...
|
|
if (attr.Length <= IndexRoot.HeaderOffset + IndexHeader.Size + 8)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Index idx = GetIndex(indexName);
|
|
return idx.ShrinkRoot();
|
|
}
|
|
|
|
private void MakeAttributeResident(AttributeReference attrRef, int maxData)
|
|
{
|
|
NtfsAttribute attr = GetAttribute(attrRef);
|
|
if (!attr.IsNonResident)
|
|
{
|
|
throw new InvalidOperationException("Attribute is already resident");
|
|
}
|
|
|
|
ushort id = _records[0].CreateAttribute(attr.Type, attr.Name, _context.AttributeDefinitions.IsIndexed(attr.Type), attr.Flags);
|
|
AttributeRecord newAttrRecord = _records[0].GetAttribute(id);
|
|
|
|
IBuffer attrBuffer = attr.GetDataBuffer();
|
|
byte[] tempData = Utilities.ReadFully(attrBuffer, 0, (int)Math.Min(maxData, attrBuffer.Capacity));
|
|
|
|
RemoveAttributeExtents(attr);
|
|
attr.SetExtent(_records[0].Reference, newAttrRecord);
|
|
|
|
attr.GetDataBuffer().Write(0, tempData, 0, tempData.Length);
|
|
|
|
UpdateAttributeList();
|
|
}
|
|
|
|
private FileRecord GetFileRecord(FileRecordReference fileReference)
|
|
{
|
|
foreach (var record in _records)
|
|
{
|
|
if (record.MasterFileTableIndex == fileReference.MftIndex)
|
|
{
|
|
return record;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private void RemoveFileRecord(FileRecordReference fileReference)
|
|
{
|
|
for (int i = 0; i < _records.Count; ++i)
|
|
{
|
|
if (_records[i].MasterFileTableIndex == fileReference.MftIndex)
|
|
{
|
|
FileRecord record = _records[i];
|
|
|
|
if (record.Attributes.Count > 0)
|
|
{
|
|
throw new IOException("Attempting to remove non-empty MFT record");
|
|
}
|
|
|
|
_context.Mft.RemoveRecord(fileReference);
|
|
_records.Remove(record);
|
|
|
|
if (_records.Count == 1)
|
|
{
|
|
NtfsAttribute attrListAttr = GetAttribute(AttributeType.AttributeList, null);
|
|
if (attrListAttr != null)
|
|
{
|
|
RemoveAttribute(attrListAttr);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wrapper for Resident/Non-Resident attribute streams, that remains valid
|
|
/// despite the attribute oscillating between resident and not.
|
|
/// </summary>
|
|
private class FileStream : SparseStream
|
|
{
|
|
private File _file;
|
|
private NtfsAttribute _attr;
|
|
private SparseStream _wrapped;
|
|
|
|
public FileStream(File file, NtfsAttribute attr, FileAccess access)
|
|
{
|
|
_file = file;
|
|
_attr = attr;
|
|
_wrapped = attr.Open(access);
|
|
}
|
|
|
|
public override IEnumerable<StreamExtent> Extents
|
|
{
|
|
get
|
|
{
|
|
return _wrapped.Extents;
|
|
}
|
|
}
|
|
|
|
public override bool CanRead
|
|
{
|
|
get
|
|
{
|
|
return _wrapped.CanRead;
|
|
}
|
|
}
|
|
|
|
public override bool CanSeek
|
|
{
|
|
get
|
|
{
|
|
return _wrapped.CanSeek;
|
|
}
|
|
}
|
|
|
|
public override bool CanWrite
|
|
{
|
|
get
|
|
{
|
|
return _wrapped.CanWrite;
|
|
}
|
|
}
|
|
|
|
public override long Length
|
|
{
|
|
get
|
|
{
|
|
return _wrapped.Length;
|
|
}
|
|
}
|
|
|
|
public override long Position
|
|
{
|
|
get
|
|
{
|
|
return _wrapped.Position;
|
|
}
|
|
|
|
set
|
|
{
|
|
_wrapped.Position = value;
|
|
}
|
|
}
|
|
|
|
public override void Close()
|
|
{
|
|
base.Close();
|
|
_wrapped.Close();
|
|
}
|
|
|
|
public override void Flush()
|
|
{
|
|
_wrapped.Flush();
|
|
}
|
|
|
|
public override int Read(byte[] buffer, int offset, int count)
|
|
{
|
|
return _wrapped.Read(buffer, offset, count);
|
|
}
|
|
|
|
public override long Seek(long offset, SeekOrigin origin)
|
|
{
|
|
return _wrapped.Seek(offset, origin);
|
|
}
|
|
|
|
public override void SetLength(long value)
|
|
{
|
|
ChangeAttributeResidencyByLength(value);
|
|
_wrapped.SetLength(value);
|
|
}
|
|
|
|
public override void Write(byte[] buffer, int offset, int count)
|
|
{
|
|
if (_wrapped.Position + count > Length)
|
|
{
|
|
ChangeAttributeResidencyByLength(_wrapped.Position + count);
|
|
}
|
|
|
|
_wrapped.Write(buffer, offset, count);
|
|
}
|
|
|
|
public override void Clear(int count)
|
|
{
|
|
if (_wrapped.Position + count > Length)
|
|
{
|
|
ChangeAttributeResidencyByLength(_wrapped.Position + count);
|
|
}
|
|
|
|
_wrapped.Clear(count);
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
return _file.ToString() + ".attr[" + _attr.Id + "]";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Change attribute residency if it gets too big (or small).
|
|
/// </summary>
|
|
/// <param name="value">The new (anticipated) length of the stream.</param>
|
|
/// <remarks>Has hysteresis - the decision is based on the input and the current
|
|
/// state, not the current state alone.</remarks>
|
|
private void ChangeAttributeResidencyByLength(long value)
|
|
{
|
|
// This is a bit of a hack - but it's really important the bitmap file remains non-resident
|
|
if (_file._records[0].MasterFileTableIndex == MasterFileTable.BitmapIndex)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!_attr.IsNonResident && value >= _file.MaxMftRecordSize)
|
|
{
|
|
_file.MakeAttributeNonResident(_attr.Reference, (int)Math.Min(value, _wrapped.Length));
|
|
}
|
|
else if (_attr.IsNonResident && value <= _file.MaxMftRecordSize / 4)
|
|
{
|
|
// Use of 1/4 of record size here is just a heuristic - the important thing is not to end up with
|
|
// zero-length non-resident attributes
|
|
_file.MakeAttributeResident(_attr.Reference, (int)value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|