// m_fc_ntfs.c : implementation of ntfs master file table (MFT) file carver.
//
// REQUIRE: FORENSIC SUB-SYSTEM INIT.
//
// The fs/ntfs module is responsible for displaying various file system files
// which are generated by carving the NTFS MFT.
//
// (c) Ulf Frisk, 2020-2025
// Author: Ulf Frisk, pcileech@frizk.net
//

#include "modules.h"
#include "../vmmwinobj.h"

#define M_NTFS_INFO_LINELENGTH_UTF8          104
#define M_NTFS_INFO_LINELENGTH_JSON          104

static LPSTR FC_SQL_SCHEMA_NTFS =
"DROP VIEW IF EXISTS v_ntfs; " \
"DROP TABLE IF EXISTS ntfs; " \
"CREATE TABLE ntfs ( id INTEGER PRIMARY KEY, id_parent INTEGER, id_str INTEGER, hash INTEGER, hash_parent INTEGER, addr_phys INTEGER, inode INTEGER, inode_parent INTEGER, flags INTEGER, size_file INTEGER, time_create INTEGER, time_modify INTEGER, time_read INTEGER, name_seq INTEGER, oln_u INTEGER, oln_j INTEGER );" \
"CREATE INDEX idx_ntfs_hash ON ntfs(hash); " \
"CREATE INDEX idx_ntfs_hash_parent ON ntfs(hash_parent); " \
"CREATE INDEX idx_ntfs_oln_u ON ntfs(oln_u); " \
"CREATE VIEW v_ntfs AS SELECT * FROM ntfs, str WHERE ntfs.id_str = str.id; ";

//-----------------------------------------------------------------------------
// NTFS MFT WINDOWS DEFINES AND TYPEDEFS BELOW:
//-----------------------------------------------------------------------------

typedef struct tdNTFS_REF {
    QWORD SegmentNumber : 48;
    QWORD SequenceNumber : 16;
} NTFS_REF, *PNTFS_REF;

typedef struct tdNTFS_FILE_RECORD {
    DWORD   Signature;                  // +000 : signature 'FILE'
    WORD    UpdateSequenceArrayOffset;  // +004 : (most common 0x30)
    WORD    UpdateSequenceArraySize;    // +006 : (most common 0x03)
    QWORD   LogFileSequenceNumber;      // +008
    WORD    SequenceNumber;             // +010
    WORD    HardLinkCount;              // +012
    WORD    FirstAttributeOffset;       // +014
    WORD    Flags;                      // +016 : 0x01 = inuse, 0x02 = directory
    DWORD   RealSize;                   // +018 : size on disk
    DWORD   AllocatedSize;              // +01c : size (QWORD aligned)
    NTFS_REF BaseFileRecordSegment;     // +020 : 0 if base record
    WORD    NextAttributeId;            // +028
    WORD    _Pad;                       // +02a
    DWORD   MftRecordNumber;            // +02c : mft record id
} NTFS_FILE_RECORD, *PNTFS_FILE_RECORD;

typedef struct tdNTFS_INDEX_RECORD {
    DWORD   Signature;                  // +000 : signature 'INDX'
    WORD    UpdateSequenceArrayOffset;  // +004 : (most common 0x28)
    WORD    UpdateSequenceArraySize;    // +006
    QWORD   LogFileSequenceNumber;      // +008
    QWORD   VCN;                        // +010 : virtual cluster number
    DWORD   IndexEntryFirstOffset;      // +018
    DWORD   IndexEntryTotalSize;        // +01c
    DWORD   AllocatedSize;              // +020
    BYTE    IndexEntryNonLeafFlag;      // +024
    BYTE    _Pad[3];                    // +025
} NTFS_INDEX_RECORD, *PNTFS_INDEX_RECORD;

typedef struct tdNTFS_INDEX_ENTRY {
    NTFS_REF FileReference;             // +000
    WORD    Length;                     // +008
    WORD    StreamLength;               // +00a
    BYTE    Flags;                      // +00c
    BYTE    _Pad[3];                    // +00d
    BYTE    Stream[0];                  // +010
} NTFS_INDEX_ENTRY, *PNTFS_INDEX_ENTRY;

#define NTFS_FILE_RECORD_FLAG_ACTIVE            0x0001
#define NTFS_FILE_RECORD_FLAG_DIRECTORY         0x0002

#define NTFS_ATTR_TYPE_STANDARD_INFORMATION     0x10
#define NTFS_ATTR_TYPE_ATTRIBUTE_LIST           0x20
#define NTFS_ATTR_TYPE_FILE_NAME                0x30
#define NTFS_ATTR_TYPE_OBJECT_ID                0x40
#define NTFS_ATTR_TYPE_SECURITY_DESCRIPTOR      0x50
#define NTFS_ATTR_TYPE_VOLUME_NAME              0x60
#define NTFS_ATTR_TYPE_VOLUME_INFORMATION       0x70
#define NTFS_ATTR_TYPE_DATA                     0x80
#define NTFS_ATTR_TYPE_INDEX_ROOT               0x90
#define NTFS_ATTR_TYPE_INDEX_ALLOCATION         0xA0
#define NTFS_ATTR_TYPE_BITMAP                   0xB0
#define NTFS_ATTR_TYPE_REPARSE_POINT            0xC0
#define NTFS_ATTR_TYPE_EA_INFORMATION           0xD0
#define NTFS_ATTR_TYPE_EA                       0xE0
#define NTFS_ATTR_TYPE_PROPERTY_SET             0xF0
#define NTFS_ATTR_TYPE_LOGGED_UTILITY_STREAM    0x100
#define NTFS_ATTR_TYPE_MAX                      0x100

static LPCSTR NTFS_ATTR_TYPE_NAME_STR[] = {
    "UNKNOWN",
    "STANDARD_INFORMATION",
    "ATTRIBUTE_LIST",
    "FILE_NAME",
    "OBJECT_ID",
    "SECURITY_DESCRIPTOR",
    "VOLUME_NAME",
    "VOLUME_INFORMATION",
    "DATA",
    "INDEX_ROOT",
    "INDEX_ALLOCATION",
    "BITMAP",
    "REPARSE_POINT",
    "REPARSE_POINT",
    "EA_INFORMATION",
    "EA",
    "PROPERTY_SET",
    "LOGGED_UTILITY_STREAM"
};

typedef struct tdNTFS_ATTR {
    DWORD Type;                         // +000
    DWORD Length;                       // +004
    BYTE fNonResident;                  // +008
    BYTE NameLength;                    // +009
    WORD NameOffset;                    // +00a
    WORD Flags;                         // +00c
    WORD AttrId;                        // +00e
    DWORD AttrLength;                   // +010
    WORD AttrOffset;                    // +014
    BYTE fIndexed;                      // +016
    BYTE _Pad;                          // +017
} NTFS_ATTR, *PNTFS_ATTR;

#define NTFS_STDINFO_PERMISSION_READONLY        0x0001
#define NTFS_STDINFO_PERMISSION_HIDDEN          0x0002
#define NTFS_STDINFO_PERMISSION_SYSTEM          0x0004
#define NTFS_STDINFO_PERMISSION_ARCHIVE         0x0020
#define NTFS_STDINFO_PERMISSION_DEVICE          0x0040
#define NTFS_STDINFO_PERMISSION_TEMPORARY       0x0100
#define NTFS_STDINFO_PERMISSION_SPARSE          0x0200
#define NTFS_STDINFO_PERMISSION_REPARSE         0x0400
#define NTFS_STDINFO_PERMISSION_COMPRESSED      0x0800
#define NTFS_STDINFO_PERMISSION_OFFLINE         0x1000
#define NTFS_STDINFO_PERMISSION_NOINDEX         0x2000
#define NTFS_STDINFO_PERMISSION_ENCRYPTED       0x4000

#define NTFS_STANDARD_INFORMATION_LEN_PREWIN2K  0x30

typedef struct tdNTFS_STANDARD_INFORMATION {
    QWORD TimeCreate;                   // +000
    QWORD TimeAlter;                    // +008
    QWORD TimeModify;                   // +010
    QWORD TimeRead;                     // +018
    DWORD DosFilePermissions;           // +020
    DWORD MaxVersions;                  // +024
    DWORD Version;                      // +028
    DWORD ClassId;                      // +02c
    DWORD OwnerId;                      // +030
    DWORD SecurityId;                   // +034
    QWORD QuotaCharged;                 // +038
    QWORD UpdateSequenceNumber;         // +040
} NTFS_STANDARD_INFORMATION, *PNTFS_STANDARD_INFORMATION;

typedef struct tdNTFS_OBJECT_ID {
    BYTE ObjectId[16];                  // +000
    BYTE BirthVolumeId[16];             // +010
    BYTE BirthObjectId[16];             // +020
    BYTE DomainId[16];                  // +030
} NTFS_OBJECT_ID, *PNTFS_OBJECT_ID;

#define NTFS_FILENAME_NAMESPACE_POSIX           0x00
#define NTFS_FILENAME_NAMESPACE_WIN32           0x01
#define NTFS_FILENAME_NAMESPACE_DOS             0x02
#define NTFS_FILENAME_NAMESPACE_WIN32DOS        0x03
#define NTFS_FILENAME_NAMESPACE_MAX             0x03

static LPCSTR NTFS_FILENAME_NAMESPACE_NAME_STR[] = {
    "POSIX",
    "WIN32",
    "DOS",
    "WIN32DOS"
};

typedef struct tdNTFS_FILE_NAME {
    NTFS_REF ParentDirectory;           // +000
    QWORD TimeCreate;                   // +008
    QWORD TimeAlter;                    // +010
    QWORD TimeModify;                   // +018
    QWORD TimeRead;                     // +020
    QWORD SizeAllocated;                // +028
    QWORD SizeReal;                     // +030
    DWORD Flags;                        // +038
    DWORD _Reserved;                    // +03c
    BYTE NameLength;                    // +040
    BYTE NameSpace;                     // +041
    WCHAR Name[0];                      // +042
} NTFS_FILE_NAME, *PNTFS_FILE_NAME;



//-----------------------------------------------------------------------------
// NTFS INTERNAL DEFINES AND TYPEDEFS BELOW:
//-----------------------------------------------------------------------------

#define FCNTFS2_FLAG_ACTIVE             0x0001
#define FCNTFS2_FLAG_DIRECTORY          0x0002
#define FCNTFS2_FLAG_RESIDENT           0x0004
#define FCNTFS2_FLAG_ADS                0x0008

#define FCNTFS2_FLAG_SOURCE_SYNTHETIC   0x0800
#define FCNTFS2_FLAG_SOURCE_PMEM_MFT    0x1000
#define FCNTFS2_FLAG_SOURCE_PMEM_DIR    0x2000
#define FCNTFS2_FLAG_SOURCE_FILE_MFT    0x4000
#define FCNTFS2_FLAG_SOURCE_FILE_DIR    0x8000

#define FCNTFS2_SYNTHETIC_NAME_BUFSIZE  0x10

typedef struct tdFCNTFS2 {
    union {
        QWORD qwKey;
        struct {
            DWORD dwMftRecordNumber;    // MFT record number.
            WORD _Filler1;
            WORD wRecordNumberIndex;    // multiple filesystems will lead to record# collisions.
        };
    };
    DWORD dwParentMftRecordNumber;
    WORD flags;
    WORD wVolumeId;                 // 0 = default/unknown, 1..n = device counter id.
    WORD wMftSequenceNumber;
    WORD wParentMftSequenceNumber;
    QWORD paRecord;
    QWORD ftCreate;
    QWORD ftModify;
    QWORD ftRead;
    QWORD cbFileSize;
    DWORD cChild;
    struct tdFCNTFS2 *pParent;          // parent entry
    struct tdFCNTFS2 *pChild;           // 1st child entry
    struct tdFCNTFS2 *pSibling;         // sibling entry list
    QWORD qwDbId;
    QWORD qwHashPath;
    DWORD dwHashName;
    WORD wName_SeqNbr;                  // collision counter for wszName (in same directory)
    WORD wDirDepth;                     // directory depth
    CHAR uszName[0];
} FCNTFS2, *PFCNTFS2;

typedef struct tdFCNTFS2_VOLUME {
    WORD wId;
    DWORD dwNextOrphan;
    QWORD vaDevice;
    QWORD cEntry;
    PFCNTFS2 pRoot;
    PFCNTFS2 pOrphan;
    BYTE uszVolumeName[MAX_PATH];
} FCNTFS2_VOLUME, *PFCNTFS2_VOLUME;

typedef struct tdOB_FCNTFS2_INIT_CONTEXT {
    OB ObHdr;
    VMMDLL_MODULE_ID MID;
    BOOL fLogTrace;
    POB_MAP pmMft;              // MFT record number + index (owns reference to PFCNTFS2)
    POB_MAP pmVolume;           // Volume ID (owns reference to PFCNTFS2_VOLUME)
    POB_COUNTER pcDuplicate;    // duplicate check
    WORD wNextVolumeId;
    PBYTE pb1M;
    DWORD cVolumes;
    PFCNTFS2_VOLUME pVolumes;
    SRWLOCK LockPhysIngestSRW;
} OB_FCNTFS2_INIT_CONTEXT, *POB_FCNTFS2_INIT_CONTEXT;

typedef struct tdFCNTFS2_FINALIZE_CONTEXT {
    sqlite3 *hSql;
    sqlite3_stmt *st;
    sqlite3_stmt *st_str;
    QWORD cbUtf8Total;
    QWORD cbJsonTotal;
    QWORD qwNextDbId;
} FCNTFS2_FINALIZE_CONTEXT, *PFCNTFS2_FINALIZE_CONTEXT;

#define FCNTFS2_DUPLICATE_CHECK_FILE_RECORD(dwMftRecordNumber, ftCreate, ftModify, ftRead)      (((QWORD)(dwMftRecordNumber) << 32) ^ (dwMftRecordNumber) ^ (ftCreate) ^ (ftModify) ^ (ftRead))

