在Windows文件系统中,如何在不读取整个文件的情况下将字节插入到文件中间(使用文件分配表)?

30
我需要一个方法在文件中间插入一些文件簇以插入数据。
通常,我会读取整个文件并将其写回,但是这些文件的大小为多个千兆字节,并且读取文件和重新写入文件需要30分钟左右。
文件簇的大小不会困扰我;我可以在我的插入簇的末尾写入零,这样它仍然可以在这种文件格式中运行。
我该如何使用Windows文件API(或其他机制)来修改文件的文件分配表,在文件中指定的位置插入一个或多个未使用的簇?

3
如果可能的话,你最好设计文件格式,使得内容可以在末尾添加,但看起来就像是插入的一样。 - David Heffernan
1
处理这种情况的常规方法是,当文件格式受控时,通过将字节追加到文件末尾来处理插入操作,但同时更新内部结构,使得这些追加块出现在结构的中间。数据库文件就是一个很好的例子。 - codekaizen
2
@minitech:完全没有问题。我已经安装它们了。 :) - Robert Harvey
3
其他人似乎都认为它有用,所以如果你有好的想法,现在是在回答中提出它们的时候了。 - Robert Harvey
2
你需要支持压缩和稀疏文件吗?那会更加复杂。 - Simon Mourier
显示剩余26条评论
9个回答

26
[EDIT:] 这个问题很难解决,至少通过修改MFT是不可行的,因为NTFS MFT结构本身并不完全“开放”,所以我开始涉足逆向工程领域,这将带来法律后果,而我不想处理。此外,在.NET中进行此操作是一项超级繁琐的过程,需要基于许多猜测来映射和调用结构(更不用说大多数MFT结构以奇怪的方式压缩了)。简而言之,虽然我确实学到了关于NTFS“工作”的很多知识,但我离解决这个问题还有很长的路要走。 [/EDIT]
呃......太多的调用和调度了....
这引起了我的注意,因此我被迫探究这个问题...这仍然是一个“正在进行中的答案”,但我想发布我所拥有的所有内容,以帮助其他人得出一些结论。 :)
此外,我大致感觉在FAT32上会容易得多,但考虑到我只有NTFS可以使用...
所以 - 很多pinvoking和marshalling,让我们从那里开始并向后工作:
正如人们可能猜到的那样,标准的.NET文件/IO API在这里无法帮助你太多 - 我们需要设备级别的访问:
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
static extern SafeFileHandle CreateFile(
    string lpFileName,
    [MarshalAs(UnmanagedType.U4)] FileAccess dwDesiredAccess,
    [MarshalAs(UnmanagedType.U4)] FileShare dwShareMode,
    IntPtr lpSecurityAttributes,
    [MarshalAs(UnmanagedType.U4)] FileMode dwCreationDisposition,
    [MarshalAs(UnmanagedType.U4)] FileAttributes dwFlagsAndAttributes,
    IntPtr hTemplateFile);

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern bool ReadFile(
    SafeFileHandle hFile,      // handle to file
    byte[] pBuffer,        // data buffer, should be fixed
    int NumberOfBytesToRead,  // number of bytes to read
    IntPtr pNumberOfBytesRead,  // number of bytes read, provide NULL here
    ref NativeOverlapped lpOverlapped // should be fixed, if not null
);

[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern bool SetFilePointerEx(
    SafeFileHandle hFile,
    long liDistanceToMove,
    out long lpNewFilePointer,
    SeekOrigin dwMoveMethod);

我们将这些令人讨厌的win32野兽用于以下方式:
// To the metal, baby!
using (var fileHandle = NativeMethods.CreateFile(
    // Magic "give me the device" syntax
    @"\\.\c:",
    // MUST explicitly provide both of these, not ReadWrite
    FileAccess.Read | FileAccess.Write,
    // MUST explicitly provide both of these, not ReadWrite
    FileShare.Write | FileShare.Read,
    IntPtr.Zero,
    FileMode.Open,
    FileAttributes.Normal,
    IntPtr.Zero))
{
    if (fileHandle.IsInvalid)
    {
        // Doh!
        throw new Win32Exception();
    }
    else
    {
        // Boot sector ~ 512 bytes long
        byte[] buffer = new byte[512];
        NativeOverlapped overlapped = new NativeOverlapped();
        NativeMethods.ReadFile(fileHandle, buffer, buffer.Length, IntPtr.Zero, ref overlapped);

        // Pin it so we can transmogrify it into a FAT structure
        var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
        try
        {
            // note, I've got an NTFS drive, change yours to suit
            var bootSector = (BootSector_NTFS)Marshal.PtrToStructure(
                 handle.AddrOfPinnedObject(), 
                 typeof(BootSector_NTFS));

哇,哇哇哇 - 什么是 BootSector_NTFS?它是一个字节映射的 struct,据我所知,它最接近 NTFS 结构(也包括 FAT32):

[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi, Pack=0)]
public struct JumpBoot
{
    [MarshalAs(UnmanagedType.ByValArray, ArraySubType=UnmanagedType.U1, SizeConst=3)]
    public byte[] BS_jmpBoot;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst=8)]
    public string BS_OEMName;
}

[StructLayout(LayoutKind.Explicit, CharSet = CharSet.Ansi, Pack = 0, Size = 90)]
public struct BootSector_NTFS
{
    [FieldOffset(0)]
    public JumpBoot JumpBoot;
    [FieldOffset(0xb)]
    public short BytesPerSector;
    [FieldOffset(0xd)]
    public byte SectorsPerCluster;
    [FieldOffset(0xe)]
    public short ReservedSectorCount;
    [FieldOffset(0x10)]
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)]
    public byte[] Reserved0_MUSTBEZEROs;
    [FieldOffset(0x15)]
    public byte BPB_Media;
    [FieldOffset(0x16)]
    public short Reserved1_MUSTBEZERO;
    [FieldOffset(0x18)]
    public short SectorsPerTrack;
    [FieldOffset(0x1A)]
    public short HeadCount;
    [FieldOffset(0x1c)]
    public int HiddenSectorCount;
    [FieldOffset(0x20)]
    public int LargeSectors;
    [FieldOffset(0x24)]
    public int Reserved6;
    [FieldOffset(0x28)]
    public long TotalSectors;
    [FieldOffset(0x30)]
    public long MftClusterNumber;
    [FieldOffset(0x38)]
    public long MftMirrorClusterNumber;
    [FieldOffset(0x40)]
    public byte ClustersPerMftRecord;
    [FieldOffset(0x41)]
    public byte Reserved7;
    [FieldOffset(0x42)]
    public short Reserved8;
    [FieldOffset(0x44)]
    public byte ClustersPerIndexBuffer;
    [FieldOffset(0x45)]
    public byte Reserved9;
    [FieldOffset(0x46)]
    public short ReservedA;
    [FieldOffset(0x48)]
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
    public byte[] SerialNumber;
    [FieldOffset(0x50)]
    public int Checksum;
    [FieldOffset(0x54)]
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x1AA)]
    public byte[] BootupCode;
    [FieldOffset(0x1FE)]
    public ushort EndOfSectorMarker;

    public long GetMftAbsoluteIndex(int recordIndex = 0)
    {
        return (BytesPerSector * SectorsPerCluster * MftClusterNumber) + (GetMftEntrySize() * recordIndex);
    }
    public long GetMftEntrySize()
    {
        return (BytesPerSector * SectorsPerCluster * ClustersPerMftRecord);
    }
}


