[NFI] Add a first skeleton of what NFI could be.
authorPierre Schweitzer <pierre@reactos.org>
Sat, 20 Jan 2018 13:52:39 +0000 (14:52 +0100)
committerPierre Schweitzer <pierre@reactos.org>
Sat, 20 Jan 2018 13:54:00 +0000 (14:54 +0100)
All it will do for now is dumping the MFT / Files and their attributes.
It lacks many features, could be really improved and is bugged.

Note that logical sectors dump seems to be broken. MS NFI and this one don't agree on values.

Developed against W2K3.

modules/rosapps/applications/rosinternals/CMakeLists.txt
modules/rosapps/applications/rosinternals/nfi/CMakeLists.txt [new file with mode: 0644]
modules/rosapps/applications/rosinternals/nfi/nfi.c [new file with mode: 0644]
modules/rosapps/applications/rosinternals/nfi/nfi.rc [new file with mode: 0644]

index 1932f92..298b745 100644 (file)
@@ -1,3 +1,4 @@
 add_subdirectory(movefile)
+add_subdirectory(nfi)
 add_subdirectory(ntfsinfo)
 add_subdirectory(pendmoves)
diff --git a/modules/rosapps/applications/rosinternals/nfi/CMakeLists.txt b/modules/rosapps/applications/rosinternals/nfi/CMakeLists.txt
new file mode 100644 (file)
index 0000000..9fa5aec
--- /dev/null
@@ -0,0 +1,5 @@
+list(APPEND SOURCE nfi.c nfi.rc)
+add_executable(nfi ${SOURCE})
+set_module_type(nfi win32cui UNICODE)
+add_importlibs(nfi msvcrt kernel32 ntdll)
+add_cd_file(TARGET nfi DESTINATION reactos/system32 FOR all)
diff --git a/modules/rosapps/applications/rosinternals/nfi/nfi.c b/modules/rosapps/applications/rosinternals/nfi/nfi.c
new file mode 100644 (file)
index 0000000..b7b2879
--- /dev/null
@@ -0,0 +1,589 @@
+/*
+ * COPYRIGHT:       See COPYING in the top level directory
+ * PROJECT:         ReactOS NTFS Information tool
+ * FILE:            rosinternals/nfi/nfi.c
+ * PURPOSE:         Query information from NTFS volume using FSCTL
+ * PROGRAMMERS:     Pierre Schweitzer <pierre@reactos.org>
+ */
+
+#include <windows.h>
+#include <tchar.h>
+#include <stdio.h>
+
+typedef struct
+{
+    ULONG Type;
+    USHORT UsaOffset;
+    USHORT UsaCount;
+    ULONGLONG Lsn;
+} NTFS_RECORD_HEADER, *PNTFS_RECORD_HEADER;
+
+#define NRH_FILE_TYPE  0x454C4946
+
+typedef enum
+{
+    AttributeStandardInformation = 0x10,
+    AttributeAttributeList = 0x20,
+    AttributeFileName = 0x30,
+    AttributeObjectId = 0x40,
+    AttributeSecurityDescriptor = 0x50,
+    AttributeVolumeName = 0x60,
+    AttributeVolumeInformation = 0x70,
+    AttributeData = 0x80,
+    AttributeIndexRoot = 0x90,
+    AttributeIndexAllocation = 0xA0,
+    AttributeBitmap = 0xB0,
+    AttributeReparsePoint = 0xC0,
+    AttributeEAInformation = 0xD0,
+    AttributeEA = 0xE0,
+    AttributePropertySet = 0xF0,
+    AttributeLoggedUtilityStream = 0x100,
+    AttributeEnd = 0xFFFFFFFF
+} ATTRIBUTE_TYPE, *PATTRIBUTE_TYPE;
+
+typedef struct _FILE_RECORD_HEADER
+{
+    NTFS_RECORD_HEADER Ntfs;
+    USHORT SequenceNumber;
+    USHORT LinkCount;
+    USHORT AttributeOffset;
+    USHORT Flags;
+    ULONG BytesInUse;
+    ULONG BytesAllocated;
+    ULONGLONG BaseFileRecord;
+    USHORT NextAttributeNumber;
+} FILE_RECORD_HEADER, *PFILE_RECORD_HEADER;
+
+typedef struct
+{
+    ULONG Type;
+    ULONG Length;
+    UCHAR IsNonResident;
+    UCHAR NameLength;
+    USHORT NameOffset;
+    USHORT Flags;
+    USHORT Instance;
+    union
+    {
+        struct
+        {
+            ULONG ValueLength;
+            USHORT ValueOffset;
+            UCHAR Flags;
+            UCHAR Reserved;
+        } Resident;
+        struct
+        {
+            ULONGLONG LowestVCN;
+            ULONGLONG HighestVCN;
+            USHORT MappingPairsOffset;
+            USHORT CompressionUnit;
+            UCHAR Reserved[4];
+            LONGLONG AllocatedSize;
+            LONGLONG DataSize;
+            LONGLONG InitializedSize;
+            LONGLONG CompressedSize;
+        } NonResident;
+    };
+} NTFS_ATTR_RECORD, *PNTFS_ATTR_RECORD;
+
+typedef struct
+{
+    ULONGLONG DirectoryFileReferenceNumber;
+    ULONGLONG CreationTime;
+    ULONGLONG ChangeTime;
+    ULONGLONG LastWriteTime;
+    ULONGLONG LastAccessTime;
+    ULONGLONG AllocatedSize;
+    ULONGLONG DataSize;
+    ULONG FileAttributes;
+    union
+    {
+        struct
+        {
+            USHORT PackedEaSize;
+            USHORT AlignmentOrReserved;
+        } EaInfo;
+        ULONG ReparseTag;
+    } Extended;
+    UCHAR NameLength;
+    UCHAR NameType;
+    WCHAR Name[1];
+} FILENAME_ATTRIBUTE, *PFILENAME_ATTRIBUTE;
+
+#define NTFS_FILE_NAME_POSIX 0
+#define NTFS_FILE_NAME_WIN32 1
+#define NTFS_FILE_NAME_DOS 2
+#define NTFS_FILE_NAME_WIN32_AND_DOS 3
+
+#define NTFS_FILE_MFT 0
+#define NTFS_FILE_MFTMIRR 1
+#define NTFS_FILE_LOGFILE 2
+#define NTFS_FILE_VOLUME 3
+#define NTFS_FILE_ATTRDEF 4
+#define NTFS_FILE_ROOT 5
+#define NTFS_FILE_BITMAP 6
+#define NTFS_FILE_BOOT 7
+#define NTFS_FILE_BADCLUS 8
+#define NTFS_FILE_QUOTA 9
+#define NTFS_FILE_UPCASE 10
+#define NTFS_FILE_EXTEND 11
+
+PWSTR KnownEntries[NTFS_FILE_EXTEND + 1] =
+{
+    _T("Master File Table ($Mft)"),
+    _T("Master File Table Mirror ($MftMirr)"),
+    _T("Log File ($LogFile)"),
+    _T("DASD ($Volume)"),
+    _T("Attribute Definition Table ($AttrDef)"),
+    _T("Root Directory"),
+    _T("Volume Bitmap ($BitMap)"),
+    _T("Boot Sectors ($Boot)"),
+    _T("Bad Cluster List ($BadClus)"),
+    _T("Security ($Secure)"),
+    _T("Upcase Table ($UpCase)"),
+    _T("Extend Table ($Extend)")
+};
+
+#define NTFS_MFT_MASK 0x0000FFFFFFFFFFFFULL
+
+#define NTFS_FILE_TYPE_DIRECTORY  0x10000000
+
+typedef struct _NAME_CACHE_ENTRY
+{
+    struct _NAME_CACHE_ENTRY * Next;
+    ULONGLONG MftId;
+    ULONG NameLen;
+    WCHAR Name[1];
+} NAME_CACHE_ENTRY, *PNAME_CACHE_ENTRY;
+
+PNAME_CACHE_ENTRY CacheHead = NULL;
+
+void PrintUsage(void)
+{
+    /* FIXME */
+}
+
+void PrintPrettyName(PNTFS_ATTR_RECORD Attributes, PNTFS_ATTR_RECORD AttributesEnd, ULONGLONG MftId)
+{
+    BOOLEAN FirstRun, Found;
+    PNTFS_ATTR_RECORD Attribute;
+
+    FirstRun = TRUE;
+    Found = FALSE;
+
+    /* Setup name for "standard" files */
+    if (MftId <= NTFS_FILE_EXTEND)
+    {
+        _tprintf(_T("%s\n"), KnownEntries[MftId]);
+        return;
+    }
+
+    /* We'll first try to use the Win32 name
+     * If we fail finding it, we'll loop again for any other name
+     */
+TryAgain:
+    /* Loop all the attributes */
+    Attribute = Attributes;
+    while (Attribute < AttributesEnd && Attribute->Type != AttributeEnd)
+    {
+        WCHAR Display[MAX_PATH];
+        PFILENAME_ATTRIBUTE Name;
+        ULONGLONG ParentId;
+        ULONG Length;
+
+        /* Move to the next arg if:
+         * - Not a file name
+         * - Not resident (should never happen!)
+         */
+        if (Attribute->Type != AttributeFileName || Attribute->IsNonResident)
+        {
+            Attribute = (PNTFS_ATTR_RECORD)((ULONG_PTR)Attribute + Attribute->Length);
+            continue;
+        }
+
+        /* Get the attribute data */
+        Name = (PFILENAME_ATTRIBUTE)((ULONG_PTR)Attribute + Attribute->Resident.ValueOffset);
+        /* If not Win32, only accept if it wasn't the first run */
+        if ((Name->NameType == NTFS_FILE_NAME_POSIX || Name->NameType == NTFS_FILE_NAME_DOS) && FirstRun)
+        {
+            Attribute = (PNTFS_ATTR_RECORD)((ULONG_PTR)Attribute + Attribute->Length);
+            continue;
+        }
+
+        /* We accepted that name, get the parent ID to setup name */
+        ParentId = Name->DirectoryFileReferenceNumber & NTFS_MFT_MASK;
+        /* If root, easy, just print \ */
+        if (ParentId == NTFS_FILE_ROOT)
+        {
+            Display[0] = L'\\';
+            CopyMemory(&Display[1], Name->Name, Name->NameLength * sizeof(WCHAR));
+            Length = Name->NameLength + 1;
+            Display[Length] = UNICODE_NULL;
+        }
+        /* Specific case for $Extend\ files
+         * FIXME: Should be made more generic?
+         */
+        else if (ParentId == NTFS_FILE_EXTEND)
+        {
+            Display[0] = L'\\';
+            Length = wcslen(L"$Extend");
+            CopyMemory(&Display[1], L"$Extend", Length * sizeof(WCHAR));
+            ++Length;
+            Display[Length] = L'\\';
+            ++Length;
+            CopyMemory(Display + Length, Name->Name, Name->NameLength * sizeof(WCHAR));
+            Length += Name->NameLength;
+            Display[Length] = UNICODE_NULL;
+        }
+        /* Default case */
+        else
+        {
+            PNAME_CACHE_ENTRY CacheEntry;
+
+            /* Did we already cache the name? */
+            for (CacheEntry = CacheHead; CacheEntry != NULL; CacheEntry = CacheEntry->Next)
+            {
+                if (ParentId == CacheEntry->MftId)
+                {
+                    break;
+                }
+            }
+
+            /* Nothing written yet */
+            Length = 0;
+            /* We cached it */
+            if (CacheEntry != NULL)
+            {
+                /* Set up name. The cache entry contains full path */
+                Length = CacheEntry->NameLen / sizeof(WCHAR);
+                CopyMemory(Display, CacheEntry->Name, CacheEntry->NameLen);
+                Display[Length] = L'\\';
+                ++Length;
+            }
+            else
+            {
+                /* FIXME: Do something, like trying to read parent... */
+                _tprintf(_T("Parent: %I64d\n"), ParentId);
+            }
+
+            /* Copy our name */
+            CopyMemory(Display + Length, Name->Name, Name->NameLength * sizeof(WCHAR));
+            Length += Name->NameLength;
+            Display[Length] = UNICODE_NULL;
+        }
+
+        /* Display the name */
+        _tprintf(_T("%s\n"), Display);
+
+        /* If that's a directory, put it in the cache */
+        if (Name->FileAttributes & NTFS_FILE_TYPE_DIRECTORY)
+        {
+            PNAME_CACHE_ENTRY CacheEntry;
+
+            /* Allocate an entry big enough to store name and cache info */
+            CacheEntry = HeapAlloc(GetProcessHeap(), 0, sizeof(NAME_CACHE_ENTRY) + Length * sizeof(WCHAR));
+            if (CacheEntry != NULL)
+            {
+                /* Insert in head (likely more perf) */
+                CacheEntry->Next = CacheHead;
+                CacheHead = CacheEntry;
+                /* Set up entry with full path */
+                CacheEntry->MftId = MftId;
+                CacheEntry->NameLen = Length * sizeof(WCHAR);
+                CopyMemory(CacheEntry->Name, Display, Length * sizeof(WCHAR));
+            }
+        }
+
+        /* Now, just quit */
+        FirstRun = FALSE;
+        Found = TRUE;
+
+        break;
+    }
+
+    /* If was first run (Win32 search), retry with other names */
+    if (FirstRun)
+    {
+        FirstRun = FALSE;
+        goto TryAgain;
+    }
+
+    /* If we couldn't find a name, print unknown */
+    if (!Found)
+    {
+        _tprintf(_T("(unknown/unnamed)\n"));
+    }
+}
+
+PUCHAR DecodeRun(PUCHAR DataRun, LONGLONG *DataRunOffset, ULONGLONG *DataRunLength)
+{
+    UCHAR DataRunOffsetSize;
+    UCHAR DataRunLengthSize;
+    CHAR i;
+
+    /* Get the offset size (in bytes) of the run */
+    DataRunOffsetSize = (*DataRun >> 4) & 0xF;
+    /* Get the length size (in bytes) of the run */
+    DataRunLengthSize = *DataRun & 0xF;
+
+    /* Initialize values */
+    *DataRunOffset = 0;
+    *DataRunLength = 0;
+
+    /* Move to next byte */
+    DataRun++;
+
+    /* First, extract (byte after byte) run length with the size extracted from header */
+    for (i = 0; i < DataRunLengthSize; i++)
+    {
+        *DataRunLength += ((ULONG64)*DataRun) << (i * 8);
+        /* Next byte */
+        DataRun++;
+    }
+
+    /* If offset size is 0, return -1 to show that's sparse run */
+    if (DataRunOffsetSize == 0)
+    {
+        *DataRunOffset = -1;
+    }
+    /* Otherwise, extract offset */
+    else
+    {
+        /* Extract (byte after byte) run offset with the size extracted from header */
+        for (i = 0; i < DataRunOffsetSize - 1; i++)
+        {
+            *DataRunOffset += ((ULONG64)*DataRun) << (i * 8);
+            /* Next byte */
+            DataRun++;
+        }
+        /* The last byte contains sign so we must process it different way. */
+        *DataRunOffset = ((LONG64)(CHAR)(*(DataRun++)) << (i * 8)) + *DataRunOffset;
+    }
+
+    /* Return next run */
+    return DataRun;
+}
+
+void PrintAttributeInfo(PNTFS_ATTR_RECORD Attribute, DWORD MaxSize)
+{
+    BOOL Known = TRUE;
+    WCHAR AttributeName[0xFF + 3];
+
+    /* First of all, try to get attribute name */
+    if (Attribute->NameLength != 0 && Attribute->NameOffset < MaxSize && Attribute->NameOffset + Attribute->NameLength < MaxSize)
+    {
+        AttributeName[0] = L' ';
+        CopyMemory(AttributeName + 1, (PUCHAR)((ULONG_PTR)Attribute + Attribute->NameOffset), Attribute->NameLength * sizeof(WCHAR));
+        AttributeName[Attribute->NameLength + 1] = L' ';
+        AttributeName[Attribute->NameLength + 2] = UNICODE_NULL;
+    }
+    else
+    {
+        AttributeName[0] = L' ';
+        AttributeName[1] = UNICODE_NULL;
+    }
+
+    /* Display attribute type, its name (if any) and whether it's resident */
+    switch (Attribute->Type)
+    {
+        case AttributeFileName:
+            _tprintf(_T("\t$FILE_NAME%s(%s)\n"), AttributeName, (Attribute->IsNonResident ? _T("nonresident") : _T("resident")));
+            break;
+
+        case AttributeStandardInformation:
+            _tprintf(_T("\t$STANDARD_INFORMATION%s(%s)\n"), AttributeName, (Attribute->IsNonResident ? _T("nonresident") : _T("resident")));
+            break;
+
+        case AttributeData:
+            _tprintf(_T("\t$DATA%s(%s)\n"), AttributeName, (Attribute->IsNonResident ? _T("nonresident") : _T("resident")));
+            break;
+
+        case AttributeBitmap:
+            _tprintf(_T("\t$BITMAP%s(%s)\n"), AttributeName, (Attribute->IsNonResident ? _T("nonresident") : _T("resident")));
+            break;
+
+        case AttributeIndexRoot:
+            _tprintf(_T("\t$INDEX_ROOT%s(%s)\n"), AttributeName, (Attribute->IsNonResident ? _T("nonresident") : _T("resident")));
+            break;
+
+        case AttributeIndexAllocation:
+            _tprintf(_T("\t$INDEX_ALLOCATION%s(%s)\n"), AttributeName, (Attribute->IsNonResident ? _T("nonresident") : _T("resident")));
+            break;
+
+        case AttributeObjectId:
+            _tprintf(_T("\t$OBJECT_ID%s(%s)\n"), AttributeName, (Attribute->IsNonResident ? _T("nonresident") : _T("resident")));
+            break;
+
+        case AttributeSecurityDescriptor:
+            _tprintf(_T("\t$SECURITY_DESCRIPTOR%s(%s)\n"), AttributeName, (Attribute->IsNonResident ? _T("nonresident") : _T("resident")));
+            break;
+
+        case AttributeVolumeName:
+            _tprintf(_T("\t$VOLUME_NAME%s(%s)\n"), AttributeName, (Attribute->IsNonResident ? _T("nonresident") : _T("resident")));
+            break;
+
+        case AttributeVolumeInformation:
+            _tprintf(_T("\t$VOLUME_INFORMATION%s(%s)\n"), AttributeName, (Attribute->IsNonResident ? _T("nonresident") : _T("resident")));
+            break;
+
+        case AttributeAttributeList:
+            _tprintf(_T("\t$ATTRIBUTE_LIST%s(%s)\n"), AttributeName, (Attribute->IsNonResident ? _T("nonresident") : _T("resident")));
+            break;
+
+        default:
+            _tprintf(_T("\tUnknown (%x)%s(%s)\n"), Attribute->Type, AttributeName, (Attribute->IsNonResident ? _T("nonresident") : _T("resident")));
+            Known = FALSE;
+            break;
+    }
+
+    /* If attribute is non resident, display the logical sectors it covers */
+    if (Known && Attribute->IsNonResident)
+    {
+        PUCHAR Run;
+        ULONGLONG Offset = 0;
+
+        /* Get the runs mapping */
+        Run = (PUCHAR)((ULONG_PTR)Attribute + Attribute->NonResident.MappingPairsOffset);
+        /* As long as header isn't 0x00, then, there's a run */
+        while (*Run != 0)
+        {
+            LONGLONG CurrOffset;
+            ULONGLONG CurrLen;
+
+            /* Decode the run, and move to the next one */
+            Run = DecodeRun(Run, &CurrOffset, &CurrLen);
+
+            /* We don't print sparse runs */
+            if (CurrOffset != -1)
+            {
+                Offset += CurrOffset;
+                _tprintf(_T("\t\tlogical sectors %I64d-%I64d (0x%I64x-0x%I64x)\n"), Offset, Offset + CurrLen, Offset, Offset + CurrLen);
+            }
+        }
+    }
+}
+
+int
+__cdecl
+_tmain(int argc, const TCHAR *argv[])
+{
+    TCHAR VolumeName[] = _T("\\\\.\\C:");
+    HANDLE VolumeHandle;
+    NTFS_VOLUME_DATA_BUFFER VolumeInfo;
+    DWORD LengthReturned;
+    ULONGLONG File;
+    PNTFS_FILE_RECORD_OUTPUT_BUFFER OutputBuffer;
+    TCHAR Letter;
+    PNAME_CACHE_ENTRY CacheEntry;
+
+    if (argc == 1)
+    {
+        PrintUsage();
+        return 0;
+    }
+
+    /* Setup volume name */
+    Letter = argv[1][0];
+    if ((Letter >= 'A' && Letter <= 'Z') ||
+        (Letter >= 'a' && Letter <= 'z'))
+    {
+        VolumeName[4] = Letter;
+    }
+
+    /* Open volume */
+    VolumeHandle = CreateFile(VolumeName, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0 );
+    if (VolumeHandle == INVALID_HANDLE_VALUE)
+    {
+        _ftprintf(stderr, _T("Failed opening the volume '%s' (%lx)\n"), VolumeName, GetLastError());
+        return 1;
+    }
+
+    /* Get NTFS volume info */
+    if (!DeviceIoControl(VolumeHandle, FSCTL_GET_NTFS_VOLUME_DATA, NULL, 0, &VolumeInfo, sizeof(VolumeInfo), &LengthReturned, NULL))
+    {
+        _ftprintf(stderr, _T("Failed requesting volume '%s' data (%lx)\n"), VolumeName, GetLastError());
+        CloseHandle(VolumeHandle);
+        return 1;
+    }
+
+    /* Validate output */
+    if (LengthReturned < sizeof(VolumeInfo))
+    {
+        _ftprintf(stderr, _T("Failed reading volume '%s' data (%lx)\n"), VolumeName, GetLastError());
+        CloseHandle(VolumeHandle);
+        return 1;
+    }
+
+    /* Allocate a buffer big enough to hold a file record */
+    OutputBuffer = HeapAlloc(GetProcessHeap(), 0, VolumeInfo.BytesPerFileRecordSegment + sizeof(NTFS_FILE_RECORD_OUTPUT_BUFFER));
+    if (OutputBuffer == NULL)
+    {
+        _ftprintf(stderr, _T("Failed to allocate %x bytes\n"), VolumeInfo.BytesPerFileRecordSegment + sizeof(NTFS_FILE_RECORD_OUTPUT_BUFFER));
+        CloseHandle(VolumeHandle);
+        return 1;
+    }
+
+    /* Forever loop, extract all the files! */
+    for (File = 0;; ++File)
+    {
+        NTFS_FILE_RECORD_INPUT_BUFFER InputBuffer;
+        PFILE_RECORD_HEADER FileRecord;
+        PNTFS_ATTR_RECORD Attribute, AttributesEnd;
+
+        /* Get the file record */
+        InputBuffer.FileReferenceNumber.QuadPart = File;
+        if (!DeviceIoControl(VolumeHandle, FSCTL_GET_NTFS_FILE_RECORD, &InputBuffer, sizeof(InputBuffer),
+                             OutputBuffer, VolumeInfo.BytesPerFileRecordSegment  + sizeof(NTFS_FILE_RECORD_OUTPUT_BUFFER),
+                             &LengthReturned, NULL))
+        {
+            continue;
+        }
+
+        /* Don't deal with it if we already browsed it
+         * FSCTL_GET_NTFS_FILE_RECORD always returns previous record if demanded
+         * isn't allocated
+         */
+        if (OutputBuffer->FileReferenceNumber.QuadPart != File)
+        {
+            continue;
+        }
+
+        /* Sanity check */
+        FileRecord = (PFILE_RECORD_HEADER)OutputBuffer->FileRecordBuffer;
+        if (FileRecord->Ntfs.Type != NRH_FILE_TYPE)
+        {
+            continue;
+        }
+
+        /* Print ID */
+        _tprintf(_T("\nFile %I64d\n"), OutputBuffer->FileReferenceNumber.QuadPart);
+
+        /* Get attributes list */
+        Attribute = (PNTFS_ATTR_RECORD)((ULONG_PTR)FileRecord + FileRecord->AttributeOffset);
+        AttributesEnd = (PNTFS_ATTR_RECORD)((ULONG_PTR)FileRecord + FileRecord->BytesInUse);
+
+        /* Print the file name */
+        PrintPrettyName(Attribute, AttributesEnd, File);
+
+        /* And print attributes information for each attribute */
+        while (Attribute < AttributesEnd && Attribute->Type != AttributeEnd)
+        {
+            PrintAttributeInfo(Attribute, VolumeInfo.BytesPerFileRecordSegment);
+            Attribute = (PNTFS_ATTR_RECORD)((ULONG_PTR)Attribute + Attribute->Length);
+        }
+    }
+
+    /* Free memory! */
+    while (CacheHead != NULL)
+    {
+        CacheEntry = CacheHead;
+        CacheHead = CacheEntry->Next;
+        HeapFree(GetProcessHeap(), 0, CacheEntry);
+    }
+
+    /* Cleanup and exit */
+    HeapFree(GetProcessHeap(), 0, OutputBuffer);
+    CloseHandle(VolumeHandle);
+    return 0;
+}
diff --git a/modules/rosapps/applications/rosinternals/nfi/nfi.rc b/modules/rosapps/applications/rosinternals/nfi/nfi.rc
new file mode 100644 (file)
index 0000000..89e0d39
--- /dev/null
@@ -0,0 +1,4 @@
+#define REACTOS_STR_FILE_DESCRIPTION    "NTFS File sector Information\0"
+#define REACTOS_STR_INTERNAL_NAME       "nfi\0"
+#define REACTOS_STR_ORIGINAL_FILENAME   "nfi.exe\0"
+#include <reactos/version.rc>