/*
* Push the pNt entry to the internal context (if possible).
*/
_Success_(return)
BOOL FcNtfs2_IngestPushEntry(_In_ VMM_HANDLE H, _In_ POB_FCNTFS2_INIT_CONTEXT ctx, _In_ PFCNTFS2 pNt, _In_opt_ PNTFS_FILE_NAME pfn)
{
    BOOL fResult = TRUE;
    QWORD qwKey;
    if((pNt->dwMftRecordNumber == 5) && !(pNt->flags & FCNTFS2_FLAG_SOURCE_SYNTHETIC)) { return FALSE; }
    if(!pNt->dwParentMftRecordNumber && !(pNt->flags & FCNTFS2_FLAG_SOURCE_SYNTHETIC)) { return FALSE; }
    while(!ObMap_Push(ctx->pmMft, pNt->qwKey, pNt)) {
        if(pNt->wRecordNumberIndex == 0xffff) {
            fResult = FALSE;
            break;
        }
        pNt->wRecordNumberIndex++;
    }
    if(fResult) {
        qwKey = FCNTFS2_DUPLICATE_CHECK_FILE_RECORD(pNt->dwMftRecordNumber, pNt->ftCreate, pNt->ftModify, pNt->ftRead);
        ObCounter_Set(ctx->pcDuplicate, qwKey, pNt->qwKey);
        if(pfn) {
            qwKey = FCNTFS2_DUPLICATE_CHECK_FILE_RECORD(pNt->dwMftRecordNumber, pfn->TimeCreate, pfn->TimeModify, pfn->TimeRead);
            ObCounter_Set(ctx->pcDuplicate, qwKey, pNt->qwKey);
        }
    }
    if(ctx->fLogTrace) {
        VmmLog(H, ctx->MID, LOGLEVEL_6_TRACE, "  %08x %08x %12llx %04x %2x%12llx %04x %c%c%c%c%c%c %s",
            pNt->dwMftRecordNumber,
            pNt->dwParentMftRecordNumber,
            pNt->paRecord,
            pNt->wRecordNumberIndex,
            pNt->wVolumeId,
            pNt->cbFileSize,
            pNt->flags,
            fResult ? ' ' : '!',
            (pNt->flags & FCNTFS2_FLAG_ACTIVE) ? 'A' : ' ',
            (pNt->flags & FCNTFS2_FLAG_DIRECTORY) ? 'D' : ' ',
            (pNt->flags & FCNTFS2_FLAG_ADS) ? 'S' : ' ',
            ((pNt->flags & FCNTFS2_FLAG_SOURCE_PMEM_DIR) || (pNt->flags & FCNTFS2_FLAG_SOURCE_PMEM_MFT)) ? 'P' : ' ',
            ((pNt->flags & FCNTFS2_FLAG_SOURCE_FILE_DIR) || (pNt->flags & FCNTFS2_FLAG_SOURCE_PMEM_DIR)) ? 'I' : ' ',
            pNt->uszName
        );
    }
    return fResult;
}

/*
* Push a synthetic directory to the internal context.
* This context must not be free'd by the caller - ctx->pmMft will take care of it.
*/
_Success_(return != 0)
PFCNTFS2 FcNtfs2_IngestPushEntrySynthetic(_In_ VMM_HANDLE H, _In_ POB_FCNTFS2_INIT_CONTEXT ctx, _In_ DWORD dwMftRecordNumber, _In_opt_ PFCNTFS2 pNtParent, _In_ WORD wVolumeId, _In_ DWORD dwId)
{
    PFCNTFS2 pNt;
    if(!(pNt = LocalAlloc(LMEM_ZEROINIT, sizeof(FCNTFS2) + FCNTFS2_SYNTHETIC_NAME_BUFSIZE))) { return NULL; }
    pNt->dwMftRecordNumber = dwMftRecordNumber;
    pNt->dwParentMftRecordNumber = pNtParent ? pNtParent->dwMftRecordNumber : 0;
    pNt->wVolumeId = wVolumeId;
    pNt->flags = FCNTFS2_FLAG_ACTIVE | FCNTFS2_FLAG_DIRECTORY | FCNTFS2_FLAG_SOURCE_SYNTHETIC;
    if(dwId == 0xffffffff) {
        _snprintf_s(pNt->uszName, FCNTFS2_SYNTHETIC_NAME_BUFSIZE, _TRUNCATE, "$_ORPHAN");
    } else {
        _snprintf_s(pNt->uszName, FCNTFS2_SYNTHETIC_NAME_BUFSIZE, _TRUNCATE, "%u", dwId);
    }
    if(FcNtfs2_IngestPushEntry(H, ctx, pNt, NULL)) {
        if(pNtParent) {
            pNt->pParent = pNtParent;
            pNt->pSibling = pNtParent->pChild;
            pNtParent->pChild = pNt;
            pNtParent->cChild++;
        }
        return pNt;
    } else {
        LocalFree(pNt);
        return NULL;
    }
}

/*
* Try ingest a Index Entry
*/
VOID FcNtfs2_IngestIndexEntry(_In_ VMM_HANDLE H, _In_ POB_FCNTFS2_INIT_CONTEXT ctx, _In_ PNTFS_INDEX_RECORD pIR, _In_ PNTFS_INDEX_ENTRY pIE, _In_ WORD wVolumeId, _In_ WORD wFlagsSource)
{
    DWORD cb, cbAttribute, cbuName = 0;
    PBYTE pb;
    QWORD qwKey;
    PFCNTFS2 pNt;
    PNTFS_FILE_NAME pfnC, pfn = NULL;
    cb = pIE->StreamLength;
    pb = pIE->Stream;
    // we don't know if this is actually a $FILE_NAME / $I30 entry so perform validity
    // checks on the data while iterating to find the file name entry.
    while(cb >= 0x42) {
        pfnC = (PNTFS_FILE_NAME)pb;
        if(0x03 != (pfnC->TimeCreate >> 56) + (pfnC->TimeModify >> 56) + (pfnC->TimeRead >> 56)) {
            return;
        }
        cbAttribute = 0x42 + sizeof(WCHAR) * pfnC->NameLength;
        if(cbAttribute > cb) {
            break;
        }
        if(pfnC->NameSpace != NTFS_FILENAME_NAMESPACE_DOS) {
            if(!pfn || (pfnC->SizeReal > pfn->SizeAllocated)) {
                pfn = pfnC;
            }
        }
        pb += cbAttribute;
        cb -= cbAttribute;
    }
    if(!pfn) {
        return;
    }
    // Verify validity:
    if(!pfn || (pfn->ParentDirectory.SegmentNumber > 0xfffffff0) || !pfn->NameLength) { return; }                               // verify
    // Duplicate check:
    qwKey = FCNTFS2_DUPLICATE_CHECK_FILE_RECORD(pIE->FileReference.SegmentNumber, pfn->TimeCreate, pfn->TimeModify, pfn->TimeRead);
    if(ObCounter_Exists(ctx->pcDuplicate, qwKey)) { return; }
    // Create NTFS object:
    if(!CharUtil_WtoU(pfn->Name, pfn->NameLength, NULL, 0, NULL, &cbuName, 0) || !cbuName) { return; }
    if(!(pNt = LocalAlloc(LMEM_ZEROINIT, sizeof(FCNTFS2) + cbuName))) { return; }
    if(!CharUtil_WtoU(pfn->Name, pfn->NameLength, pNt->uszName, cbuName, NULL, &cbuName, CHARUTIL_FLAG_STR_BUFONLY) || !cbuName) {
        LocalFree(pNt);
        return;
    }
    pNt->dwMftRecordNumber = (DWORD)pIE->FileReference.SegmentNumber;
    pNt->dwParentMftRecordNumber = (DWORD)pfn->ParentDirectory.SegmentNumber;
    pNt->wParentMftSequenceNumber = (WORD)pfn->ParentDirectory.SequenceNumber;
    pNt->flags = wFlagsSource | (pfn->Flags & 0x10000000 ? FCNTFS2_FLAG_DIRECTORY : 0);
    pNt->wVolumeId = wVolumeId;
    pNt->ftCreate = pfn->TimeCreate;
    pNt->ftModify = pfn->TimeModify;
    pNt->ftRead = pfn->TimeRead;
    pNt->cbFileSize = pfn->SizeReal;
    // Push NTFS object to "MAP":
    if(!FcNtfs2_IngestPushEntry(H, ctx, pNt, pfn)) {
        LocalFree(pNt);
        return;
    }
}

/*
* Try to ingest an 'INDX' page-sized directory index record.
* This could be either a $Directory from a _FILE_OBJECT or a physical memory page.
*/
VOID FcNtfs2_IngestIndexRecord(_In_ VMM_HANDLE H, _In_ POB_FCNTFS2_INIT_CONTEXT ctx, _In_ PNTFS_INDEX_RECORD pIR, _In_reads_(0x1000) PBYTE pb, _In_ WORD wVolumeId, _In_ WORD wFlagsSource)
{
    DWORD oNextIndexEntry, cbTotalIndexEntry;
    PNTFS_INDEX_ENTRY pIE;
    if(pIR->Signature != 'XDNI') { return; }
    if((0x18 + pIR->AllocatedSize > 0x1000) || (pIR->IndexEntryTotalSize > pIR->AllocatedSize)) {
        return;
    }
    oNextIndexEntry = 0x18 + pIR->IndexEntryFirstOffset;
    if(oNextIndexEntry > pIR->AllocatedSize) {
        return;
    }
    cbTotalIndexEntry = min(pIR->AllocatedSize - oNextIndexEntry, pIR->IndexEntryTotalSize);
    if(oNextIndexEntry < 0x2A) {
        return;
    }
    while(cbTotalIndexEntry >= sizeof(NTFS_INDEX_ENTRY)) {
        pIE = (PNTFS_INDEX_ENTRY)(pb + oNextIndexEntry);
        if(pIE->Length > cbTotalIndexEntry) {
            return;
        }
        if(sizeof(NTFS_INDEX_ENTRY) + pIE->StreamLength > pIE->Length) {
            return;
        }
        FcNtfs2_IngestIndexEntry(H, ctx, pIR, pIE, wVolumeId, wFlagsSource);
        cbTotalIndexEntry -= pIE->Length;
        oNextIndexEntry += pIE->Length;
    }
}