// Note: dont have fat32, so can't verify all these...they *should* work, tho
// refs:
//    http://www.pjrc.com/tech/8051/ide/fat32.html
//    http://msdn.microsoft.com/en-US/windows/hardware/gg463084
[StructLayout(LayoutKind.Explicit, CharSet=CharSet.Auto, Pack=0, Size=90)]
public struct BootSector_FAT32
{
    [FieldOffset(0)]
    public JumpBoot JumpBoot;    
    [FieldOffset(11)]
    public short BPB_BytsPerSec;
    [FieldOffset(13)]
    public byte BPB_SecPerClus;
    [FieldOffset(14)]
    public short BPB_RsvdSecCnt;
    [FieldOffset(16)]
    public byte BPB_NumFATs;
    [FieldOffset(17)]
    public short BPB_RootEntCnt;
    [FieldOffset(19)]
    public short BPB_TotSec16;
    [FieldOffset(21)]
    public byte BPB_Media;
    [FieldOffset(22)]
    public short BPB_FATSz16;
    [FieldOffset(24)]
    public short BPB_SecPerTrk;
    [FieldOffset(26)]
    public short BPB_NumHeads;
    [FieldOffset(28)]
    public int BPB_HiddSec;
    [FieldOffset(32)]
    public int BPB_TotSec32;
    [FieldOffset(36)]
    public FAT32 FAT;
}

[StructLayout(LayoutKind.Sequential)]
public struct FAT32
{
    public int BPB_FATSz32;
    public short BPB_ExtFlags;
    public short BPB_FSVer;
    public int BPB_RootClus;
    public short BPB_FSInfo;
    public short BPB_BkBootSec;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst=12)]
    public byte[] BPB_Reserved;
    public byte BS_DrvNum;
    public byte BS_Reserved1;
    public byte BS_BootSig;
    public int BS_VolID;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst=11)] 
    public string BS_VolLab;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst=8)] 
    public string BS_FilSysType;
}

现在我们可以将一大堆乱码映射回到这个结构中:

// Pin it so we can transmogrify it into a FAT structure
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
    try
    {            
        // note, I've got an NTFS drive, change yours to suit
        var bootSector = (BootSector_NTFS)Marshal.PtrToStructure(
              handle.AddrOfPinnedObject(), 
              typeof(BootSector_NTFS));
        Console.WriteLine(
            "I think that the Master File Table is at absolute position:{0}, sector:{1}", 
            bootSector.GetMftAbsoluteIndex(),
            bootSector.GetMftAbsoluteIndex() / bootSector.BytesPerSector);

此时输出如下:

I think that the Master File Table is at 
absolute position:3221225472, sector:6291456

让我们使用原始设备制造商支持工具nfi.exe来快速确认:

C:\tools\OEMTools\nfi>nfi c:
NTFS File Sector Information Utility.
Copyright (C) Microsoft Corporation 1999. All rights reserved.


File 0
Master File Table ($Mft)
    $STANDARD_INFORMATION (resident)
    $FILE_NAME (resident)
    $DATA (nonresident)
        logical sectors 6291456-6487039 (0x600000-0x62fbff)
        logical sectors 366267960-369153591 (0x15d4ce38-0x1600d637)
    $BITMAP (nonresident)
        logical sectors 6291448-6291455 (0x5ffff8-0x5fffff)
        logical sectors 7273984-7274367 (0x6efe00-0x6eff7f)

酷,看起来我们在正确的轨道上...继续前进!
            // If you've got LinqPad, uncomment this to look at boot sector
            bootSector.Dump();

    Console.WriteLine("Jumping to Master File Table...");
    long lpNewFilePointer;
    if (!NativeMethods.SetFilePointerEx(
            fileHandle, 
            bootSector.GetMftAbsoluteIndex(), 
            out lpNewFilePointer, 
            SeekOrigin.Begin))
    {
        throw new Win32Exception();
    }
    Console.WriteLine("Position now: {0}", lpNewFilePointer);

    // Read in one MFT entry
    byte[] mft_buffer = new byte[bootSector.GetMftEntrySize()];
    Console.WriteLine("Reading $MFT entry...calculated size: 0x{0}",
       bootSector.GetMftEntrySize().ToString("X"));

    var seekIndex = bootSector.GetMftAbsoluteIndex();
    overlapped.OffsetHigh = (int)(seekIndex >> 32);
    overlapped.OffsetLow = (int)seekIndex;
    NativeMethods.ReadFile(
          fileHandle, 
          mft_buffer, 
          mft_buffer.Length, 
          IntPtr.Zero, 
          ref overlapped);
    // Pin it for transmogrification
    var mft_handle = GCHandle.Alloc(mft_buffer, GCHandleType.Pinned);
    try
    {
        var mftRecords = (MFTSystemRecords)Marshal.PtrToStructure(
              mft_handle.AddrOfPinnedObject(), 
              typeof(MFTSystemRecords));
        mftRecords.Dump();
    }
    finally
    {
        // make sure we clean up
        mft_handle.Free();
    }
}
finally
{
    // make sure we clean up
    handle.Free();
}