/*
* Try to ingest a MFT 'FILE' record.
* This could be either a $Mft from a _FILE_OBJECT or a physical memory page.
*/
VOID FcNtfs2_IngestFileRecord(_In_ VMM_HANDLE H, _In_ POB_FCNTFS2_INIT_CONTEXT ctx, _In_ PNTFS_FILE_RECORD pR, _In_reads_(0x400) PBYTE pb, _In_ WORD wVolumeId, _In_ WORD wFlagsSource, _In_ QWORD paRecord)
{
    QWORD qwKey1, qwKey2;
    DWORD oA, cbData = 0, cbuName = 0, cbuNameADS = 0;
    PFCNTFS2 pNt = NULL, pNtADS = NULL, pNtDuplicate;
    PNTFS_ATTR pA, pADataADS = NULL;
    PNTFS_FILE_NAME pfnC, pfn = NULL;
    PNTFS_STANDARD_INFORMATION psi = NULL;
    if(pR->Signature != 'ELIF') { return; }
    // Extract attributes loop:
    oA = pR->FirstAttributeOffset;
    while((oA + sizeof(NTFS_ATTR) < 0x400)) {
        pA = (PNTFS_ATTR)(pb + oA);
        if((pA->Type == 0xffffffff) || (pA->Length < sizeof(NTFS_ATTR))) { break; }
        if(oA + pA->Length > 0x400) { break; }
        if(pA->Type == NTFS_ATTR_TYPE_DATA) {
            if(pA->NameLength) {
                // NAMED DATA (ADS):
                if(((pA->NameOffset) == 0x18) && (pA->Length >= pA->NameOffset + sizeof(WCHAR) * pA->NameLength)) {
                    pADataADS = pA;
                    if(!pA->fNonResident) {
                        if(pA->Length < pA->AttrOffset + pA->AttrLength) {
                            break;
                        }
                    }
                }
            } else {
                // UNNAMED DATA (STANDARD FILE):
                if(!pA->fNonResident) {
                    if(pA->Length < pA->AttrOffset + pA->AttrLength) {
                        break;
                    }
                    cbData = pA->AttrLength;
                }
            }
        }
        if(pA->Type == NTFS_ATTR_TYPE_STANDARD_INFORMATION) {
            if(pA->Length < pA->AttrOffset + pA->AttrLength) {
                break;
            }
            if(pA->AttrLength < sizeof(NTFS_STANDARD_INFORMATION) && (pA->AttrLength != NTFS_STANDARD_INFORMATION_LEN_PREWIN2K)) { break; }
            psi = (PNTFS_STANDARD_INFORMATION)(pb + oA + pA->AttrOffset);
        }
        if(pA->Type == NTFS_ATTR_TYPE_FILE_NAME) {
            if(pA->Length < pA->AttrOffset + pA->AttrLength) {
                break;
            }
            pfnC = (PNTFS_FILE_NAME)(pb + oA + pA->AttrOffset);
            if(pA->AttrLength >= 42 + pfnC->NameLength * sizeof(WCHAR)) {
                if(pfnC->NameSpace == NTFS_FILENAME_NAMESPACE_DOS) {
                    ;   // DOS NameSpace is currently ignored
                } else if(!pfn || (pfnC->SizeReal > pfn->SizeAllocated)) {
                    pfn = pfnC;
                }
            }
        }
        oA += pA->Length;
    }
    // Verify validity:
    if(!psi || !pfn || (pfn->ParentDirectory.SegmentNumber > 0xfffffff0) || !pfn->NameLength) { return; }           // verify
    if((pR->Flags & 2) && cbData) { return; }                                                                       // verify (directory must not have unnamed data)
    // Duplicate check:
    // Consider pre-existing $Mft records as duplicates.
    // Do not consider pre-existing $Directory records as duplicates if the replacing record is a $Mft record.
    qwKey1 = FCNTFS2_DUPLICATE_CHECK_FILE_RECORD(pR->MftRecordNumber, psi->TimeCreate, psi->TimeModify, psi->TimeRead);
    if((qwKey2 = ObCounter_Get(ctx->pcDuplicate, qwKey1))) {
        if((pNtDuplicate = (PFCNTFS2)ObMap_GetByKey(ctx->pmMft, qwKey2))) {
            if((wFlagsSource == FCNTFS2_FLAG_SOURCE_FILE_DIR) || (wFlagsSource == FCNTFS2_FLAG_SOURCE_PMEM_DIR) || (pNtDuplicate->flags & FCNTFS2_FLAG_SOURCE_FILE_MFT) || (pNtDuplicate->flags & FCNTFS2_FLAG_SOURCE_PMEM_MFT)) {
                return;
            }
            LocalFree(ObMap_Remove(ctx->pmMft, pNtDuplicate));
        }
    }
    // Create NTFS object:
    if(!CharUtil_WtoU(pfn->Name, pfn->NameLength, NULL, 0, NULL, &cbuName, 0) || !cbuName) { return; }
    if(!(pNt = LocalAlloc(LMEM_ZEROINIT, sizeof(FCNTFS2) + cbuName))) { return; }
    if(!CharUtil_WtoU(pfn->Name, pfn->NameLength, pNt->uszName, cbuName, NULL, &cbuName, CHARUTIL_FLAG_STR_BUFONLY) || !cbuName) {
        LocalFree(pNt);
        return;
    }
    pNt->paRecord = paRecord;
    pNt->dwMftRecordNumber = pR->MftRecordNumber;
    pNt->dwParentMftRecordNumber = (DWORD)pfn->ParentDirectory.SegmentNumber;
    pNt->wParentMftSequenceNumber = (WORD)pfn->ParentDirectory.SequenceNumber;
    pNt->wMftSequenceNumber = pR->SequenceNumber;
    pNt->flags = wFlagsSource | (pR->Flags & 3) | (cbData ? FCNTFS2_FLAG_RESIDENT : 0);
    pNt->wVolumeId = wVolumeId;
    pNt->ftCreate = psi->TimeCreate;
    pNt->ftModify = psi->TimeModify;
    pNt->ftRead = psi->TimeRead;
    pNt->cbFileSize = cbData ? cbData : pfn->SizeReal;
    // Push NTFS object to "MAP":
    if(!FcNtfs2_IngestPushEntry(H, ctx, pNt, pfn)) {
        LocalFree(pNt);
        return;
    }
    if(!pADataADS) { return; }
    // If ADS, create NTFS object for ADS:
    if(!CharUtil_WtoU((LPWSTR)((PBYTE)pADataADS + pADataADS->NameOffset), pADataADS->NameLength, NULL, 0, NULL, &cbuNameADS, 0) || !cbuNameADS) { return; }
    if(!(pNtADS = LocalAlloc(LMEM_ZEROINIT, sizeof(FCNTFS2) + cbuName + cbuNameADS))) { return; }
    memcpy(pNtADS, pNt, sizeof(FCNTFS2) + cbuName);
    pNtADS->uszName[cbuName - 1] = ':';
    if(!CharUtil_WtoU((LPWSTR)((PBYTE)pADataADS + pADataADS->NameOffset), pADataADS->NameLength, pNtADS->uszName + cbuName, cbuNameADS, NULL, &cbuNameADS, CHARUTIL_FLAG_STR_BUFONLY) || !cbuNameADS) {
        LocalFree(pNtADS);
        return;
    }
    pNtADS->cbFileSize = pADataADS->fNonResident ? 0 : pADataADS->AttrLength;
    pNtADS->flags = wFlagsSource | (pR->Flags & FCNTFS2_FLAG_ACTIVE) | ((pADataADS->fNonResident || !pNtADS->cbFileSize) ? 0 : FCNTFS2_FLAG_RESIDENT) | FCNTFS2_FLAG_ADS;
    pNtADS->wRecordNumberIndex++;
    // Push NTFS ADS object to "MAP":
    if(!FcNtfs2_IngestPushEntry(H, ctx, pNtADS, NULL)) {
        LocalFree(pNtADS);
        return;
    }
}

/*
* Fetch a "volume id" for the $Mft file.
*/
_Success_(return != 0)
WORD FcNtfs2_Init_NewVolumeFromMft(_In_ VMM_HANDLE H, _In_ POB_FCNTFS2_INIT_CONTEXT ctx, _In_ POB_VMMWINOBJ_FILE pFileMft, _In_ BOOL fForceExisting)
{
    QWORD vaDevice = 0;
    BOOL f32 = H->vmm.f32;
    PFCNTFS2_VOLUME pVolume = NULL;
    // 1: fetch device virtual address (in case of multiple $Mft pointers to same device):
    if(!VmmRead(H, PVMM_PROCESS_SYSTEM, pFileMft->va + (f32 ? 4 : 8), (PBYTE)&vaDevice, (f32 ? 4 : 8))) { return 0; }
    // 2: try find existing volume:
    if((pVolume = ObMap_GetByKey(ctx->pmVolume, vaDevice))) {
        return fForceExisting ? pVolume->wId : 0;
    }
    if(fForceExisting) { return 0; }
    // 3: create new volume:
    if(ctx->wNextVolumeId == (WORD)-1) { return 0; }
    if(!(pVolume = LocalAlloc(LMEM_ZEROINIT, sizeof(FCNTFS2_VOLUME)))) { return 0; }
    pVolume->wId = ctx->wNextVolumeId++;
    pVolume->vaDevice = vaDevice;
    _snprintf_s(pVolume->uszVolumeName, _countof(pVolume->uszVolumeName), _TRUNCATE, "$MFT @ 0x%llx", pFileMft->va);
    if(!ObMap_Push(ctx->pmVolume, pVolume->vaDevice, pVolume)) { return 0; }
    return pVolume->wId;
}

/*
* Ingest a $Mft file (from _FILE_OBJECT).
*/
VOID FcNtfs2_InitMft(_In_ VMM_HANDLE H, _In_ POB_FCNTFS2_INIT_CONTEXT ctx, _In_ POB_VMMWINOBJ_FILE pFile)
{
    PNTFS_FILE_RECORD pR;
    DWORD cbBuffer, oBuffer;
    QWORD oFile = 0, pa;
    WORD wVolumeId = 0;
    if(!pFile->cb || (pFile->cb % 0x400)) { return; }
    if(!(wVolumeId = FcNtfs2_Init_NewVolumeFromMft(H, ctx, pFile, FALSE))) { return; }
    while(oFile < pFile->cb) {
        cbBuffer = (DWORD)min(0x00100000, pFile->cb - oFile);
        ZeroMemory(ctx->pb1M, 0x00100000);
        VmmWinObjFile_Read(H, pFile, oFile, ctx->pb1M, cbBuffer, 0, VMMWINOBJ_FILE_TP_DEFAULT);
        for(oBuffer = 0; oBuffer < cbBuffer; oBuffer += 0x400) {
            if((oBuffer & 0xfff) == 0) { pa = 0; }
            pR = (PNTFS_FILE_RECORD)(ctx->pb1M + oBuffer);
            if(pR->Signature != 'ELIF') { continue; }
            if(pR->MftRecordNumber != ((oFile + oBuffer) >> 10)) { continue; }
            if(!pa) {
                VmmWinObjFile_GetPA(H, pFile, oFile + (oBuffer & ~0xfff), &pa);
                if(pa > 0x400000000) {
                    DWORD DEBUG = 1;
                    VmmWinObjFile_GetPA(H, pFile, oFile + (oBuffer & ~0xfff), &pa);
                }

            }
            FcNtfs2_IngestFileRecord(H, ctx, pR, ctx->pb1M + oBuffer, wVolumeId, FCNTFS2_FLAG_SOURCE_FILE_MFT, pa + (oBuffer & 0xfff));
        }
        oFile += cbBuffer;
    }
}

/*
* Ingest a $Directory file (from _FILE_OBJECT).
*/
VOID FcNtfs2_InitDir(_In_ VMM_HANDLE H, _In_ POB_FCNTFS2_INIT_CONTEXT ctx, _In_ POB_VMMWINOBJ_FILE pFile)
{
    PNTFS_INDEX_RECORD pIR;
    DWORD cbBuffer, oBuffer;
    QWORD oFile = 0;
    WORD wVolumeId = 0;
    if(!pFile->cb || (pFile->cb % 0x1000)) { return; }
    if(!(wVolumeId = FcNtfs2_Init_NewVolumeFromMft(H, ctx, pFile, TRUE))) { return; }
    while(oFile < pFile->cb) {
        cbBuffer = (DWORD)min(0x00100000, pFile->cb - oFile);
        ZeroMemory(ctx->pb1M, 0x00100000);
        VmmWinObjFile_Read(H, pFile, oFile, ctx->pb1M, cbBuffer, 0, VMMWINOBJ_FILE_TP_DEFAULT);
        for(oBuffer = 0; oBuffer < cbBuffer; oBuffer += 0x1000) {
            pIR = (PNTFS_INDEX_RECORD)(ctx->pb1M + oBuffer);
            if(pIR->Signature != 'XDNI') { continue; }
            FcNtfs2_IngestIndexRecord(H, ctx, pIR, ctx->pb1M + oBuffer, wVolumeId, FCNTFS2_FLAG_SOURCE_FILE_DIR);
        }
        oFile += cbBuffer;
    }
}

int FcNtfs2_Init_CmpSortFileSize(_In_ POB_MAP_ENTRY p1, _In_ POB_MAP_ENTRY p2)
{
    POB_VMMWINOBJ_FILE e1 = p1->v;
    POB_VMMWINOBJ_FILE e2 = p2->v;
    if(e1->cb != e2->cb) {
        return (e1->cb < e2->cb) ? -1 : 1;
    }
    return (int)(e1->va - e2->va);
}

VOID FcNtfs2_Init1(_In_ VMM_HANDLE H, POB_FCNTFS2_INIT_CONTEXT ctx)
{
    POB_VMMWINOBJ_FILE pObFile = NULL;
    POB_SET psObDuplicates = NULL;
    POB_MAP pmObFiles = NULL, pmObMft = NULL, pmObDirectory = NULL;
    // init and fetch all files:
    if(!(psObDuplicates = ObSet_New(H))) { goto fail; }
    if(!(pmObMft = ObMap_New(H, OB_MAP_FLAGS_OBJECT_OB))) { goto fail; }
    if(!(pmObDirectory = ObMap_New(H, OB_MAP_FLAGS_OBJECT_OB))) { goto fail; }
    if(!VmmWinObjFile_GetAll(H, &pmObFiles)) { goto fail; }
    // find $Mft and $Directory files, sort by file size:
    while((pObFile = ObMap_GetNext(pmObFiles, pObFile))) {
        if(CharUtil_StrEquals("\\$Mft", pObFile->uszPath, FALSE)) {
            if(pObFile->vaSectionObjectPointers && ObSet_Push(psObDuplicates, pObFile->vaSectionObjectPointers)) {
                ObMap_Push(pmObMft, pObFile->va, pObFile);
            }
        } else if(CharUtil_StrEquals("\\$Directory", pObFile->uszPath, FALSE)) {
            if(pObFile->vaSectionObjectPointers && ObSet_Push(psObDuplicates, pObFile->vaSectionObjectPointers)) {
                ObMap_Push(pmObDirectory, pObFile->va, pObFile);
            }
        }
    }
    ObMap_SortEntryIndex(pmObMft, FcNtfs2_Init_CmpSortFileSize);
    ObMap_SortEntryIndex(pmObDirectory, FcNtfs2_Init_CmpSortFileSize);
    // ingest $Mft files:
    while((pObFile = ObMap_Pop(pmObMft))) {
        VmmLog(H, ctx->MID, LOGLEVEL_5_DEBUG, "Ingesting $Mft file:[%s] size:[%llu] va:[0x%llx]", pObFile->uszPath, pObFile->cb, pObFile->va);
        FcNtfs2_InitMft(H, ctx, pObFile);
    }
    // ingest $Directory files:
    while((pObFile = ObMap_Pop(pmObDirectory))) {
        VmmLog(H, ctx->MID, LOGLEVEL_5_DEBUG, "Ingesting $Directory file:[%s] size:[%llu] va:[0x%llx]", pObFile->uszPath, pObFile->cb, pObFile->va);
        FcNtfs2_InitDir(H, ctx, pObFile);
    }
fail:
    Ob_DECREF(psObDuplicates);
    Ob_DECREF(pmObDirectory);
    Ob_DECREF(pmObFiles);
    Ob_DECREF(pmObMft);
}

/*
* Filter incoming POB_FC_SCANPHYSMEM_CHUNK to retrieve potential MFT entry
* physical page addresses and their data in a map [pa -> pb].
* CALLER DECREF: return
* -- pc
* -- return = MAP or NULL if no candidate pages found.
*/
POB_MAP FcNtfs2_IngestGetValidAddrMap(_In_ VMM_HANDLE H, _In_ PVMMDLL_FORENSIC_INGEST_PHYSMEM pc)
{
    BOOL fPfnValidForMft;
    DWORD i, dwSignature = 0;
    POB_MAP pmObAddr;
    PVMMDLL_MAP_PFNENTRY pePfn;
    if(!(pmObAddr = ObMap_New(H, 0))) { return NULL; }
    for(i = 0; i < 0x1000; i++) {
        if((pc->ppMEMs[i]->qwA != (QWORD)-1) && pc->ppMEMs[i]->f && (pc->ppMEMs[i]->cb == 0x1000) && (dwSignature = *(PDWORD)pc->ppMEMs[i]->pb) && ((dwSignature == 'ELIF') || dwSignature == 'XDNI')) {
            pePfn = (pc->pPfnMap && (i < pc->pPfnMap->cMap)) ? (pc->pPfnMap->pMap + i) : NULL;
            fPfnValidForMft =
                !pePfn || (pePfn->dwPfn != (pc->ppMEMs[i]->qwA >> 12)) ||
                (pePfn->PageLocation == MmPfnTypeStandby) ||
                (pePfn->PageLocation == MmPfnTypeModified) ||
                (pePfn->PageLocation == MmPfnTypeModifiedNoWrite) ||
                (pePfn->PageLocation == MmPfnTypeTransition) ||
                ((pePfn->PageLocation == MmPfnTypeActive) && (pePfn->Priority >= 5));
            if(fPfnValidForMft) {
                ObMap_Push(pmObAddr, pc->ppMEMs[i]->qwA, pc->ppMEMs[i]->pb);
            }
        }
    }
    if(0 == ObMap_Size(pmObAddr)) {
        Ob_DECREF_NULL(&pmObAddr);
    }
    return pmObAddr;
}