啊,还有更多的本地结构需要讨论 - 因此MFT的排列是这样的,前16个或左右的条目是“固定”的:

[StructLayout(LayoutKind.Sequential)]
public struct MFTSystemRecords
{
    public MFTRecord Mft;
    public MFTRecord MftMirror;
    public MFTRecord LogFile;
    public MFTRecord Volume;
    public MFTRecord AttributeDefs;
    public MFTRecord RootFile;
    public MFTRecord ClusterBitmap;
    public MFTRecord BootSector;
    public MFTRecord BadClusterFile;
    public MFTRecord SecurityFile;
    public MFTRecord UpcaseTable;
    public MFTRecord ExtensionFile;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
    public MFTRecord[] MftReserved;
    public MFTRecord MftFileExt;
}

这里的MFTRecord是:

[StructLayout(LayoutKind.Sequential, Size = 1024)]
public struct MFTRecord
{
    const int BASE_RECORD_SIZE = 48;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 4)]
    public string Type;
    public short UsaOffset;
    public short UsaCount;
    public long Lsn;  /* $LogFile sequence number for this record. Changed every time the record is modified. */
    public short SequenceNumber; /* # of times this record has been reused */
    public short LinkCount;  /* Number of hard links, i.e. the number of directory entries referencing this record. */
    public short AttributeOffset; /* Byte offset to the first attribute in this mft record from the start of the mft record. */
    public short MftRecordFlags;
    public int BytesInUse;
    public int BytesAllocated;
    public long BaseFileRecord;
    public short NextAttributeNumber;
    public short Reserved;
    public int MftRecordNumber;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 976)]
    public byte[] Data;
    public byte[] SetData
    {
        get
        {
            return this.Data
               .Skip(AttributeOffset - BASE_RECORD_SIZE)
               .Take(BytesInUse - BASE_RECORD_SIZE)
               .ToArray();
        }
    }
    public MftAttribute[] Attributes
    {
        get
        {
            var idx = 0;
            var ret = new List<MftAttribute>();
            while (idx < SetData.Length)
            {
                var attr = MftAttribute.FromBytes(SetData.Skip(idx).ToArray());
                ret.Add(attr);
                idx += attr.Attribute.Length;
                // A special "END" attribute denotes the end of the list
                if (attr.Attribute.AttributeType == MftAttributeType.AT_END) break;
            }
            return ret.ToArray();
        }
    }
}

这里是我的限制,我得去吃晚饭之类的原因所在。但是我会回来的!
参考文献(部分为了自己记忆,部分为了协助其他调查人员): 以下是完整的代码转储:

上面我略过的所有本地映射(由于帖子大小限制而不是全面回顾):

public enum MftRecordFlags : ushort
{
    MFT_RECORD_IN_USE = 0x0001,
    MFT_RECORD_IS_DIRECTORY = 0x0002,
    MFT_RECORD_IN_EXTEND = 0x0004,
    MFT_RECORD_IS_VIEW_INDEX = 0x0008,
    MFT_REC_SPACE_FILLER = 0xffff
}
public enum MftAttributeType : uint
{
    AT_UNUSED = 0,
    AT_STANDARD_INFORMATION = 0x10,
    AT_ATTRIBUTE_LIST = 0x20,
    AT_FILENAME = 0x30,
    AT_OBJECT_ID = 0x40,
    AT_SECURITY_DESCRIPTOR = 0x50,
    AT_VOLUME_NAME = 0x60,
    AT_VOLUME_INFORMATION = 0x70,
    AT_DATA = 0x80,
    AT_INDEX_ROOT = 0x90,
    AT_INDEX_ALLOCATION = 0xa0,
    AT_BITMAP = 0xb0,
    AT_REPARSE_POINT = 0xc0,
    AT_EA_INFORMATION = 0xd0,
    AT_EA = 0xe0,
    AT_PROPERTY_SET = 0xf0,
    AT_LOGGED_UTILITY_STREAM = 0x100,
    AT_FIRST_USER_DEFINED_ATTRIBUTE = 0x1000,
    AT_END = 0xffffffff
}

public enum MftAttributeDefFlags : byte
{
    ATTR_DEF_INDEXABLE = 0x02, /* Attribute can be indexed. */
    ATTR_DEF_MULTIPLE = 0x04, /* Attribute type can be present multiple times in the mft records of an inode. */
    ATTR_DEF_NOT_ZERO = 0x08, /* Attribute value must contain at least one non-zero byte. */
    ATTR_DEF_INDEXED_UNIQUE = 0x10, /* Attribute must be indexed and the attribute value must be unique for the attribute type in all of the mft records of an inode. */
    ATTR_DEF_NAMED_UNIQUE = 0x20, /* Attribute must be named and the name must be unique for the attribute type in all of the mft records of an inode. */
    ATTR_DEF_RESIDENT = 0x40, /* Attribute must be resident. */
    ATTR_DEF_ALWAYS_LOG = 0x80, /* Always log modifications to this attribute, regardless of whether it is resident or
                non-resident.  Without this, only log modifications if the attribute is resident. */
}

[StructLayout(LayoutKind.Explicit)]
public struct MftInternalAttribute
{
    [FieldOffset(0)]
    public MftAttributeType AttributeType;
    [FieldOffset(4)]
    public int Length;
    [FieldOffset(8)]
    [MarshalAs(UnmanagedType.Bool)]
    public bool NonResident;
    [FieldOffset(9)]
    public byte NameLength;
    [FieldOffset(10)]
    public short NameOffset;
    [FieldOffset(12)]
    public int AttributeFlags;
    [FieldOffset(14)]
    public short Instance;
    [FieldOffset(16)]
    public ResidentAttribute ResidentAttribute;
    [FieldOffset(16)]
    public NonResidentAttribute NonResidentAttribute;
}

[StructLayout(LayoutKind.Sequential)]
public struct ResidentAttribute
{
    public int ValueLength;
    public short ValueOffset;
    public byte ResidentAttributeFlags;
    public byte Reserved;

    public override string ToString()
    {
        return string.Format("{0}:{1}:{2}:{3}", ValueLength, ValueOffset, ResidentAttributeFlags, Reserved);
    }
}
[StructLayout(LayoutKind.Sequential)]
public struct NonResidentAttribute
{
    public long LowestVcn;
    public long HighestVcn;
    public short MappingPairsOffset;
    public byte CompressionUnit;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)]
    public byte[] Reserved;
    public long AllocatedSize;
    public long DataSize;
    public long InitializedSize;
    public long CompressedSize;
    public override string ToString()
    {
        return string.Format("{0}:{1}:{2}:{3}:{4}:{5}:{6}:{7}", LowestVcn, HighestVcn, MappingPairsOffset, CompressionUnit, AllocatedSize, DataSize, InitializedSize, CompressedSize);
    }
}

public struct MftAttribute
{
    public MftInternalAttribute Attribute;

    [field: NonSerialized]
    public string Name;

    [field: NonSerialized]
    public byte[] Data;

    [field: NonSerialized]
    public object Payload;

    public static MftAttribute FromBytes(byte[] buffer)
    {
        var hnd = GCHandle.Alloc(buffer, GCHandleType.Pinned);
        try
        {
            var attr = (MftInternalAttribute)Marshal.PtrToStructure(hnd.AddrOfPinnedObject(), typeof(MftInternalAttribute));
            var ret = new MftAttribute() { Attribute = attr };
            ret.Data = buffer.Skip(Marshal.SizeOf(attr)).Take(attr.Length).ToArray();
            if (ret.Attribute.AttributeType == MftAttributeType.AT_STANDARD_INFORMATION)
            {
                var payloadHnd = GCHandle.Alloc(ret.Data, GCHandleType.Pinned);
                try
                {
                    var payload = (MftStandardInformation)Marshal.PtrToStructure(payloadHnd.AddrOfPinnedObject(), typeof(MftStandardInformation));
                    ret.Payload = payload;
                }
                finally
                {
                    payloadHnd.Free();
                }
            }
            return ret;
        }
        finally
        {
            hnd.Free();
        }
    }
}

[StructLayout(LayoutKind.Sequential)]
public struct MftStandardInformation
{
    public ulong CreationTime;
    public ulong LastDataChangeTime;
    public ulong LastMftChangeTime;
    public ulong LastAccessTime;
    public int FileAttributes;
    public int MaximumVersions;
    public int VersionNumber;
    public int ClassId;
    public int OwnerId;
    public int SecurityId;
    public long QuotaChanged;
    public long Usn;
}

// Note: dont have fat32, so can't verify all these...they *should* work, tho
// refs:
//    http://www.pjrc.com/tech/8051/ide/fat32.html
//    http://msdn.microsoft.com/en-US/windows/hardware/gg463084
[StructLayout(LayoutKind.Explicit, CharSet = CharSet.Auto, Pack = 0, Size = 90)]
public struct BootSector_FAT32
{
    [FieldOffset(0)]
    public JumpBoot JumpBoot;
    [FieldOffset(11)]
    public short BPB_BytsPerSec;
    [FieldOffset(13)]
    public byte BPB_SecPerClus;
    [FieldOffset(14)]
    public short BPB_RsvdSecCnt;
    [FieldOffset(16)]
    public byte BPB_NumFATs;
    [FieldOffset(17)]
    public short BPB_RootEntCnt;
    [FieldOffset(19)]
    public short BPB_TotSec16;
    [FieldOffset(21)]
    public byte BPB_Media;
    [FieldOffset(22)]
    public short BPB_FATSz16;
    [FieldOffset(24)]
    public short BPB_SecPerTrk;
    [FieldOffset(26)]
    public short BPB_NumHeads;
    [FieldOffset(28)]
    public int BPB_HiddSec;
    [FieldOffset(32)]
    public int BPB_TotSec32;
    [FieldOffset(36)]
    public FAT32 FAT;
}

[StructLayout(LayoutKind.Sequential)]
public struct FAT32
{
    public int BPB_FATSz32;
    public short BPB_ExtFlags;
    public short BPB_FSVer;
    public int BPB_RootClus;
    public short BPB_FSInfo;
    public short BPB_BkBootSec;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 12)]
    public byte[] BPB_Reserved;
    public byte BS_DrvNum;
    public byte BS_Reserved1;
    public byte BS_BootSig;
    public int BS_VolID;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 11)]
    public string BS_VolLab;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 8)]
    public string BS_FilSysType;
}

还有测试工具:

class Program
{        
    static void Main(string[] args)
    {
        // To the metal, baby!
        using (var fileHandle = NativeMethods.CreateFile(
            // Magic "give me the device" syntax
            @"\\.\c:",
            // MUST explicitly provide both of these, not ReadWrite
            FileAccess.Read | FileAccess.Write,
            // MUST explicitly provide both of these, not ReadWrite
            FileShare.Write | FileShare.Read,
            IntPtr.Zero,
            FileMode.Open,
            FileAttributes.Normal,
            IntPtr.Zero))
        {
            if (fileHandle.IsInvalid)
            {
                // Doh!
                throw new Win32Exception();
            }
            else
            {
                // Boot sector ~ 512 bytes long
                byte[] buffer = new byte[512];
                NativeOverlapped overlapped = new NativeOverlapped();
                NativeMethods.ReadFile(fileHandle, buffer, buffer.Length, IntPtr.Zero, ref overlapped);

                // Pin it so we can transmogrify it into a FAT structure
                var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
                try
                {
                    // note, I've got an NTFS drive, change yours to suit
                    var bootSector = (BootSector_NTFS)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(BootSector_NTFS));
                    Console.WriteLine(
                        "I think that the Master File Table is at absolute position:{0}, sector:{1}",
                        bootSector.GetMftAbsoluteIndex(),
                        bootSector.GetMftAbsoluteIndex() / bootSector.BytesPerSector);
                    Console.WriteLine("MFT record size:{0}", bootSector.ClustersPerMftRecord * bootSector.SectorsPerCluster * bootSector.BytesPerSector);

                    // If you've got LinqPad, uncomment this to look at boot sector
                    bootSector.DumpToHtmlString();

                    Pause();

                    Console.WriteLine("Jumping to Master File Table...");
                    long lpNewFilePointer;
                    if (!NativeMethods.SetFilePointerEx(fileHandle, bootSector.GetMftAbsoluteIndex(), out lpNewFilePointer, SeekOrigin.Begin))
                    {
                        throw new Win32Exception();
                    }
                    Console.WriteLine("Position now: {0}", lpNewFilePointer);

                    // Read in one MFT entry
                    byte[] mft_buffer = new byte[bootSector.GetMftEntrySize()];
                    Console.WriteLine("Reading $MFT entry...calculated size: 0x{0}", bootSector.GetMftEntrySize().ToString("X"));

                    var seekIndex = bootSector.GetMftAbsoluteIndex();
                    overlapped.OffsetHigh = (int)(seekIndex >> 32);
                    overlapped.OffsetLow = (int)seekIndex;
                    NativeMethods.ReadFile(fileHandle, mft_buffer, mft_buffer.Length, IntPtr.Zero, ref overlapped);
                    // Pin it for transmogrification
                    var mft_handle = GCHandle.Alloc(mft_buffer, GCHandleType.Pinned);
                    try
                    {
                        var mftRecords = (MFTSystemRecords)Marshal.PtrToStructure(mft_handle.AddrOfPinnedObject(), typeof(MFTSystemRecords));
                        mftRecords.DumpToHtmlString();
                    }
                    finally
                    {
                        // make sure we clean up
                        mft_handle.Free();
                    }
                }
                finally
                {
                    // make sure we clean up
                    handle.Free();
                }
            }
        }
        Pause();
    }

    private static void Pause()
    {
        Console.WriteLine("Press enter to continue...");
        Console.ReadLine();
    }
}


public static class Dumper
{
    public static string DumpToHtmlString<T>(this T objectToSerialize)
    {
        string strHTML = "";
        try
        {
            var writer = LINQPad.Util.CreateXhtmlWriter(true);
            writer.Write(objectToSerialize);
            strHTML = writer.ToString();
        }
        catch (Exception exc)
        {
            Debug.Assert(false, "Investigate why ?" + exc);
        }

        var shower = new Thread(
            () =>
                {
                    var dumpWin = new Window();
                    var browser = new WebBrowser();
                    dumpWin.Content = browser;
                    browser.NavigateToString(strHTML);
                    dumpWin.ShowDialog();                        
                });
        shower.SetApartmentState(ApartmentState.STA);
        shower.Start();
        return strHTML;
    }

    public static string Dump(this object value)
    {
         return JsonConvert.SerializeObject(value, Formatting.Indented);
    }
}

@NikBougalis 嘿 - 谢谢,但最终是徒劳的;我认为那里有一些希望,但它变得非常棘手...不过还是挺有趣的。;) - JerKimball
1
恭喜获得赏金!正如你所提到的,我们还没有接近实际操作分配表。这需要内核级别访问、分配表和相关文件的独占锁,以及可能需要不断更新以确保新版本的Windows不会破坏它。我记得在旧的8位时代,当一切都是开放的时候,这将是易如反掌的,只需要40行代码就可以了。就像你说的,这是一个有趣的转移注意力的小玩意儿,我很高兴它引起了这么多关注。也许这是一个提示,特别是在视频编辑领域,提供插入数据API! - Yimin Rong
@YiminRong 哈哈,我甚至没有注意到 :) 你是正确的,这里还有很多工作要做;我计划将其收藏起来,每当我有时间扩展时回来(至少直到我达到 SO 的限制)。 - JerKimball