/*
* Analyze a POB_FC_SCANPHYSMEM_CHUNK 16MB memory chunk for MFT/INDX file candidates
* and add any found to the internal data sets.
* -- H
* -- ctxfc
* -- pIngestPhysmem
*/
VOID FcNtfs2_FcIngestPhysmem(_In_ VMM_HANDLE H, _In_opt_ PVOID ctxfc, _In_ PVMMDLL_FORENSIC_INGEST_PHYSMEM pIngestPhysmem)
{
    PNTFS_INDEX_RECORD pI;
    PNTFS_FILE_RECORD pR;
    DWORD i;
    QWORD pa;
    PBYTE pb;
    POB_MAP pmObAddr = NULL;
    POB_FCNTFS2_INIT_CONTEXT ctx = (POB_FCNTFS2_INIT_CONTEXT)ctxfc;
    if(ctx && (pmObAddr = FcNtfs2_IngestGetValidAddrMap(H, pIngestPhysmem))) {
        while((pb = ObMap_PopWithKey(pmObAddr, &pa))) {
            if(*(PDWORD)pb == 'XDNI') {
                pI = (PNTFS_INDEX_RECORD)pb;
                FcNtfs2_IngestIndexRecord(H, ctx, pI, pb, 0, FCNTFS2_FLAG_SOURCE_PMEM_DIR);
            } else {
                for(i = 0; i < 4; i++) {
                    pR = (PNTFS_FILE_RECORD)pb;
                    if((pR->Signature != 'ELIF') || ((pR->MftRecordNumber & 3) != i)) { continue; }
                    FcNtfs2_IngestFileRecord(H, ctx, pR, pb, 0, FCNTFS2_FLAG_SOURCE_PMEM_MFT, pa);
                    pa += 0x400;
                    pb += 0x400;
                }
            }
        }
    }
    Ob_DECREF(pmObAddr);
}

/*
* Cleanup callback for POB_FCNTFS2_INIT_CONTEXT.
*/
VOID FcNtfs2_InitContext_CleanupCB(POB_FCNTFS2_INIT_CONTEXT pOb)
{
    Ob_DECREF(pOb->pmMft);
    Ob_DECREF(pOb->pmVolume);
    Ob_DECREF(pOb->pcDuplicate);
    LocalFree(pOb->pb1M);
    LocalFree(pOb->pVolumes);
}

/*
* Inititialize the initialization context.
* CALLER DECREF: return
* -- H
* -- return
*/
_Success_(return != NULL)
POB_FCNTFS2_INIT_CONTEXT FcNtfs2_InitContext(_In_ VMM_HANDLE H, _In_ PVMMDLL_PLUGIN_CONTEXT ctxP)
{
    PFCNTFS2_VOLUME pVolume = NULL;
    POB_FCNTFS2_INIT_CONTEXT ctxOb = NULL;
    if(!(ctxOb = Ob_AllocEx(H, 'CNtF', LMEM_ZEROINIT, sizeof(OB_FCNTFS2_INIT_CONTEXT), (OB_CLEANUP_CB)FcNtfs2_InitContext_CleanupCB, NULL))) { goto fail; }
    if(!(ctxOb->pmMft = ObMap_New(H, OB_MAP_FLAGS_OBJECT_LOCALFREE))) { goto fail; }
    if(!(ctxOb->pmVolume = ObMap_New(H, OB_MAP_FLAGS_OBJECT_LOCALFREE))) { goto fail; }
    if(!(ctxOb->pcDuplicate = ObCounter_New(H, 0))) { goto fail; }
    if(!(ctxOb->pb1M = LocalAlloc(0, 0x00100000))) { goto fail; }
    ctxOb->MID = ctxP->MID;
    ctxOb->fLogTrace = VmmLogIsActive(H, ctxP->MID, LOGLEVEL_6_TRACE);
    // create a fake volume for physical-only entries:
    {
        if(!(pVolume = LocalAlloc(LMEM_ZEROINIT, sizeof(FCNTFS2_VOLUME)))) { goto fail; }
        pVolume->wId = ctxOb->wNextVolumeId++;
        pVolume->vaDevice = 1;
        strncpy_s(pVolume->uszVolumeName, _countof(pVolume->uszVolumeName), "PHYSICAL", _TRUNCATE);
        if(!ObMap_Push(ctxOb->pmVolume, pVolume->vaDevice, pVolume)) { goto fail; }
    }
    Ob_INCREF(ctxOb);
fail:
    return Ob_DECREF(ctxOb);
}

_Success_(return)
BOOL FcNtfs2_FcIngestFinalize_CreateVolumeRoots(_In_ VMM_HANDLE H, _In_ POB_FCNTFS2_INIT_CONTEXT ctx)
{
    DWORD i;
    PFCNTFS2_VOLUME pVolume;
    // 1: copy volumes to array:
    ctx->cVolumes = ObMap_Size(ctx->pmVolume);
    ctx->pVolumes = LocalAlloc(LMEM_ZEROINIT, ctx->cVolumes * sizeof(FCNTFS2_VOLUME));
    if(!ctx->pVolumes) { return FALSE; }
    for(i = 0; i < ctx->cVolumes; i++) {
        pVolume = (PFCNTFS2_VOLUME)ObMap_GetByIndex(ctx->pmVolume, i);
        if(!pVolume) { return FALSE; }
        memcpy(ctx->pVolumes + i, pVolume, sizeof(FCNTFS2_VOLUME));
    }
    // 2: create volume roots:
    for(i = 0; i < ctx->cVolumes; i++) {
        ctx->pVolumes[i].pRoot = FcNtfs2_IngestPushEntrySynthetic(H, ctx, 5, NULL, ctx->pVolumes[i].wId, ctx->pVolumes[i].wId);
        if(!ctx->pVolumes[i].pRoot) { return FALSE; }
        ctx->pVolumes[i].pOrphan = FcNtfs2_IngestPushEntrySynthetic(H, ctx, (DWORD)-1, ctx->pVolumes[i].pRoot, ctx->pVolumes[i].wId, 0xffffffff);
        if(!ctx->pVolumes[i].pOrphan) { return FALSE; }
    }
    return TRUE;
}

/*
* Calculate a validity score for merge suitability.
* -- pDir
* -- pe
* -- return = match score: Higher is better. Zero is forbidden.
*/
DWORD FcNtfs2_FcIngestFinalize_MergeScore(_In_ PFCNTFS2 pDir, _In_ PFCNTFS2 pe)
{
    DWORD dwScore = 0;
    // 1: check forbidden conditions:
    if((pDir->wVolumeId != pe->wVolumeId) && !pDir->wVolumeId && !pe->wVolumeId) { return 0; }
    // 2: calculate score:
    dwScore += (pDir->wVolumeId && (pDir->wVolumeId == pe->wVolumeId)) ? 0x02000000 : 0;
    dwScore += (pDir->wVolumeId && (pDir->wVolumeId < 0xff)) ? ((0x00000100 - pDir->wVolumeId) << 16) : 0;
    dwScore += (pDir->flags & FCNTFS2_FLAG_ACTIVE) ? 0x01000000 : 0;
    dwScore += pDir->wMftSequenceNumber;
    // 3: loop protect when attach dir:
    if(pe->flags & FCNTFS2_FLAG_DIRECTORY) {
        while((pDir = pDir->pParent)) {
            if(pDir == pe) { return 0; }
        }
    }
    return dwScore;
}

/*
* Find, if possible, the most suitable directory to merge the ntfs entry onto.
* -- ctx
* -- pe
* -- return = merge directory or null on fail.
*/
PFCNTFS2 FcNtfs2_FcIngestFinalize_MergeFind(_In_ POB_FCNTFS2_INIT_CONTEXT ctx, _In_ PFCNTFS2 pe)
{
    PFCNTFS2 pDirThis, pDirMerge = NULL;
    DWORD dwScoreThis, dwScoreMerge = 0;
    QWORD qwKey = pe->dwParentMftRecordNumber;
    while((pDirThis = ObMap_GetByKey(ctx->pmMft, qwKey))) {
        qwKey += 0x0001000000000000;
        if(!(pDirThis->flags & FCNTFS2_FLAG_DIRECTORY)) { continue; }
        dwScoreThis = FcNtfs2_FcIngestFinalize_MergeScore(pDirThis, pe);
        if(!dwScoreThis || (dwScoreThis < dwScoreMerge)) { continue; }
        dwScoreMerge = dwScoreThis;
        pDirMerge = pDirThis;
    }
    return pDirMerge;
}

/*
* Merge entries onto already existing directories, or set as orphan.
*/
_Success_(return)
BOOL FcNtfs2_FcIngestFinalize_MergeAll(_In_ VMM_HANDLE H, _In_ POB_FCNTFS2_INIT_CONTEXT ctx)
{
    DWORD i, cMap;
    PFCNTFS2 pe, pParent;
    PFCNTFS2_VOLUME pVolume;
    cMap = ObMap_Size(ctx->pmMft);
    for(i = 0; i < cMap; i++) {
        pe = (PFCNTFS2)ObMap_GetByIndex(ctx->pmMft, i);
        if(pe->flags & FCNTFS2_FLAG_SOURCE_SYNTHETIC) { continue; }
        pParent = FcNtfs2_FcIngestFinalize_MergeFind(ctx, pe);
        if(!pParent) {
            // no parent -> create synthetic orphan sub-directory:
            pVolume = ctx->pVolumes + pe->wVolumeId;
            pParent = FcNtfs2_IngestPushEntrySynthetic(H, ctx, pe->dwParentMftRecordNumber, pVolume->pOrphan, pVolume->wId, --pVolume->dwNextOrphan);
            if(!pParent) { return FALSE; }
        }
        pe->pParent = pParent;
        pe->pSibling = pParent->pChild;
        pParent->pChild = pe;
        pParent->cChild++;
    }
    return TRUE;
}

/*
* Shrink orphan sub-dirs with 1-3 entries into $_ORPHAN parent.
*/
VOID FcNtfs2_FcIngestFinalize_MergeShrink(_In_ VMM_HANDLE H, _In_ POB_FCNTFS2_INIT_CONTEXT ctx)
{
    DWORD i, iDir;
    PFCNTFS2 peOrphan, peDir, peDirNext, pe;
    for(i = 0; i < ctx->cVolumes; i++) {
        peOrphan = ctx->pVolumes[i].pOrphan;
        peDir = peOrphan->pChild;
        peOrphan->pChild = NULL;
        iDir = 0;
        while(peDir) {
            peDirNext = peDir->pSibling;
            if(peDir->cChild <= 3) {
                // move children to orphan root (and drop directory):
                pe = peDir->pChild;
                pe->pParent = peOrphan;
                pe->pSibling = peOrphan->pChild;
                peOrphan->pChild = pe;
                peOrphan->cChild += peDir->cChild - 1;
                peDir->pChild = NULL;
                peDir->cChild = 0;
            } else {
                // keep directory:
                peDir->pSibling = peOrphan->pChild;
                peOrphan->pChild = peDir;
                _snprintf_s(peDir->uszName, FCNTFS2_SYNTHETIC_NAME_BUFSIZE, _TRUNCATE, "$%u", ++iDir);
            }
            peDir = peDirNext;
        }
    }
}

int FcNtfs2_FcIngestFinalize_MergeSortCompare(_In_ PFCNTFS2 *ppe1, _In_ PFCNTFS2 *ppe2)
{
    CHAR c1, c2;
    DWORD i = 0;
    PFCNTFS2 pe1 = *ppe1;
    PFCNTFS2 pe2 = *ppe2;
    // dir/file diff:
    if((pe1->flags & FCNTFS2_FLAG_DIRECTORY) != (pe2->flags & FCNTFS2_FLAG_DIRECTORY)) {
        return (pe1->flags & FCNTFS2_FLAG_DIRECTORY) ? 1 : -1;
    }
    // name diff:
    while(TRUE) {
        c1 = pe1->uszName[i]; if(c1 >= 'A' && c1 <= 'Z') { c1 += 'a' - 'A'; }
        c2 = pe2->uszName[i]; if(c2 >= 'A' && c2 <= 'Z') { c2 += 'a' - 'A'; }
        if((c1 != c2) || !c1) { break; }
        i++;
    }
    if(c1 != c2) {
        return (c1 < c2) ? -1 : 1;
    }
    // child diff:
    if(pe1->cChild != pe2->cChild) {
        return (pe1->cChild < pe2->cChild) ? 1 : -1;
    }
    // active diff:
    if((pe1->flags & FCNTFS2_FLAG_ACTIVE) != (pe2->flags & FCNTFS2_FLAG_ACTIVE)) {
        return (pe2->flags & FCNTFS2_FLAG_ACTIVE) ? 1 : -1;
    }
    // mft record number diff:
    if(pe1->dwMftRecordNumber != pe2->dwMftRecordNumber) {
        return (pe1->dwMftRecordNumber < pe2->dwMftRecordNumber) ? 1 : -1;
    }
    // physical address diff:
    if(pe1->paRecord != pe2->paRecord) {
        return (pe1->paRecord < pe2->paRecord) ? 1 : -1;
    }
    return 0;
}