7
罗伯特,我认为你想实现的目标在没有主动操作文件系统数据结构的情况下是不可能做到的。听起来这个文件系统已经被挂载了。我不认为我需要告诉你这种练习是多么的 危险不明智
但是如果你需要这样做,我想我可以给你提供一个"餐巾纸背面的草图",以帮助你入门:
你可以利用NTFS的“稀疏文件”支持,通过调整LCN / VCN映射来添加“间隙”。一旦你这样做了,只需打开文件,定位到新位置并写入数据即可。NTFS将透明地分配空间并在文件中间写入数据,即你创建的空洞处。
要了解更多,请参阅有关 NTFS碎片整理支持 的页面,以获取有关如何稍微操纵一下以允许你在文件中间插入簇的提示。至少使用官方API进行此类操作,你不太可能使文件系统无法修复,虽然你仍然可能会破坏文件。
获取所需文件的检索指针,将它们分裂到需要添加尽可能多的额外空间的位置,并移动这个文件。Russinovich / Ionescu的“Windows Internals”书中有关于这方面内容的有趣章节(http://www.amazon.com/Windows%C2%AE-Internals-Including-Windows-Developer/dp/0735625301)。

这基本上是我到目前为止已经弄清楚的。除非有一些已经做过这个的软件库,否则我不指望任何人能够立即回答这个问题。赏金过期前有7天的时间窗口。我愿意等待。 :) - Robert Harvey
我想我可以玩一下这个...谁需要睡眠呢?;) - Nik Bougalis
NTFS从Windows8开始不再支持稀疏文件,而FAT32也不存在此功能。即使在稀疏文件中,您也无法随意插入节。如果您已经写入了0-100和100-200字节,则即使在稀疏文件中,您也无法在位置100处插入100个字节。 - SecurityMatt
我的观点是使用碎片整理API重新排列文件的LCN/VCN映射,并有效地创建一个“空洞”,该空洞将被解释为文件的稀疏部分。 - Nik Bougalis
在文件中间插入内容会导致文件碎片化,我首先想到的是碎片整理 API。但我们需要的是 FSCTL_GET_RETRIEVAL_POINTERS 的相反操作(我们需要一个 setter)。我们可以移动簇,但我仍然无法弄清如何打破链条。如果一个文件由簇 1-2-3-4-5 组成,我们可以将这些簇物理上按照 5-4-3-2-1 的顺序排列,但它们在逻辑上仍然保持原来的顺序。换句话说,我们需要写入 MFT,以便物理结构 1-2-3-4-5-9 被逻辑上读作 1-2-3-9-4-5。 - ixe013
1
@ixe013:FSCTL_MOVE_FILE大致相当于那个。关键是它不允许你调整VCN。 - Nik Bougalis

2
不行。在Windows中直接插入文件的内容是不可能的。因为在Windows中,文件是一系列逻辑连续的字节,如果插入字节而不覆盖原有内容则是不可能的。要了解其中的原因,我们来进行一个思想实验。
首先,内存映射文件会变得更加复杂。如果我们将一个文件映射到特定的地址,然后在其中间插入一些额外的字节,那么这对于内存映射意味着什么?难道内存映射现在就要突然移动吗?如果是这样的话,不知程序是否能预料到?
其次,让我们考虑一下如果两个句柄同时打开同一个文件,其中一个在它的中间插入额外的字节会发生什么事情。假设进程A打开文件以供读取,进程B则以读取和写入方式打开同一个文件。进程A想要在进行几次读取时保存它的位置,因此编写了一些类似以下代码的代码:
DWORD DoAndThenRewind(HANDLE hFile, FARPROC fp){
   DWORD result;
   LARGEINTEGER zero = { 0 };
   LARGEINTEGER li;
   SetFilePointer(hFile, zero, &li, FILE_CURRENT);

   result = fp();

   SetFilePointer(hFile, &li, &li, FILE_BEGIN);
   return result;
}

现在,如果进程B想要在文件中插入一些额外的字节会发生什么?如果我们在进程A当前所在位置之后添加字节,则一切正常——文件指针(即从文件开头开始的线性地址)在添加字节前后保持不变,一切都很好。
但是,如果我们在进程A所在位置之前添加额外的字节,那么我们捕获的文件指针就会失去对齐,出现问题。
换句话说,将字节添加到文件中间意味着我们需要发明更聪明的方法来描述我们在文件中的位置,以便进行倒带,因为文件不再是逻辑上连续的字节选择。
因此,我们已经讨论了为什么Windows公开这种功能可能是一个坏主意;但这并没有真正回答“它是否可能”。答案仍然是否定的。不可能。
为什么?因为没有向用户模式程序公开此类功能。作为用户模式程序,您有一种机制可以获取文件句柄(NtCreateFile / NtOpenFile),可以通过NtReadFile / NtWriteFile读写它,可以通过NtSetFileInformation将其定位和重命名并删除它,并且可以通过NtClose释放句柄引用。
即使从内核模式,您也没有更多的选择。文件系统API被抽象化,文件系统将文件视为逻辑上连续的字节集合,而不是字节范围的链接列表或任何使得容易公开一种方法让您在文件中间插入非覆盖字节的东西。
这并不是说它不可能。正如其他人所提到的,您可以打开磁盘本身,假装是NTFS并直接更改分配给特定FCB的磁盘簇。但是这样做是勇敢的。 NTFS几乎没有文档,很复杂,易于发生变化,即使在未被操作系统挂载时也很难修改,更不用说在被操作系统挂载时了。
所以,恐怕答案是否定的。无法通过正常安全的Windows机制将额外的字节作为插入而不是覆盖操作添加到文件的中间。
相反,考虑查看您的问题,以确定是否适合将文件分成较小的文件并具有索引文件。这样,您就可以修改索引文件以插入额外的块。通过打破数据需要驻留在一个文件中的依赖关系,您将更容易避免文件系统要求文件是逻辑上连续的字节集合的要求。然后,您将能够修改索引文件以向您的“伪文件”添加额外的块,而无需将整个伪文件读入内存。

我的需求不会改变;我仍然需要一种将聚类插入文件中心的方法,你已经说明这是可能的。我已经知道了注意事项,所以如果你仍然想参与这个问题,请把精力集中在如何实现上,而不是为什么不能实现。谢谢。 - Robert Harvey
顺便说一下,这不是我在进行负投票。 - Robert Harvey
@RobertHarvey:我并没有说通过Windows可以在文件中间添加集群。如果你真的想要,可以通过共挂载NTFS来编写自己的文件系统驱动程序。但是当你这样做时,你并不是通过Windows来进行操作的(尽管磁盘碎片整理器是由编写文件系统的同一组人员编写的)。从Windows内部向文件中间插入额外字节是不可能的,因为这样做违反了Windows文件系统抽象的文件是逻辑连续的字节集合的概念。 - SecurityMatt
@RobertHarvey:此外,我的最后一段告诉你如何重新构造问题,以使你能够做出等效的操作;也就是说,为了突破Windows文件系统的限制,你需要减少对它的依赖。如果将你的文件变成一系列带有索引文件和一系列“叶子”文件,那么插入额外字节就变得非常容易,只需添加一个新的叶子并更新索引即可,无需内核模式驱动程序、解析NTFS或者与无法在Windows文件系统上执行的事实作斗争。 - SecurityMatt
+1 这个回答似乎与(或多或少)“被接受”的答案(通过赏金奖励)相符。不理解为什么会有负评。 - JDB

2
抽象问题,抽象答案:
在FAT文件系统中肯定可以做到这一点,而且可能在大多数其他文件系统中也可以。你实际上是在分散文件,而不是更常见的碎片整理过程。
FAT是通过约束指针组织的,这些指针产生存储数据的簇号链,第一个链接索引存储在文件记录中,第二个链接存储在分配表中的索引[第一个链接的编号]等等。可以在链中的任何位置插入另一个链接,只要插入的数据在簇边界结束即可。
很可能你会在C语言中找到一个开源库,这样做会更容易得多。虽然在C#中使用PInvoke可能也行,但你不会找到任何好的示例代码来帮助你入门。
我猜你对文件格式没有任何控制(视频文件?),如果你有控制,最好设计你的数据存储以避免出现这个问题。

2
这不是一个故障排除问题。我不是在询问如何修复错误的代码。 - Robert Harvey
@RobertHarvey:没错!你在问它是否能够完成:是的,它可以。 - Sten Petrov
3
如果你知道如何操作,现在是发布详细示例的时候了。 - Robert Harvey
你能分享更多关于文件的信息或者你尝试做什么吗?你有尝试使用现成的CMS吗? - Sten Petrov
@StenPetrov:你是在谈论一种晦涩的现成CMS类型,对吗? - Ry-
显示剩余10条评论

2
您不需要(也可能无法)修改文件访问表。您可以使用过滤器驱动程序或可堆叠的FS来实现相同的功能。让我们考虑一个4K的簇大小。我只是为了解释原因而写出设计。
1. 创建新文件时,将在标题中创建文件的布局映射。标题将提到条目数和条目列表。标题的大小将与群集的大小相同。为简单起见,让标题具有固定大小和4K条目。例如,假设有一个20KB的文件,标题可能会提到:[DWORD:5][DWORD:1][DWORD:2][DWORD:3][DWORD:4][DWORD:5]。此文件目前没有插入。
2. 假设有人在第3个扇区后插入了一个簇。您可以将其添加到文件末尾,并更改布局映射为:[5][1][2][3][5][6][4]
3. 假设有人需要查找第4个簇。您将需要访问布局映射并计算偏移量,然后查找它。它将在前5个簇之后开始,因此将从16K开始。
4. 假设有人顺序读取或写入文件。读取和写入将以相同的方式映射。
5. 假设标题只剩下一个条目:我们需要通过在文件末尾使用与上面其他指针相同的格式指向新群集来扩展它。要知道我们有多个群集,我们只需要查看项目数并计算所需的群集数。
您可以使用Windows上的过滤器驱动程序或Linux上的可堆叠文件系统(LKM)来实现上述所有内容。实现基本级别的功能在研究生小型项目的难度水平上。使其成为商业文件系统可能非常具有挑战性,特别是因为您不希望影响IO速度。
请注意,上述过滤器不会受到磁盘布局/碎片整理等任何更改的影响。如果您认为有用,您还可以对自己的文件进行碎片整理。

1
只要生成的文件可以在任何不包含您假设的过滤器/驱动程序或可堆叠FS的系统上无损读取,并且我不必第一次完全读取文件进行转换,那么这一切都很好、美好和精彩,因为这将使我从所述的过滤器-驱动程序或可堆叠FS中获得的任何好处变得无意义。 - Robert Harvey
使其在所有系统上运行是微不足道的。它只取决于过滤驱动程序的顺序。当您在另一个系统上查看文件时,您将a)将文件复制到临时存储(USB 等)中并稍后打开它或b)在网络上挂载现有分区。对于(a),无需进行任何更改,而对于(b),必须在此过滤器上设置CIFS/SMB/NFS。但是,在现有文件上使其工作并不是微不足道的,但如果您在 Windows 上使用流,则可以完成。头可以分配给该流,并且未来的添加也可以进入该流中。 - user1952500
顺便提一下,修改属性表/MFT仅适用于NTFS,并且如果将文件移动到FAT分区/ext3等,则会出现相同的问题。 - user1952500
我无法控制这个文件将被用于哪些系统。 - Robert Harvey
我的意思是,无论你使用什么机制,如果你想在所有文件系统上运行它,你都会遇到麻烦。Windows也支持ext3,NTFS也支持压缩文件。 - user1952500
显示剩余2条评论

1

你是否知道,在非对齐的位置插入非对齐数据几乎是99.99%不可能的?(也许可以使用基于压缩的一些黑客技巧。)我认为你知道。