/*
* Sort directory entries by type, and and children.
*/
VOID FcNtfs2_FcIngestFinalize_MergeSort(_In_ VMM_HANDLE H, _In_ POB_FCNTFS2_INIT_CONTEXT ctx)
{
    DWORD iEntry, cEntries, i;
    DWORD cChildArrayMax = 0x80000;     // 512k entries
    PFCNTFS2 *pChildArray, pDir, pe;
    pChildArray = (PFCNTFS2*)LocalAlloc(0, cChildArrayMax * sizeof(SIZE_T));
    if(!pChildArray) {
        VmmLog(H, ctx->MID, LOGLEVEL_1_CRITICAL, "Out of memory.");
        return;
    }
    cEntries = ObMap_Size(ctx->pmMft);
    for(iEntry = 0; iEntry < cEntries; iEntry++) {
        pDir = (PFCNTFS2)ObMap_GetByIndex(ctx->pmMft, iEntry);
        if(pDir->cChild < 2) { continue; }
        if(pDir->cChild >= cChildArrayMax) {
            VmmLog(H, ctx->MID, LOGLEVEL_2_WARNING, "Large number of files (>%uk) in directory '%s'. If possible to share memory dump file create an issue @Github!", cChildArrayMax >> 10, pDir->uszName);
            continue;
        }
        pe = pDir->pChild;
        i = 0;
        while(pe) {
            pChildArray[i++] = pe;
            pe = pe->pSibling;
        }
        qsort(pChildArray, i, sizeof(PFCNTFS2), (_CoreCrtNonSecureSearchSortCompareFunction)FcNtfs2_FcIngestFinalize_MergeSortCompare);
        pDir->pChild = NULL;
        while(i) {
            i--;
            pe = pChildArray[i];
            pe->pSibling = pDir->pChild;
            pDir->pChild = pe;
        }
    }
    LocalFree(pChildArray);
}

int FcNtfs2_FcIngestFinalize_VolumeCountSort_Compare(PFCNTFS2_VOLUME p1, PFCNTFS2_VOLUME p2)
{
    if(p1->cEntry != p2->cEntry) {
        return (p1->cEntry < p2->cEntry) ? 1 : -1;
    }
    return p1->wId - p2->wId;
}

/*
* Sort volumes by #entries, so that non-physical with largest #entries are put as [1], physical being [0].
*/
VOID FcNtfs2_FcIngestFinalize_VolumeCountSort(_In_ VMM_HANDLE H, _In_ POB_FCNTFS2_INIT_CONTEXT ctx)
{
    DWORD i, c;
    PFCNTFS2 pe;
    POB_MAP pmOb = NULL;
    if(ctx->cVolumes == 1) { return; }
    if(!(pmOb = ObMap_New(H, OB_MAP_FLAGS_OBJECT_VOID | OB_MAP_FLAGS_NOKEY))) { return; }
    // 1: volume roots (excl. required physical [i = 0]:
    for(i = 1; i < ctx->cVolumes; i++) {
        c = 0;
        ObMap_Push(pmOb, 0, ctx->pVolumes[i].pRoot);
        while((pe = ObMap_Pop(pmOb))) {
            while(pe) {
                c++;
                if(pe->pChild) {
                    ObMap_Push(pmOb, 0, pe->pChild);
                }
                pe = pe->pSibling;
            }
        }
        ctx->pVolumes[i].cEntry = c;
    }
    // 2: sort volumes by entry count:
    qsort(ctx->pVolumes + 1, ctx->cVolumes - 1, sizeof(FCNTFS2_VOLUME), (_CoreCrtNonSecureSearchSortCompareFunction)FcNtfs2_FcIngestFinalize_VolumeCountSort_Compare);
    for(i = 1; i < ctx->cVolumes; i++) {
        ctx->pVolumes[i].wId = (WORD)i;
        if(ctx->pVolumes[i].pRoot) {
            _snprintf_s(ctx->pVolumes[i].pRoot->uszName, FCNTFS2_SYNTHETIC_NAME_BUFSIZE, _TRUNCATE, "%u", i);
        }
    }
    Ob_DECREF(pmOb);
}

/*
* File system hash entries.
*/
_Success_(return)
BOOL FcNtfs2_FcIngestFinalize_MergeHash(_In_ VMM_HANDLE H, _In_ POB_FCNTFS2_INIT_CONTEXT ctx)
{
    BOOL fResult = FALSE;
    DWORD iParent = 0, i;
    PFCNTFS2 peParent, pe;
    POB_MAP pmOb = NULL;
    POB_COUNTER pcOb = NULL;
    QWORD qwName_SeqNbr;
    if(!(pmOb = ObMap_New(H, OB_MAP_FLAGS_OBJECT_VOID | OB_MAP_FLAGS_NOKEY))) { goto fail; }
    if(!(pcOb = ObCounter_New(H, 0))) { goto fail; }
    // 1: hash volume roots:
    for(i = 0; i < ctx->cVolumes; i++) {
        if((pe = ctx->pVolumes[i].pRoot)) {
            pe->dwHashName = CharUtil_HashNameFsU(pe->uszName, pe->wName_SeqNbr);
            pe->qwHashPath = pe->dwHashName;
            ObMap_Push(pmOb, 0, pe);
        }
    }
    // 2: hash all entries - a breath-first traversal:
    while(TRUE) {
        peParent = ObMap_GetByIndex(pmOb, iParent++);
        if(!peParent) { break; }
        ObCounter_Clear(pcOb);
        pe = peParent->pChild;
        while(pe) {
            pe->dwHashName = CharUtil_HashNameFsU(pe->uszName, 0);
            if((qwName_SeqNbr = ObCounter_Inc(pcOb, pe->dwHashName) - 1)) {
                if(qwName_SeqNbr >= 0xffff) {
                    pe->pSibling = NULL;
                }
                pe->wName_SeqNbr = (WORD)qwName_SeqNbr;
                pe->dwHashName = CharUtil_HashNameFsU(pe->uszName, pe->wName_SeqNbr);
            }
            pe->qwHashPath = pe->dwHashName + ((peParent->qwHashPath >> 13) | (peParent->qwHashPath << 51));
            if(pe->pChild) {
                ObMap_Push(pmOb, 0, pe);
            }
            // detach/remove same-name entries with no physical address/children:
            while(pe->pSibling && !pe->pSibling->paRecord && !pe->pSibling->cChild && (pe->dwHashName == CharUtil_HashNameFsU(pe->pSibling->uszName, 0))) {
                pe->pSibling = pe->pSibling->pSibling;
                pe->pParent->cChild--;
            }
            pe = pe->pSibling;
        }
    }
    fResult = TRUE;
fail:
    Ob_DECREF(pcOb);
    Ob_DECREF(pmOb);
    return fResult;
}

/*
* Add a file system entry to the database.
*/
VOID FcNtfs2_FcIngestFinalize_DbPush_Database(_In_ VMM_HANDLE H, _In_ PFCNTFS2_FINALIZE_CONTEXT ctx, _In_ PFCNTFS2 pe, _In_ LPSTR uszPathName)
{
    FCSQL_INSERTSTRTABLE SqlStrInsert = { 0 };
    if(!Fc_SqlInsertStr(H, ctx->st_str, uszPathName + 1, &SqlStrInsert)) { return; }
    sqlite3_reset(ctx->st);
    sqlite3_bind_int64(ctx->st, 1, pe->qwDbId);
    sqlite3_bind_int64(ctx->st, 2, pe->pParent ? pe->pParent->qwDbId : (QWORD)-1);
    sqlite3_bind_int64(ctx->st, 3, SqlStrInsert.id);
    sqlite3_bind_int64(ctx->st, 4, pe->qwHashPath);
    sqlite3_bind_int64(ctx->st, 5, pe->pParent ? pe->pParent->qwHashPath : 0);
    sqlite3_bind_int64(ctx->st, 6, pe->paRecord);
    sqlite3_bind_int64(ctx->st, 7, pe->dwMftRecordNumber);
    sqlite3_bind_int64(ctx->st, 8, pe->dwParentMftRecordNumber);
    sqlite3_bind_int64(ctx->st, 9, pe->flags);
    sqlite3_bind_int64(ctx->st, 10, pe->cbFileSize);
    sqlite3_bind_int64(ctx->st, 11, pe->ftCreate);
    sqlite3_bind_int64(ctx->st, 12, pe->ftModify);
    sqlite3_bind_int64(ctx->st, 13, pe->ftRead);
    sqlite3_bind_int64(ctx->st, 14, pe->wName_SeqNbr);
    sqlite3_bind_int64(ctx->st, 15, ctx->cbUtf8Total + pe->qwDbId * M_NTFS_INFO_LINELENGTH_UTF8);
    sqlite3_bind_int64(ctx->st, 16, ctx->cbJsonTotal + pe->qwDbId * M_NTFS_INFO_LINELENGTH_JSON);
    sqlite3_step(ctx->st);
    ctx->cbUtf8Total += SqlStrInsert.cbu;
    ctx->cbJsonTotal += SqlStrInsert.cbj;
}

VOID FcNtfs2_FcIngestFinalize_DbPush_BuildPath(_In_ VMM_HANDLE H, _In_ PFCNTFS2_FINALIZE_CONTEXT ctx, _In_ PFCNTFS2 peNt, _In_ WORD wDirDepth, _In_reads_(2048) LPSTR uszPath, _In_ DWORD cuszPath)
{
    DWORD cuszName;
    while(peNt) {
        // update/set path
        cuszName = (DWORD)strlen(peNt->uszName);
        if(cuszPath + cuszName + 2 >= 2048) { break; }
        uszPath[cuszPath] = '\\';
        memcpy(&uszPath[cuszPath + 1], peNt->uszName, cuszName + 1ULL);
        // update/set path hash
        peNt->wDirDepth = wDirDepth;
        peNt->qwDbId = ctx->qwNextDbId++;
        FcNtfs2_FcIngestFinalize_DbPush_Database(H, ctx, peNt, uszPath);
        if(peNt->pChild) {
            FcNtfs2_FcIngestFinalize_DbPush_BuildPath(H, ctx, peNt->pChild, wDirDepth + 1, uszPath, cuszPath + cuszName + 1);
        }
        peNt = peNt->pSibling;
    }
    uszPath[cuszPath] = 0;
}

/*
* Finalize the NTFS setup/initialization phase. Try to put re-assemble the NTFS
* MFT file fragments into some kind of usable file-system approximation using
* heuristics and save it to the forensic database.
* -- H
* -- ctxfc
*/
VOID FcNtfs2_FcIngestFinalize_DbPush(_In_ VMM_HANDLE H, POB_FCNTFS2_INIT_CONTEXT ctx)
{
    FCNTFS2_FINALIZE_CONTEXT ctxFinal = { 0 };
    CHAR uszPath[2048] = { 0 };
    POB_SET psObHashPath = NULL;
    DWORD i;
    int rc;
    // SETUP FINISH:
    uszPath[0] = '\\';
    if(!(ctxFinal.hSql = Fc_SqlReserve(H))) { goto fail; }
    rc = sqlite3_prepare_v2(ctxFinal.hSql,
        "INSERT INTO ntfs " \
        "(id, id_parent, id_str, hash, hash_parent, addr_phys, inode, inode_parent, flags, size_file, time_create, time_modify, time_read, name_seq, oln_u, oln_j) " \
        "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"
        , -1, &ctxFinal.st, NULL);
    if(rc != SQLITE_OK) { goto fail; }
    rc = sqlite3_prepare_v2(ctxFinal.hSql, szFC_SQL_STR_INSERT, -1, &ctxFinal.st_str, NULL);
    if(rc != SQLITE_OK) { goto fail; }
    sqlite3_exec(ctxFinal.hSql, "BEGIN TRANSACTION", NULL, NULL, NULL);
    for(i = 0; i < ctx->cVolumes; i++) {
        if((i == 0) || (ctx->pVolumes[i].cEntry > 2)) {
            FcNtfs2_FcIngestFinalize_DbPush_BuildPath(H, &ctxFinal, ctx->pVolumes[i].pRoot, 0, uszPath, 1);
        }
    }
    sqlite3_exec(ctxFinal.hSql, "COMMIT TRANSACTION", NULL, NULL, NULL);
    // CLEAN UP:
fail:
    sqlite3_finalize(ctxFinal.st);
    sqlite3_finalize(ctxFinal.st_str);
    Fc_SqlReserveReturn(H, ctxFinal.hSql);
    Ob_DECREF(psObHashPath);
}

VOID FcNtfs2_FcIngestFinalize(_In_ VMM_HANDLE H, _In_opt_ PVOID ctxfc)
{
    POB_FCNTFS2_INIT_CONTEXT ctx = (POB_FCNTFS2_INIT_CONTEXT)ctxfc;
    if(!ctx) { return; }
    if(!FcNtfs2_FcIngestFinalize_CreateVolumeRoots(H, ctx)) { return; }
    if(!FcNtfs2_FcIngestFinalize_MergeAll(H, ctx)) { return; }
    FcNtfs2_FcIngestFinalize_MergeShrink(H, ctx);
    FcNtfs2_FcIngestFinalize_MergeSort(H, ctx);
    FcNtfs2_FcIngestFinalize_VolumeCountSort(H, ctx);
    if(!FcNtfs2_FcIngestFinalize_MergeHash(H, ctx)) { return; }
    Fc_SqlExec(H, FC_SQL_SCHEMA_NTFS);
    FcNtfs2_FcIngestFinalize_DbPush(H, ctx);
}