“最简单”的解决方案是创建稀疏运行记录,然后在稀疏范围上进行写入。

  1. 对NTFS缓存进行操作。最好在脱机/卸载驱动器上执行操作。
  2. 获取文件记录(@JerKimball的答案听起来很有帮助,但没有详细说明)。如果文件被属性淹没并且它们被存储,则可能会出现问题。
  3. 进入文件的数据运行列表。数据运行的概念和格式在此处描述(http://inform.pucp.edu.pe/~inf232/Ntfs/ntfs_doc_v0.5/concepts/data_runs.html),其他NTFS格式数据可以在相邻页面上看到。
  4. 迭代数据运行,累加文件长度,以找到正确的插入点。
  5. 你很可能会发现你的插入点在运行的中间。你需要分裂运行,这并不难。(现在只需存储两个结果运行。)
  6. 创建一个稀疏运行记录非常容易。它只是运行长度(以簇为单位),前面加上字节,该字节包含其低4位中的长度字节大小(高4位应为零,表示空闲运行)。
  7. 现在您需要计算要插入到数据运行列表中的附加字节数,为它们腾出位置并进行插入/替换。
  8. 然后您需要修复文件大小属性,使其与运行一致。
  9. 最后,您可以挂载驱动器并将插入的信息写入空闲区域。

1
这一切都取决于原始问题是什么,也就是你要实现什么。修改FAT / NTFS表不是问题,而是解决问题的方法--可能是优雅和高效的解决方案,但更有可能是非常危险和不合适的。你提到你无法控制用户系统的使用情况,因此至少对于其中一些用户,管理员会反对黑客入侵文件系统内部。

无论如何,让我们回到问题本身。在给定不完整的信息下,可以想象出几种使用情况,并且解决方案将根据使用情况而易或难。

如果您知道在编辑后该文件一段时间内不会被需要,那么在半秒钟内保存编辑很容易--只需关闭窗口,让应用程序在后台完成保存,即使需要半个小时。我知道这听起来很愚蠢,但这是一个频繁使用的用例--一旦您完成对文件的编辑,保存它,关闭程序,您就不再需要那个文件了很长一段时间。
除非你需要。也许用户决定再编辑一些内容,或者另一个用户出现了。在这两种情况下,您的应用程序可以轻松检测到文件正在保存到硬盘中(例如,在主文件保存时可能会有一个隐藏的保护文件)。在这种情况下,您将按原样打开文件(部分保存),但向用户呈现文件的定制视图,使其看起来像文件处于最终状态。毕竟,您拥有关于哪些文件块必须移动到哪里的所有信息。
除非用户需要立即在另一个编辑器中打开文件(这不是很常见的情况,尤其是对于非常专业的文件格式,但是谁知道)。如果是这样,请问您是否可以访问该其他编辑器的源代码?或者您能否与该其他编辑器的开发人员交谈,并说服他们将未完全保存的文件视为处于最终状态(这并不难--只需从保护文件中读取偏移信息)。我想象一下,那个其他编辑器的开发人员同样对长时间保存感到沮丧,并且很乐意采用您的解决方案,因为这将有助于他们的产品。
我们还能有什么?也许用户想立即将文件复制或移动到其他地方。微软可能不会为了你的利益而改变Windows Explorer。在这种情况下,您需要实现UMDF驱动程序,或者干脆禁止用户这样做(例如,重命名原始文件并隐藏它,在其位置留下一个空白占位符;当用户尝试复制文件时,至少他会知道出了问题)。
另一个可能性,不太适合以上1-4层次结构,是如果您事先知道将要编辑哪些文件。在这种情况下,您可以“预分散”文件,在文件的体积中均匀插入随机间隙。这是由于您提到的文件格式的特殊性质:可能存在没有数据的间隙,只要链接正确指向以下下一个数据块即可。如果您知道将要编辑哪些文件(这不是不合理的假设--您的硬盘上有多少10Gb文件?),您可以在用户开始编辑之前(比如前一晚)“膨胀”文件,然后只需在需要插入新数据时移动这些较小的数据块。当然,这也依赖于您不必插入太多的假设。
无论如何,根据你的用户实际想要的情况,总会有不止一个答案。但我的建议来自于设计师的角度,而非程序员的角度。

0

还有一种可能性。

创建一个用户模式文件系统,例如使用FUSE或Dokan,并设计为容纳单个文件。从那里开始,您可以使用任何解决方案,涉及将多个文件的片段连接在一起,以使其看起来像是一个单独的大文件。

然后创建一个指向该文件的符号链接。


-2

编辑 - 另一种方法 - 为什么不考虑在 Mac 上完成这个任务?它们具有卓越的编辑能力和自动化能力!

编辑 - 最初的规格说明文件被频繁修改,实际上只修改了一次。建议像其他人指出的那样在后台执行操作:复制到新文件,删除旧文件,将新文件重命名为旧文件。

我建议放弃这种方法。你需要的是一个数据库。/YR


1
一个数据库如何帮助? - Robert Harvey
您需要频繁修改的大量信息。如果信息是结构化的,即使是不透明的二进制块,您也可以修改其中的任何部分,而无需考虑文件结构。像这样简单的事情: - Yimin Rong
我正在查看一个很大的二进制文件,其中文件开头的小部分需要进行一次修订。将其放入数据库中需要读取整个文件,这正是我想避免的。 - Robert Harvey
好的,那不是很清楚。建议在后台执行完整的复制操作,删除旧文件,然后将新文件重命名为旧文件。这是最安全的方法,而且即使在其他操作系统上也可以使用。 - Yimin Rong
是的。你想要的和你需要的是不同的。你想要的对你和其他应用程序来说都很危险,而且即使在不同的硬盘或 MS Windows 操作系统版本中也不可移植。被接受的“解决方案”可能只能工作五分钟。我从事安全工作,你最终需要的代码将引起许多病毒扫描器的警报。真的,重新考虑一下方法,这将为你节省很多麻烦。 - Yimin Rong
然后提供一个符合目标的替代方案。数据库不是它。很难相信在2013年,没有办法在不重写整个文件的情况下将数据插入到大文件的中间。 - Robert Harvey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接