/*
* Timeline data by executing a partial SQL query on pre-existing data.
* -- H
* -- ctxfc
* -- hTimeline
* -- pfnAddEntry
* -- pfnEntryAddBySql
*/
VOID FcNtfs2_FcTimeline(
    _In_ VMM_HANDLE H,
    _In_opt_ PVOID ctxfc,
    _In_ HANDLE hTimeline,
    _In_ VOID(*pfnAddEntry)(_In_ VMM_HANDLE H, _In_ HANDLE hTimeline, _In_ QWORD ft, _In_ DWORD dwAction, _In_ DWORD dwPID, _In_ DWORD dwData32, _In_ QWORD qwData64, _In_ LPCSTR uszText),
    _In_ VOID(*pfnEntryAddBySql)(_In_ VMM_HANDLE H, _In_ HANDLE hTimeline, _In_ DWORD cEntrySql, _In_ LPCSTR *pszEntrySql)
) {
    LPCSTR pszSql[] = {
        "id_str, time_create, "STRINGIZE(FC_TIMELINE_ACTION_CREATE)", 0, size_file, addr_phys FROM ntfs WHERE time_create > 0;",
        "id_str, time_modify, "STRINGIZE(FC_TIMELINE_ACTION_MODIFY)", 0, size_file, addr_phys FROM ntfs WHERE time_modify > 0 AND time_modify != time_create;",
        "id_str, time_read,   "STRINGIZE(FC_TIMELINE_ACTION_READ)"  , 0, size_file, addr_phys FROM ntfs WHERE time_read   > 0 AND time_read != time_create AND time_read != time_modify;"
    };
    pfnEntryAddBySql(H, hTimeline, sizeof(pszSql) / sizeof(LPSTR), pszSql);
}

/*
* Finalize the NTFS setup/initialization phase. Close/Clear any allocated contexts.
* -- H
* -- ctxfc
*/
VOID FcNtfs2_FcFinalize(_In_ VMM_HANDLE H, _In_opt_ PVOID ctxfc)
{
    Ob_DECREF(ctxfc);
}

/*
* Initialize the forensic context which is used as an initialization context.
* At this stage the context is also populated with $Mft and $Directory contents.
* This takes a bit of time, but it's imperative that it's done before physical
* memory is ingested as it will increase the quality of the results.
* CALLER DECREF: return
*/
PVOID FcNtfs2_FcInitialize(_In_ VMM_HANDLE H, _In_ PVMMDLL_PLUGIN_CONTEXT ctxP)
{
    POB_FCNTFS2_INIT_CONTEXT ctxOb = FcNtfs2_InitContext(H, ctxP);
    if(!ctxOb) { return NULL; }
    FcNtfs2_Init1(H, ctxOb);
    return ctxOb;
}



//-----------------------------------------------------------------------------
// NTFS MFT DATA RETRIEVAL FUNCTIONALITY BELOW:
// In essence this is "just" a query interface towards the sqlite database
// with the exception of the relatively minor functionality to retrieve MFT
// resident file contents of very small files.
//-----------------------------------------------------------------------------

typedef struct tdFC_MAP_NTFSENTRY {
    QWORD qwId;
    QWORD pa;
    QWORD qwFileSize;
    QWORD ftCreate;
    QWORD ftModify;
    QWORD ftRead;
    DWORD dwMftId;
    DWORD dwMftIdParent;
    DWORD flags;
    DWORD dwTextSeq;
    QWORD cszuOffset;               // offset to start of "line" in bytes (utf-8)
    QWORD cszjOffset;               // offset to start of "line" in bytes (json)
    DWORD cbuText;                  // utf-8 byte count including terminating null
    LPSTR uszText;                  // utf-8 string pointed into FCOB_MAP_NTFS.uszMultiText
} FC_MAP_NTFSENTRY, *PFC_MAP_NTFSENTRY;

typedef struct tdFCOB_MAP_NTFS {
    OB ObHdr;
    LPSTR uszMultiText;             // multi-utf-8 string pointed into by FC_MAP_NTFSENTRY.uszText
    DWORD cbuMultiText;
    DWORD cMap;                     // # map entries.
    FC_MAP_NTFSENTRY pMap[0];        // map entries.
} FCOB_MAP_NTFS, *PFCOB_MAP_NTFS;

/*
* Retrieve the MFT resident data (i.e. read file contents that fit into the MFT).
* -- H
* -- pNtfsEntry
* -- pbData
* -- cbData
* -- pcbDataRead
* -- return
*/
_Success_(return)
BOOL FcNtfs2_GetMftResidentData(_In_ VMM_HANDLE H, _In_ PFC_MAP_NTFSENTRY pNtfsEntry, _Out_writes_opt_(cbData) PBYTE pbData, _In_ DWORD cbData, _Out_opt_ PDWORD pcbDataRead)
{
    DWORD oA;
    BOOL fADS;
    PNTFS_ATTR pA;
    PNTFS_FILE_RECORD pr;
    BYTE pbMftEntry[0x400];
    if(!VmmRead(H, NULL, pNtfsEntry->pa, pbMftEntry, 0x400)) { return FALSE; }
    pr = (PNTFS_FILE_RECORD)pbMftEntry;
    // Check MFT record number is within the correct location inside the page.
    if((((pNtfsEntry->pa >> 10) & 0x3) != (0x3 & pr->MftRecordNumber)) || (pr->MftRecordNumber == 0)) { return FALSE; }
    // Extract attributes loop.
    oA = pr->FirstAttributeOffset;
    while((oA + sizeof(NTFS_ATTR) < 0x400)) {
        pA = (PNTFS_ATTR)(pbMftEntry + oA);
        if((pA->Type == 0xffffffff) || (pA->Length < sizeof(NTFS_ATTR))) { return FALSE; }
        if(pA->Type == NTFS_ATTR_TYPE_DATA) {
            if((oA + pA->AttrOffset + pA->AttrLength > 0x400) || (pA->AttrLength > 0x400)) { return FALSE; }
            fADS = (pNtfsEntry->flags & FCNTFS2_FLAG_ADS) ? TRUE : FALSE;
            if((fADS && !pA->NameLength) || (!fADS && pA->NameLength)) {
                oA += pA->Length;
                continue;
            }
            if(cbData != pA->AttrLength) { return FALSE; }
            if(pbData) {
                memcpy(pbData, (pbMftEntry + oA + pA->AttrOffset), pA->AttrLength);
            }
            if(pcbDataRead) {
                *pcbDataRead = cbData;
            }
            return TRUE;
        }
        oA += pA->Length;
    }
    return FALSE;
}

#define FCNTFS_SQL_SELECT_FIELDS " sz, id, addr_phys, inode, inode_parent, flags, name_seq, time_create, time_modify, time_read, size_file, oln_u, oln_j "

_Success_(return)
BOOL FcNtfs2_Map_CreateInternal(_In_ VMM_HANDLE H, _In_ LPSTR szSqlCount, _In_ LPSTR szSqlSelect, _In_ DWORD cQueryValues, _In_reads_(cQueryValues) PQWORD pqwQueryValues, _Out_ PFCOB_MAP_NTFS *ppObNtfsMap)
{
    // TODO: CHANGE MAP INTO UTF-8 STRING!
    int rc;
    QWORD pqwResult[2];
    DWORD i, cbuMultiText, oMultiText = 1;
    LPSTR uszEntryText;
    PFCOB_MAP_NTFS pObNtfsMap = NULL;
    PFC_MAP_NTFSENTRY pe;
    sqlite3 *hSql = NULL;
    sqlite3_stmt *hStmt = NULL;
    rc = Fc_SqlQueryN(H, szSqlCount, cQueryValues, pqwQueryValues, 2, pqwResult, NULL);
    if((rc != SQLITE_OK) || (pqwResult[0] > 0x00010000) || (pqwResult[1] > 0x01000000)) { goto fail; }
    cbuMultiText = (DWORD)(1 + pqwResult[0] + pqwResult[1]);
    pObNtfsMap = Ob_AllocEx(H, OB_TAG_MOD_FCNTFS_CTX, LMEM_ZEROINIT, (SIZE_T)(sizeof(FCOB_MAP_NTFS) + pqwResult[0] * sizeof(FC_MAP_NTFSENTRY) + cbuMultiText), NULL, NULL);
    if(!pObNtfsMap) { goto fail; }
    pObNtfsMap->uszMultiText = (LPSTR)((PBYTE)pObNtfsMap + sizeof(FCOB_MAP_NTFS) + pqwResult[0] * sizeof(FC_MAP_NTFSENTRY));
    pObNtfsMap->cbuMultiText = cbuMultiText;
    pObNtfsMap->cMap = (DWORD)pqwResult[0];
    if(!(hSql = Fc_SqlReserve(H))) { goto fail; }
    rc = sqlite3_prepare_v2(hSql, szSqlSelect, -1, &hStmt, 0);
    if(rc != SQLITE_OK) { goto fail; }
    for(i = 0; i < cQueryValues; i++) {
        sqlite3_bind_int64(hStmt, i + 1, pqwQueryValues[i]);
    }
    for(i = 0; i < pObNtfsMap->cMap; i++) {
        rc = sqlite3_step(hStmt);
        if(rc != SQLITE_ROW) { goto fail; }
        pe = pObNtfsMap->pMap + i;
        // populate text related data: path
        uszEntryText = (LPSTR)sqlite3_column_text(hStmt, 0);
        pe->cbuText = sqlite3_column_bytes(hStmt, 0) + 1;
        if(!uszEntryText || (oMultiText + pe->cbuText > cbuMultiText)) { goto fail; }
        memcpy(pObNtfsMap->uszMultiText + oMultiText, uszEntryText, pe->cbuText);
        pe->uszText = pObNtfsMap->uszMultiText + oMultiText;
        oMultiText += pe->cbuText;
        // populate numeric data
        pe->qwId = sqlite3_column_int64(hStmt, 1);
        pe->pa = sqlite3_column_int64(hStmt, 2);
        pe->dwMftId = sqlite3_column_int(hStmt, 3);
        pe->dwMftIdParent = sqlite3_column_int(hStmt, 4);
        pe->flags = sqlite3_column_int(hStmt, 5);
        pe->dwTextSeq = sqlite3_column_int(hStmt, 6);
        pe->ftCreate = sqlite3_column_int64(hStmt, 7);
        pe->ftModify = sqlite3_column_int64(hStmt, 8);
        pe->ftRead = sqlite3_column_int64(hStmt, 9);
        pe->qwFileSize = sqlite3_column_int64(hStmt, 10);
        pe->cszuOffset = sqlite3_column_int64(hStmt, 11);
        pe->cszjOffset = sqlite3_column_int64(hStmt, 12);
    }
    Ob_INCREF(pObNtfsMap);
fail:
    sqlite3_finalize(hStmt);
    Fc_SqlReserveReturn(H, hSql);
    *ppObNtfsMap = Ob_DECREF(pObNtfsMap);
    return (*ppObNtfsMap != NULL);
}

/*
* Retrieve a FCOB_MAP_NTFS map object containing a specific entry given by its
* file system hash.
* -- H
* -- qwHash
* -- ppObNtfsMap
* -- return
*/
_Success_(return)
BOOL FcNtfs2_Map_GetFromHash(_In_ VMM_HANDLE H, _In_ QWORD qwHash, _Out_ PFCOB_MAP_NTFS *ppObNtfsMap)
{
    return FcNtfs2_Map_CreateInternal(
        H,
        "SELECT COUNT(*), SUM(cbu) FROM v_ntfs WHERE hash = ?",
        "SELECT "FCNTFS_SQL_SELECT_FIELDS" FROM v_ntfs WHERE hash = ?",
        1,
        &qwHash,
        ppObNtfsMap
    );
}

/*
* Retrieve a FCOB_MAP_NTFS map object containing entries which have the same
* file system parent given by its parent hash.
* -- H
* -- qwHashParent
* -- ppObNtfsMap
* -- return
*/
_Success_(return)
BOOL FcNtfs2_Map_GetFromHashParent(_In_ VMM_HANDLE H, _In_ QWORD qwHashParent, _Out_ PFCOB_MAP_NTFS *ppObNtfsMap)
{
    return FcNtfs2_Map_CreateInternal(
        H,
        "SELECT COUNT(*), SUM(cbu) FROM v_ntfs WHERE hash_parent = ?",
        "SELECT "FCNTFS_SQL_SELECT_FIELDS" FROM v_ntfs WHERE hash_parent = ?",
        1,
        &qwHashParent,
        ppObNtfsMap
    );
}

/*
* Retrieve a FCOB_MAP_NTFS map object containing entries within a range.
* -- H
* -- qwId
* -- cId
* -- ppObNtfsMap
* -- return
*/
_Success_(return)
BOOL FcNtfs2_Map_GetFromIdRange(_In_ VMM_HANDLE H, _In_ QWORD qwId, _In_ QWORD cId, _Out_ PFCOB_MAP_NTFS * ppObNtfsMap)
{
    QWORD v[] = { qwId, qwId + cId };
    return FcNtfs2_Map_CreateInternal(
        H,
        "SELECT COUNT(*), SUM(cbu) FROM v_ntfs WHERE id >= ? AND id < ?",
        "SELECT "FCNTFS_SQL_SELECT_FIELDS" FROM v_ntfs WHERE id >= ? AND id < ? ORDER BY id",
        2,
        v,
        ppObNtfsMap
    );
}

/*
* Retieve the file size of the ntfs information file either in JSON or UTF8.
* -- H
* -- pcRecords = number of entries/lines/records.
* -- pcbUTF8 = UTF8 text file size.
* -- pcbJSON = JSON file size.
* -- return
*/
_Success_(return)
BOOL FcNtfs2_GetFileSize(_In_ VMM_HANDLE H, _Out_opt_ PQWORD pcRecords, _Out_opt_ PQWORD pcbUTF8, _Out_opt_ PQWORD pcbJSON)
{
    QWORD pqwResult[3];
    // query below is convoluted but it's very fast ...
    if(SQLITE_OK != Fc_SqlQueryN(H, "SELECT id, oln_u+cbu+"STRINGIZE(M_NTFS_INFO_LINELENGTH_UTF8)" AS cbu_tot, oln_j+cbj+"STRINGIZE(M_NTFS_INFO_LINELENGTH_JSON)" AS cbj_tot FROM v_ntfs WHERE id = (SELECT MAX(id) FROM v_ntfs)", 0, NULL, 3, pqwResult, NULL)) { return FALSE; }
    if(pcRecords) { *pcRecords = pqwResult[0]; }
    if(pcbUTF8) { *pcbUTF8 = pqwResult[1]; }
    if(pcbJSON) { *pcbJSON = pqwResult[1]; }
    return TRUE;
}

/*
* Retrieve the id associated within the position of the info file.
* -- H
* -- qwFilePos
* -- fJSON
* -- pqwId
* -- return
*/
_Success_(return)
BOOL FcNtfs2_GetIdFromPosition(_In_ VMM_HANDLE H, _In_ QWORD qwFilePos, _In_ BOOL fJSON, _Out_ PQWORD pqwId)
{
    QWORD v[] = { max(2048, qwFilePos) - 2048, qwFilePos };
    return fJSON ?
        (SQLITE_OK == Fc_SqlQueryN(H, "SELECT MAX(id) FROM ntfs WHERE oln_j >= ? AND oln_j <= ?", 2, v, 1, pqwId, NULL)) :
        (SQLITE_OK == Fc_SqlQueryN(H, "SELECT MAX(id) FROM ntfs WHERE oln_u >= ? AND oln_u <= ?", 2, v, 1, pqwId, NULL));
}



//-----------------------------------------------------------------------------
// NTFS VFS FUNCTIONALITY BELOW:
//-----------------------------------------------------------------------------

#define M_NTFS_READINFOSINGLE_BUFFER        0x10000

/*
* Retrieve info file for single ntfs entry
* NB! CALLER LocalFree: return
* -- H
* -- peNtfs
* -- pcsz
* -- return
*/
PBYTE FcNtfs2_ReadInfoSingle(_In_ VMM_HANDLE H, _In_ PFC_MAP_NTFSENTRY peNtfs, _Out_ PDWORD pcsz)
{
    LPSTR sz = NULL;
    LPCSTR uszTextName;
    DWORD cszHexAscii;
    SIZE_T csz = 0;
    BYTE pbr[0x400];
    BYTE szHexAscii[0xC00];
    //----
    LPSTR uszRecordFileName;
    BYTE pbBuffer[2 * MAX_PATH];
    CHAR uszAttributeName[2 * MAX_PATH];
    CHAR szTimeC[24], szTimeA[24], szTimeM[24], szTimeR[24];
    CHAR szGuid1[37], szGuid2[37], szGuid3[37], szGuid4[37];
    DWORD oA, iStr;
    PNTFS_FILE_RECORD pR;
    PNTFS_ATTR pA;
    PNTFS_STANDARD_INFORMATION pSI;
    PNTFS_FILE_NAME pFN;
    PNTFS_OBJECT_ID pOID;
    *pcsz = 0;
    if(!VmmRead(H, NULL, peNtfs->pa, pbr, 0x400)) { goto fail; }
    if(!(sz = LocalAlloc(0, M_NTFS_READINFOSINGLE_BUFFER))) { goto fail; }
    pR = (PNTFS_FILE_RECORD)pbr;
    if(pR->FirstAttributeOffset > 0x300) { goto fail; }
    // file name+path:
    uszTextName = CharUtil_PathSplitLast(peNtfs->uszText);
    csz += snprintf(sz + csz, M_NTFS_READINFOSINGLE_BUFFER - csz,
        "INFORMATION\n======================\nName: %s\nPath: %s\n\n\n",
        uszTextName,
        peNtfs->uszText
    );
    // mft record:
    csz += snprintf(sz + csz, M_NTFS_READINFOSINGLE_BUFFER - csz,
        "$MFT_RECORD\n======================\nFlags:            %s, %s\nPhysical Address: 0x%llX\nRecord Number:    0x%X\nSequence Number:  0x%X\nHard Link Count:  %i\n\n\n",
        (pR->Flags & NTFS_FILE_RECORD_FLAG_ACTIVE ? "ACTIVE" : "INACTIVE"),
        (pR->Flags & NTFS_FILE_RECORD_FLAG_DIRECTORY ? "DIRECTORY" : "FILE"),
        peNtfs->pa,
        pR->MftRecordNumber,
        pR->SequenceNumber,
        pR->HardLinkCount
    );
    // Attributes Loop:
    oA = pR->FirstAttributeOffset;
    while((oA + sizeof(NTFS_ATTR) < 0x400)) {
        pA = (PNTFS_ATTR)(pbr + oA);
        if((pA->Type == 0xffffffff) || (pA->Length < sizeof(NTFS_ATTR))) { break; }
        if(oA + pA->Length > 0x400) { break; }
        if((oA + pA->AttrOffset + pA->AttrLength > 0x400) || (pA->AttrLength > 0x400)) { break; }
        // $ATTRIBUTE_HEADER NAME:
        uszAttributeName[0] = 0;
        if(pA->NameLength && pA->NameOffset && (pA->Length >= pA->NameOffset + sizeof(WCHAR) * pA->NameLength)) {
            strncpy_s(uszAttributeName, sizeof(uszAttributeName), "Attribute Name:   ", _TRUNCATE);
            CharUtil_WtoU((LPWSTR)((PBYTE)pA + pA->NameOffset), pA->NameLength, uszAttributeName + 18, (DWORD)(sizeof(uszAttributeName) - 18 - 2), NULL, NULL, CHARUTIL_FLAG_TRUNCATE | CHARUTIL_FLAG_STR_BUFONLY);
            strncat_s(uszAttributeName, sizeof(uszAttributeName), "\n", _TRUNCATE);
        }
        // $ATTRIBUTE_HEADER
        iStr = ((pA->Type <= NTFS_ATTR_TYPE_MAX) & !(pA->Type & 0xf)) ? pA->Type >> 4 : 0;
        csz += snprintf(sz + csz, M_NTFS_READINFOSINGLE_BUFFER - csz,
            "$%s\n======================\nO Offset:Length:  %03X:%03X\nA Offset:Length:  %03X:%03X\nType:             %s%s\n%sAttribute ID:     %i\n",
            NTFS_ATTR_TYPE_NAME_STR[iStr],
            oA, pA->Length,
            (pA->fNonResident ? 0 : pA->AttrOffset), (pA->fNonResident ? 0 : pA->AttrLength),
            (pA->fNonResident ? "Non-Resident" : "Resident"),
            (pA->NameLength ? ", Named" : ""),
            uszAttributeName,
            pA->AttrId
        );
        // only parse resident attributes below:
        if(pA->fNonResident || (pA->Length < pA->AttrOffset + pA->AttrLength)) {
            csz += snprintf(sz + csz, M_NTFS_READINFOSINGLE_BUFFER - csz, "\n\n");
            oA += pA->Length;
            continue;
        }
        // $STANDARD_INFORMATION
        if((pA->Type == NTFS_ATTR_TYPE_STANDARD_INFORMATION) && ((pA->AttrLength >= sizeof(NTFS_STANDARD_INFORMATION)) || pA->AttrLength == NTFS_STANDARD_INFORMATION_LEN_PREWIN2K)) {
            pSI = (PNTFS_STANDARD_INFORMATION)(pbr + oA + pA->AttrOffset);
            Util_FileTime2String(pSI->TimeCreate, szTimeC);
            Util_FileTime2String(pSI->TimeAlter, szTimeA);
            Util_FileTime2String(pSI->TimeModify, szTimeM);
            Util_FileTime2String(pSI->TimeRead, szTimeR);
            csz += snprintf(sz + csz, M_NTFS_READINFOSINGLE_BUFFER - csz,
                "---\nTime File Create: %s\nTime File Alter:  %s\nTime File Read:   %s\nTime MFT Change:  %s\nFile Permissions: %s%s%s%s%s%s%s%s%s%s%s%s",
                szTimeC, szTimeA, szTimeR, szTimeM,
                (pSI->DosFilePermissions & NTFS_STDINFO_PERMISSION_READONLY ? "ReadOnly, " : ""),
                (pSI->DosFilePermissions & NTFS_STDINFO_PERMISSION_HIDDEN ? "Hidden, " : ""),
                (pSI->DosFilePermissions & NTFS_STDINFO_PERMISSION_SYSTEM ? "System, " : ""),
                (pSI->DosFilePermissions & NTFS_STDINFO_PERMISSION_ARCHIVE ? "Archive, " : ""),
                (pSI->DosFilePermissions & NTFS_STDINFO_PERMISSION_DEVICE ? "Device, " : ""),
                (pSI->DosFilePermissions & NTFS_STDINFO_PERMISSION_TEMPORARY ? "Temporary, " : ""),
                (pSI->DosFilePermissions & NTFS_STDINFO_PERMISSION_SPARSE ? "Sparse, " : ""),
                (pSI->DosFilePermissions & NTFS_STDINFO_PERMISSION_REPARSE ? "Reparse, " : ""),
                (pSI->DosFilePermissions & NTFS_STDINFO_PERMISSION_COMPRESSED ? "Compressed, " : ""),
                (pSI->DosFilePermissions & NTFS_STDINFO_PERMISSION_OFFLINE ? "Offline, " : ""),
                (pSI->DosFilePermissions & NTFS_STDINFO_PERMISSION_NOINDEX ? "NoIndex, " : ""),
                (pSI->DosFilePermissions & NTFS_STDINFO_PERMISSION_ENCRYPTED ? "Encrypted, " : "")
            );
        }
        // $FILE_NAME
        if(pA->Type == NTFS_ATTR_TYPE_FILE_NAME) {
            pFN = (PNTFS_FILE_NAME)(pbr + oA + pA->AttrOffset);
            if(pA->AttrLength >= 42 + pFN->NameLength * sizeof(WCHAR)) {
                CharUtil_WtoU((LPWSTR)pFN->Name, pFN->NameLength, pbBuffer, sizeof(pbBuffer), &uszRecordFileName, NULL, CHARUTIL_FLAG_TRUNCATE);
                Util_FileTime2String(pFN->TimeCreate, szTimeC);
                Util_FileTime2String(pFN->TimeAlter, szTimeA);
                Util_FileTime2String(pFN->TimeModify, szTimeM);
                Util_FileTime2String(pFN->TimeRead, szTimeR);
                csz += snprintf(sz + csz, M_NTFS_READINFOSINGLE_BUFFER - csz,
                    "---\nName:             %s\nName Space:       %s\nParent Directory: %llX:%llX\nSize Real:        %lli\nSize Allocated:   %lli\nTime File Create: %s\nTime File Alter:  %s\nTime File Read:   %s\nTime MFT Change:  %s",
                    uszRecordFileName,
                    NTFS_FILENAME_NAMESPACE_NAME_STR[min(pFN->NameSpace, NTFS_FILENAME_NAMESPACE_MAX)],
                    (QWORD)pFN->ParentDirectory.SegmentNumber, (QWORD)pFN->ParentDirectory.SequenceNumber,
                    pFN->SizeReal,
                    pFN->SizeAllocated,
                    szTimeC, szTimeA, szTimeR, szTimeM
                );
            }
        }
        // $DATA
        if(pA->Type == NTFS_ATTR_TYPE_DATA) {
            cszHexAscii = sizeof(szHexAscii) - 2;
            if(Util_FillHexAscii(pbr + oA + pA->AttrOffset, pA->AttrLength, 0, szHexAscii, &cszHexAscii)) {
                csz += snprintf(sz + csz, M_NTFS_READINFOSINGLE_BUFFER - csz, "---\n%s", szHexAscii);
            }
        }
        // $OBJECT_ID
        if((pA->Type == NTFS_ATTR_TYPE_OBJECT_ID) && (pA->AttrLength >= sizeof(NTFS_OBJECT_ID))) {
            pOID = (PNTFS_OBJECT_ID)(pbr + oA + pA->AttrOffset);
            Util_GuidToString(pOID->ObjectId, szGuid1);
            Util_GuidToString(pOID->BirthVolumeId, szGuid2);
            Util_GuidToString(pOID->BirthObjectId, szGuid3);
            Util_GuidToString(pOID->DomainId, szGuid4);
            csz += snprintf(sz + csz, M_NTFS_READINFOSINGLE_BUFFER - csz,
                "---\nObject ID:        {%s}\nBirth Volume ID:  {%s}\nBirth Object ID:  {%s}\nDomain ID:        {%s}",
                szGuid1, szGuid2, szGuid3, szGuid4
            );
        }
        csz += snprintf(sz + csz, M_NTFS_READINFOSINGLE_BUFFER - csz, "\n\n\n");
        oA += pA->Length;
    }
fail:
    *pcsz = (DWORD)csz;
    return sz;
}

NTSTATUS FcNtfs2_ReadInfoAll(_In_ VMM_HANDLE H, _Out_ PBYTE pb, _In_ DWORD cb, _Out_ PDWORD pcbRead, _In_ QWORD cbOffset)
{
    NTSTATUS nt = VMMDLL_STATUS_FILE_INVALID;
    PFC_MAP_NTFSENTRY pe;
    PFCOB_MAP_NTFS pObNtfsMap = NULL;
    QWORD i, o, qwIdBase, qwIdTop, cId, cszuBuffer, cbOffsetBuffer;
    LPSTR szuBuffer = NULL;
    CHAR szTimeCreate[24], szTimeModify[24];
    if(!FcNtfs2_GetIdFromPosition(H, cbOffset, FALSE, &qwIdBase)) { goto fail; }
    if(!FcNtfs2_GetIdFromPosition(H, cbOffset + cb, FALSE, &qwIdTop)) { goto fail; }
    cId = min(cb / M_NTFS_INFO_LINELENGTH_UTF8, qwIdTop - qwIdBase) + 1;
    if(!FcNtfs2_Map_GetFromIdRange(H, qwIdBase, cId, &pObNtfsMap) || !pObNtfsMap->cMap) { goto fail; }
    cbOffsetBuffer = pObNtfsMap->pMap[0].cszuOffset;
    if((cbOffsetBuffer > cbOffset) || (cbOffset - cbOffsetBuffer > 0x10000)) { goto fail; }
    cszuBuffer = 0x01000000;
    if(!(szuBuffer = LocalAlloc(0, (SIZE_T)cszuBuffer))) { goto fail; }
    for(i = 0, o = 0; (i < pObNtfsMap->cMap) && (o < cszuBuffer - 0x1000); i++) {
        pe = pObNtfsMap->pMap + i;
        Util_FileTime2String(pe->ftCreate, szTimeCreate);
        Util_FileTime2String(pe->ftModify, szTimeModify);
        o += snprintf(
            szuBuffer + o,
            (SIZE_T)(cszuBuffer - o),
            "%6llx%12llx%8x%8x %s : %s %12llx %c%c%c%c %s\n",
            pe->qwId,
            pe->pa,
            (pe->dwMftId & 0x80000000 ? 0 : pe->dwMftId),
            (pe->dwMftIdParent & 0x80000000 ? 0 : pe->dwMftIdParent),
            szTimeCreate,
            szTimeModify,
            pe->qwFileSize,
            (pe->flags & FCNTFS2_FLAG_ACTIVE) ? 'A' : ' ',
            (pe->flags & FCNTFS2_FLAG_RESIDENT) ? 'R' : ' ',
            (pe->flags & FCNTFS2_FLAG_ADS) ? 'S' : ' ',
            (pe->flags & FCNTFS2_FLAG_DIRECTORY) ? 'D' : ' ',
            pe->uszText
        );
    }
    nt = Util_VfsReadFile_FromPBYTE(szuBuffer, o, pb, cb, pcbRead, cbOffset - cbOffsetBuffer);
fail:
    LocalFree(szuBuffer);
    Ob_DECREF(pObNtfsMap);
    return nt;
}

/*
* Check if the path contains the meta info directory '$_INFO' and also if
* it points to a file '\mftinfo.txt' / '\mftdata.mem' / '\mftdata.bin'.
* If so strip this info.
* -- H
* -- wszPath
* -- uszPathStripped
* -- pfMeta = path contains '\\$_INFO'
* -- pfEnd = path ends with '\\$_INFO'
* -- pfTxt = path ends with '\\mftinfo.txt'
* -- pfMem = path ends with '\\mftdata.mem'
* -- pfBin = path ends with '\\mftfile.bin'
*/
VOID FcNtfs2_PathStripMftInfo(_In_ VMM_HANDLE H, _In_ LPSTR uszPath, _Out_writes_(MAX_PATH) LPSTR uszPathStripped, _Out_opt_ PBOOL pfMeta, _Out_opt_ PBOOL pfEnd, _Out_opt_ PBOOL pfTxt, _Out_opt_ PBOOL pfMem, _Out_opt_ PBOOL pfBin)
{
    QWORD cch;
    LPSTR usz;
    if(pfMeta) { *pfMeta = FALSE; }
    if(pfTxt) { *pfTxt = FALSE; }
    if(pfMem) { *pfMem = FALSE; }
    if(pfBin) { *pfBin = FALSE; }
    if(pfEnd) { *pfEnd = CharUtil_StrEndsWith(uszPath, "\\$_INFO", TRUE); }
    strncpy_s(uszPathStripped, MAX_PATH, uszPath, _TRUNCATE);
    if(!(usz = strstr(uszPath, "\\$_INFO"))) { return; }
    cch = (QWORD)usz - (QWORD)uszPath;
    strncpy_s(uszPathStripped + cch, MAX_PATH - (DWORD)cch, usz + 7, _TRUNCATE);
    if(CharUtil_StrEndsWith(uszPathStripped, "\\mftinfo.txt", TRUE)) {
        if(pfTxt) { *pfTxt = TRUE; }
        uszPathStripped[strlen(uszPathStripped) - 12] = 0;
    }
    if(CharUtil_StrEndsWith(uszPathStripped, "\\mftdata.mem", TRUE)) {
        if(pfMem) { *pfMem = TRUE; }
        uszPathStripped[strlen(uszPathStripped) - 12] = 0;
    }
    if(CharUtil_StrEndsWith(uszPathStripped, "\\mftfile.bin", TRUE)) {
        if(pfBin) { *pfBin = TRUE; }
        uszPathStripped[strlen(uszPathStripped) - 12] = 0;
    }
    if(pfMeta) { *pfMeta = TRUE; }
}

NTSTATUS FcNtfs2_Read(_In_ VMM_HANDLE H, _In_ PVMMDLL_PLUGIN_CONTEXT ctxP, _Out_writes_to_(cb, *pcbRead) PBYTE pb, _In_ DWORD cb, _Out_ PDWORD pcbRead, _In_ QWORD cbOffset)
{
    NTSTATUS nt = VMMDLL_STATUS_FILE_INVALID;
    PFCOB_MAP_NTFS pObNtfsMap = NULL;
    PFC_MAP_NTFSENTRY peNtfs;
    QWORD qwHashPath;
    BYTE pbNtfsRecordMax[0x400];
    BOOL fMeta, fMetaTxt, fMetaMem, fMetaBin;
    CHAR uszPathStripped[MAX_PATH];
    PBYTE pbInfoTxt;
    DWORD cbInfoTxt;
    if(!strcmp(ctxP->uszPath, "ntfs_files.txt")) {
        return FcNtfs2_ReadInfoAll(H, pb, cb, pcbRead, cbOffset);
    }
    FcNtfs2_PathStripMftInfo(H, ctxP->uszPath, uszPathStripped, &fMeta, NULL, &fMetaTxt, &fMetaMem, &fMetaBin);
    qwHashPath = CharUtil_HashPathFsU(uszPathStripped);
    if(FcNtfs2_Map_GetFromHash(H, qwHashPath, &pObNtfsMap) && pObNtfsMap->cMap) {
        peNtfs = pObNtfsMap->pMap + 0;
        if(!fMeta || fMetaBin) {
            if(peNtfs->qwFileSize && (peNtfs->flags & FCNTFS2_FLAG_RESIDENT) && (peNtfs->qwFileSize < 0x400)) {
                if(FcNtfs2_GetMftResidentData(H, peNtfs, pbNtfsRecordMax, (DWORD)peNtfs->qwFileSize, NULL)) {
                    nt = Util_VfsReadFile_FromPBYTE(pbNtfsRecordMax, peNtfs->qwFileSize, pb, cb, pcbRead, cbOffset);
                }
            } else {
                nt = Util_VfsReadFile_FromZERO(peNtfs->qwFileSize, pb, cb, pcbRead, cbOffset);
            }
        }
        if(fMetaMem) {
            VmmRead2(H, NULL, peNtfs->pa, pbNtfsRecordMax, 0x400, VMM_FLAG_ZEROPAD_ON_FAIL);
            nt = Util_VfsReadFile_FromPBYTE(pbNtfsRecordMax, 0x400, pb, cb, pcbRead, cbOffset);
        }
        if(fMetaTxt && (pbInfoTxt = FcNtfs2_ReadInfoSingle(H, peNtfs, &cbInfoTxt))) {
            nt = Util_VfsReadFile_FromPBYTE(pbInfoTxt, cbInfoTxt, pb, cb, pcbRead, cbOffset);
            LocalFree(pbInfoTxt);
        }
    }
    Ob_DECREF(pObNtfsMap);
    return nt;
}

VOID FcNtfs2_ListDirectory(_In_ VMM_HANDLE H, _In_ LPSTR uszPath, _Inout_ PHANDLE pFileList)
{
    BOOL fMeta, fEnd, fTxt, fMem, fBin;
    PBYTE pbInfoTxt;
    DWORD i, cbInfoTxt;
    PFC_MAP_NTFSENTRY pe;
    PFCOB_MAP_NTFS pObNtfsMap = NULL;
    VMMDLL_VFS_FILELIST_EXINFO FileExInfo = { 0 };
    CHAR uszNameFix[2 * MAX_PATH];
    LPCSTR uszTextName;
    QWORD qwHashPath;
    FileExInfo.dwVersion = VMMDLL_VFS_FILELIST_EXINFO_VERSION;
    FcNtfs2_PathStripMftInfo(H, uszPath, uszNameFix, &fMeta, &fEnd, &fTxt, &fMem, &fBin);
    if(fTxt || fMem || fBin) { return; }
    qwHashPath = CharUtil_HashPathFsU(uszNameFix);
    // single mft entry metadata files
    if(fMeta && !fEnd) {
        if(FcNtfs2_Map_GetFromHash(H, qwHashPath, &pObNtfsMap) && pObNtfsMap->cMap) {
            pe = pObNtfsMap->pMap;
            if(pe->pa) {
                FileExInfo.qwCreationTime = pe->ftCreate;
                FileExInfo.qwLastWriteTime = pe->ftModify;
                FileExInfo.qwLastAccessTime = pe->ftRead;
                if((pbInfoTxt = FcNtfs2_ReadInfoSingle(H, pe, &cbInfoTxt))) {
                    VMMDLL_VfsList_AddFile(pFileList, "mftinfo.txt", cbInfoTxt, &FileExInfo);
                    LocalFree(pbInfoTxt);
                }
                VMMDLL_VfsList_AddFile(pFileList, "mftdata.mem", 0x400, &FileExInfo);
                if(pe->flags & FCNTFS2_FLAG_RESIDENT) {
                    VMMDLL_VfsList_AddFile(pFileList, "mftfile.bin", pe->qwFileSize, &FileExInfo);
                }
            }
        }
        Ob_DECREF(pObNtfsMap);
        return;
    }
    // ordinary directory or metadata directory
    if(!FcNtfs2_Map_GetFromHashParent(H, qwHashPath, &pObNtfsMap)) { return; }
    if(!fMeta && uszPath[0] && pObNtfsMap->cMap) {
        VMMDLL_VfsList_AddDirectory(pFileList, "$_INFO", NULL);
    }
    for(i = 0; i < pObNtfsMap->cMap; i++) {
        pe = pObNtfsMap->pMap + i;
        uszTextName = CharUtil_PathSplitLast(pe->uszText);
        if(!CharUtil_FixFsName(uszNameFix, sizeof(uszNameFix), uszTextName, NULL, NULL, -1, pe->dwTextSeq, FALSE)) { continue; }
        FileExInfo.qwCreationTime = pe->ftCreate;
        FileExInfo.qwLastWriteTime = pe->ftModify;
        FileExInfo.qwLastAccessTime = pe->ftRead;
        if((pe->flags & FCNTFS2_FLAG_DIRECTORY) || fMeta) {
            if(pe->pa || !fMeta) {
                FileExInfo.fCompressed = FALSE;
                VMMDLL_VfsList_AddDirectory(pFileList, uszNameFix, &FileExInfo);
            }
        } else {
            FileExInfo.fCompressed = (pe->flags & FCNTFS2_FLAG_RESIDENT) ? TRUE : FALSE;
            VMMDLL_VfsList_AddFile(pFileList, uszNameFix, pe->qwFileSize, &FileExInfo);
        }
    }
    Ob_DECREF(pObNtfsMap);
}

BOOL FcNtfs2_List(_In_ VMM_HANDLE H, _In_ PVMMDLL_PLUGIN_CONTEXT ctxP, _Inout_ PHANDLE pFileList)
{
    QWORD cbFileSizeUTF8;
    FcNtfs2_ListDirectory(H, ctxP->uszPath, pFileList);
    if(!ctxP->uszPath[0] && FcNtfs2_GetFileSize(H, NULL, &cbFileSizeUTF8, NULL)) {
        VMMDLL_VfsList_AddFile(pFileList, "ntfs_files.txt", cbFileSizeUTF8, NULL);
    }
    return TRUE;
}

VOID FcNtfs2_Notify(_In_ VMM_HANDLE H, _In_ PVMMDLL_PLUGIN_CONTEXT ctxP, _In_ DWORD fEvent, _In_opt_ PVOID pvEvent, _In_opt_ DWORD cbEvent)
{
    if(fEvent == VMMDLL_PLUGIN_NOTIFY_FORENSIC_INIT_COMPLETE) {
        PluginManager_SetVisibility(H, TRUE, "\\forensic\\ntfs", TRUE);
    }
}

VOID M_FcNtfs_Initialize(_In_ VMM_HANDLE H, _Inout_ PVMMDLL_PLUGIN_REGINFO pRI)
{
    if((pRI->magic != VMMDLL_PLUGIN_REGINFO_MAGIC) || (pRI->wVersion != VMMDLL_PLUGIN_REGINFO_VERSION)) { return; }
    if((pRI->tpSystem != VMMDLL_SYSTEM_WINDOWS_64) && (pRI->tpSystem != VMMDLL_SYSTEM_WINDOWS_32)) { return; }
    strcpy_s(pRI->reg_info.uszPathName, 128, "\\forensic\\ntfs");               // module name
    pRI->reg_info.fRootModule = TRUE;                                           // module shows in root directory
    pRI->reg_info.fRootModuleHidden = TRUE;                                     // module hidden by default
    pRI->reg_fn.pfnList = FcNtfs2_List;                                         // List function supported
    pRI->reg_fn.pfnRead = FcNtfs2_Read;                                         // Read function supported
    pRI->reg_fn.pfnNotify = FcNtfs2_Notify;                                     // Notify function supported
    pRI->reg_fnfc.pfnInitialize = FcNtfs2_FcInitialize;                         // Forensic initialize function supported
    pRI->reg_fnfc.pfnIngestPhysmem = FcNtfs2_FcIngestPhysmem;                   // Forensic physmem ingest supported
    pRI->reg_fnfc.pfnIngestFinalize = FcNtfs2_FcIngestFinalize;                 // Forensic ingest finalize function supported
    pRI->reg_fnfc.pfnTimeline = FcNtfs2_FcTimeline;                             // Forensic timelining supported
    pRI->reg_fnfc.pfnFinalize = FcNtfs2_FcFinalize;                             // Forensic finalize function supported
    memcpy(pRI->reg_info.sTimelineNameShort, "NTFS", 5);
    strncpy_s(pRI->reg_info.uszTimelineFile, 32, "timeline_ntfs", _TRUNCATE);
    pRI->pfnPluginManager_Register(H, pRI);
}
