// 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-2023
// Author: Ulf Frisk, pcileech@frizk.net
//

#include "modules.h"

#define M_NTFS_INFO_LINELENGTH_UTF8          98
#define M_NTFS_INFO_LINELENGTH_JSON          98

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, mft_flags INTEGER, depth INTEGER, size_file INTEGER, size_fileres 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;

#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:
//-----------------------------------------------------------------------------

typedef struct tdFCNTFS {
    QWORD pa;
    QWORD va;
    QWORD ftCreate;
    QWORD ftModify;
    QWORD ftRead;
    QWORD cbFileSize;
    DWORD dwMftRecordNumber;
    BOOL fSynthenticDir;                // fake "synthentic" directory
    BOOL fRootDir;
    BOOL fActive;
    BOOL fDir;
    WORD Flags;
    WORD cbFileSizeMftResident;
    WORD wMftSequenceNumber;
    // setup data
    struct {
        DWORD dwParentRecordNumber;
        WORD  wParentSeqenceNumber;
        struct tdFCNTFS *pNextDir;      // next directory with same RecordNumber (in case of multiple)
    } Setup;
    struct tdFCNTFS *pParent;           // parent entry [not counted as reference]
    struct tdFCNTFS *pChild;            // 1st child entry [not counted as reference]
    struct tdFCNTFS *pSibling;          // sibling entry list [not counted as reference]
    QWORD qwHashThis;
    WORD wName_SeqNbr;                  // collision counter for wszName (in same directory)
    WORD iDirDepth;                     // directory depth from GlobalRoot
    DWORD iMap;                         // index (after setup)
    // initial general create attributes - name
    CHAR uszName[0];
} FCNTFS, *PFCNTFS;

typedef struct tdFCNTFS_SETUP_CONTEXT {
    POB_MAP pmDuplicate;    // duplicate checks
    POB_SET psRoot;         // root '.' entries
    POB_SET psDirFile;      // non-root entries
    POB_MAP pmDir;          // directory map
    POB_SET psOrphan;       // orphan entries
} FCNTFS_SETUP_CONTEXT, *PFCNTFS_SETUP_CONTEXT;

typedef struct tdFCNTFS_FINALIZE_CONTEXT {
    sqlite3 *hSql;
    sqlite3_stmt *st;
    sqlite3_stmt *st_str;
    QWORD cbUtf8Total;
    QWORD cbJsonTotal;
} FCNTFS_FINALIZE_CONTEXT, *PFCNTFS_FINALIZE_CONTEXT;



//-----------------------------------------------------------------------------
// NTFS MFT ANALYSIS / INITIALIZATION FUNCTIONALITY BELOW:
// The NTFS MFT analysis heavily builds upon the analysis of physical memory
// which is analyzed page-by-page as the forensic sub-system is doing its
// physical memory scan. The results of the NTFS MFT entries gathered during
// the physical memory scan phase is then analyzed and pieced together using
// a best-effort algorithm into somewhat valid file systems.
// NB! artifacts will be missing from memory and best-effort guesses will be
//     made. This may result in files/directories being missed or assembled
//     towards another file system. Manual analysis of the MFT entries are
//     recommended for more correct forensics.
// NB! the code below is somewhat messy and should be cleaned up; but it's
//     working at the moment and provides the functionality needed.
//-----------------------------------------------------------------------------

/*
* Close and clean up PFCNTFS_SETUP_CONTEXT.
* -- ctx
*/
VOID FcNtfs_Close(_Frees_ptr_opt_ PFCNTFS_SETUP_CONTEXT ctx)
{
    if(ctx) {
        Ob_DECREF(ctx->pmDuplicate);
        Ob_DECREF(ctx->psRoot);
        Ob_DECREF(ctx->psDirFile);
        Ob_DECREF(ctx->pmDir);
        Ob_DECREF(ctx->psOrphan);
        LocalFree(ctx);
    }
}

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

/*
* Initialize a new empty PFCNTFS_SETUP_CONTEXT.
* -- return = the initialized context, or NULL on fail.
*/
PVOID FcNtfs_Initialize(_In_ VMM_HANDLE H, _In_ PVMMDLL_PLUGIN_CONTEXT ctxP)
{
    PFCNTFS_SETUP_CONTEXT ctx;
    Fc_SqlExec(H, FC_SQL_SCHEMA_NTFS);
    if(!(ctx = LocalAlloc(LMEM_ZEROINIT, sizeof(FCNTFS_SETUP_CONTEXT)))) { goto fail; }
    if(!(ctx->pmDuplicate = ObMap_New(H, OB_MAP_FLAGS_OBJECT_LOCALFREE))) { goto fail; }
    if(!(ctx->psRoot = ObSet_New(H))) { goto fail; }
    if(!(ctx->psDirFile = ObSet_New(H))) { goto fail; }
    if(!(ctx->pmDir = ObMap_New(H, 0))) { goto fail; }
    if(!(ctx->psOrphan = ObSet_New(H))) { goto fail; }
    return ctx;
fail:
    FcNtfs_Close(ctx);
    return NULL;
}

/*
* Try add a single MFT entry to the NTFS MFT dataset.
* -- H
* -- ctx
* -- qwPhysicalAddress
* -- qwVirtualAddress
* -- pb
*/
VOID FcNtfs_IngestMftEntry(_In_ VMM_HANDLE H, _In_ PFCNTFS_SETUP_CONTEXT ctx, _In_ QWORD qwPhysicalAddress, _In_opt_ QWORD qwVirtualAddress, _In_reads_(0x400) PBYTE pb)
{
    QWORD qwHashDuplicateCheck;
    DWORD oA, cbData = 0, cbuName;
    PNTFS_FILE_RECORD pr;
    PNTFS_ATTR pa;
    PNTFS_FILE_NAME pfnC, pfn = NULL;
    PNTFS_STANDARD_INFORMATION psi = NULL;
    PFCNTFS pNtfs = NULL, pNtfs_Coll = NULL;
    pr = (PNTFS_FILE_RECORD)pb;
    // Check MFT record number is within the correct location inside the page:
    if((((qwPhysicalAddress >> 10) & 0x3) != (0x3 & pr->MftRecordNumber)) || (pr->MftRecordNumber == 0)) { 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->Length < pa->AttrOffset + pa->AttrLength) { break; }
        if(!pa->fNonResident && (pa->Type == NTFS_ATTR_TYPE_DATA)) {
            cbData = pa->AttrLength;
        }
        if(pa->Type == NTFS_ATTR_TYPE_STANDARD_INFORMATION) {
            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) {
            pfnC = (PNTFS_FILE_NAME)(pb + oA + pa->AttrOffset);
            if((pfnC->NameSpace != NTFS_FILENAME_NAMESPACE_DOS) && (pa->AttrLength >= 42 + pfnC->NameLength * sizeof(WCHAR))) {
                if(!pfn || (pfnC->SizeReal > pfn->SizeAllocated)) {
                    pfn = pfnC;
                }
            }
        }
        oA += pa->Length;
    }
    if(!psi || !pfn || (pfn->ParentDirectory.SegmentNumber > 0xfffffff0)) { return; }
    // Duplicate check by MftRecordNumber and LogFileSequenceNumber:
    qwHashDuplicateCheck = (((QWORD)pr->MftRecordNumber << 32) ^ pr->LogFileSequenceNumber);
    if(ObMap_ExistsKey(ctx->pmDuplicate, qwHashDuplicateCheck)) { return; }
    // Create NTFS object and populate:
    if(!pfn->NameLength) { return; }
    if(!CharUtil_WtoU(pfn->Name, pfn->NameLength, NULL, 0, NULL, &cbuName, 0)) { return; }
    if(!(pNtfs = LocalAlloc(LMEM_ZEROINIT, sizeof(FCNTFS) + cbuName))) { return; }
    if(!CharUtil_WtoU(pfn->Name, pfn->NameLength, pNtfs->uszName, cbuName, NULL, &cbuName, CHARUTIL_FLAG_STR_BUFONLY)) {
        LocalFree(pNtfs);
        return;
    }
    pNtfs->pa = qwPhysicalAddress;
    pNtfs->va = qwVirtualAddress;
    pNtfs->ftCreate = psi->TimeCreate;
    pNtfs->ftModify = psi->TimeModify;
    pNtfs->ftRead = psi->TimeRead;
    pNtfs->cbFileSize = max(cbData, pfn->SizeReal);
    pNtfs->dwMftRecordNumber = pr->MftRecordNumber;
    pNtfs->fActive = (pr->Flags & 1) ? TRUE : FALSE;
    pNtfs->fDir = (pr->Flags & 2) ? TRUE : FALSE;
    pNtfs->Flags = pr->Flags;
    pNtfs->cbFileSizeMftResident = (WORD)cbData;
    pNtfs->wMftSequenceNumber = pr->SequenceNumber;
    pNtfs->Setup.dwParentRecordNumber = (DWORD)pfn->ParentDirectory.SegmentNumber;
    pNtfs->Setup.wParentSeqenceNumber = (WORD)pfn->ParentDirectory.SequenceNumber;
    // Commit:
    ObMap_Push(ctx->pmDuplicate, qwHashDuplicateCheck, pNtfs);
    if(pNtfs->fDir) {
        if(pNtfs->dwMftRecordNumber == pNtfs->Setup.dwParentRecordNumber) {
            pNtfs->fRootDir = TRUE;
            ObSet_Push(ctx->psRoot, (QWORD)pNtfs);
        }
        if(!ObMap_Push(ctx->pmDir, (QWORD)pNtfs->dwMftRecordNumber, pNtfs)) {
            // fail == already exists -> queue on NextDir list
            if((pNtfs_Coll = ObMap_GetByKey(ctx->pmDir, (QWORD)pNtfs->dwMftRecordNumber))) {
                pNtfs->Setup.pNextDir = pNtfs_Coll->Setup.pNextDir;
                pNtfs_Coll->Setup.pNextDir = pNtfs;
            }
        }
    }
    if(!pNtfs->fRootDir) {
        ObSet_Push(ctx->psDirFile, (QWORD)pNtfs);
    }
    // Debug output:
    VmmLog(H, MID_FORENSIC, LOGLEVEL_6_TRACE, "   %08x:%04x %12llx %8lli : %c : %s",
        pNtfs->Setup.dwParentRecordNumber,
        pNtfs->Setup.wParentSeqenceNumber,
        pNtfs->pa,
        pNtfs->cbFileSize,
        (pNtfs->fDir ? 'D' : ' '),
        pNtfs->uszName
    );
}

/*
* Try add a physical memory page to the NTFS MFT dataset.
* -- ctx
* -- pa
* -- pbPage
*/
VOID FcNtfs_IngestMftPage(_In_ VMM_HANDLE H, _In_ PFCNTFS_SETUP_CONTEXT ctx, _In_ QWORD pa, _In_reads_(0x1000) PBYTE pbPage)
{
    QWORD i, va = 0;
    PNTFS_FILE_RECORD pr;
    if(*(PDWORD)pbPage != 'ELIF') { return; }     // file signature
    for(i = 0; i < 0x1000; i += 0x400) {
        pr = (PNTFS_FILE_RECORD)(pbPage + i);
        if(pr->Signature != 'ELIF') { continue; }
        if((pr->UpdateSequenceArrayOffset > 0x100) || (pr->UpdateSequenceArraySize > 0x100)) { continue; }
        if(pr->BaseFileRecordSegment.SegmentNumber) { continue; }
        if(pr->FirstAttributeOffset > 0x300) { continue; }
        FcNtfs_IngestMftEntry(H, ctx, pa + i, (va ? va + i : 0), pbPage + i);
    }
}

/*
* 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 FcNtfs_IngestGetValidAddrMap(_In_ VMM_HANDLE H, _In_ PVMMDLL_PLUGIN_FORENSIC_INGEST_PHYSMEM pc)
{
    BOOL fPfnValidForMft;
    DWORD i;
    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) && (*(PDWORD)pc->ppMEMs[i]->pb == 'ELIF')) {
            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 file candidates
* and add any found to the internal data sets.
* -- H
* -- ctxfc
* -- pIngestPhysmem
*/
VOID FcNtfs_IngestPhysmem(_In_ VMM_HANDLE H, _In_opt_ PVOID ctxfc, _In_ PVMMDLL_PLUGIN_FORENSIC_INGEST_PHYSMEM pIngestPhysmem)
{
    QWORD pa;
    PBYTE pb;
    POB_MAP pmObAddr = NULL;
    PFCNTFS_SETUP_CONTEXT ctx = (PFCNTFS_SETUP_CONTEXT)ctxfc;
    if(ctx && (pmObAddr = FcNtfs_IngestGetValidAddrMap(H, pIngestPhysmem))) {
        while((pb = ObMap_PopWithKey(pmObAddr, &pa))) {
            FcNtfs_IngestMftPage(H, ctx, pa, pb);
        }
    }
    Ob_DECREF(pmObAddr);
}

/*
* Calculate a validity score for merge suitability. Score ranges from 0-100
* where 100 is a perfect match, while 0 is invalid/forbidden match.
* -- pdir
* -- pe
* -- return = match score 0-100.
*/
DWORD FcNtfs_FinalizeMergeScore(_In_ PFCNTFS pdir, _In_ PFCNTFS pe)
{
    DWORD dwScore;
    // 1: calculate validity score
    if(pe->Setup.wParentSeqenceNumber > pdir->wMftSequenceNumber) { return 0; }
    if(pe->fActive)
    {
        if(!pe->fActive) { return 0; }
        dwScore = 100 - min(99, pdir->wMftSequenceNumber - pe->Setup.wParentSeqenceNumber);
    } else {
        dwScore = (pdir->fActive ? 100 : 50) - min(49, pdir->wMftSequenceNumber - pe->Setup.wParentSeqenceNumber);
    }
    // 2: loop protect when attach dir
    if(pe->fDir) {
        while((pe = pe->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.
*/
PFCNTFS FcNtfs_FinalizeMergeFind(_In_ PFCNTFS_SETUP_CONTEXT ctx, _In_ PFCNTFS pe)
{
    PFCNTFS pDir, pMergeDir = NULL;;
    DWORD dwMergeScoreThis, dwMergeScoreMax = 0;
    if((pDir = ObMap_GetByKey(ctx->pmDir, (QWORD)pe->Setup.dwParentRecordNumber))) {
        while(pDir && (dwMergeScoreMax < 100)) {
            dwMergeScoreThis = FcNtfs_FinalizeMergeScore(pDir, pe);
            if(dwMergeScoreThis > dwMergeScoreMax) {
                dwMergeScoreMax = dwMergeScoreThis;
                pMergeDir = pDir;
            }
            pDir = pDir->Setup.pNextDir;
        }
    }
    return pMergeDir;
}

/*
* Merge entries onto already existing directories, or set as orphan.
*/
VOID FcNtfs_FinalizeMerge1(_In_ PFCNTFS_SETUP_CONTEXT ctx)
{
    PFCNTFS pe, pParent;
    while((pe = (PFCNTFS)ObSet_Pop(ctx->psDirFile))) {
        if((pParent = FcNtfs_FinalizeMergeFind(ctx, pe))) {
            pe->pParent = pParent;
            pe->pSibling = pParent->pChild;
            pParent->pChild = pe;
        } else {
            ObSet_Push(ctx->psOrphan, (QWORD)pe);
        }
    }
}

/*
* Create a "synthentic" root directory
*/
PFCNTFS FcNtfs_FinalizeCreateSynthenticDir(_In_ PFCNTFS_SETUP_CONTEXT ctx, _In_ DWORD dwMftRecordNumber, _In_ LPSTR uszName, _In_ BOOL fRootDir)
{
    PFCNTFS pNtfs, pNtfs_Coll;
    SIZE_T cuszName = strlen(uszName);
    if(!(pNtfs = LocalAlloc(LMEM_ZEROINIT, sizeof(FCNTFS) + cuszName + 1))) { return NULL; }
    memcpy(pNtfs->uszName, uszName, cuszName + 1);
    pNtfs->dwMftRecordNumber = dwMftRecordNumber;
    pNtfs->wMftSequenceNumber = (WORD)-1;
    pNtfs->fSynthenticDir = TRUE;
    pNtfs->fRootDir = fRootDir;
    pNtfs->fDir = TRUE;
    pNtfs->Flags = 0x03;    // active directory
    if(!ObMap_Push(ctx->pmDir, (QWORD)pNtfs->dwMftRecordNumber, pNtfs)) {
        // fail == already exists -> queue on NextDir list
        if((pNtfs_Coll = ObMap_GetByKey(ctx->pmDir, (QWORD)pNtfs->dwMftRecordNumber))) {
            pNtfs->Setup.pNextDir = pNtfs_Coll->Setup.pNextDir;
            pNtfs_Coll->Setup.pNextDir = pNtfs;
        }
    }
    if(pNtfs->fRootDir) {
        ObSet_Push(ctx->psRoot, (QWORD)pNtfs);
    }
    ObMap_Push(ctx->pmDuplicate, ((QWORD)dwMftRecordNumber << 32 | dwMftRecordNumber), pNtfs);  // for later free
    return pNtfs;
}

/*
* Merge orphan entries and return the global root
* -- H
* -- ctx
* -- return = the global root, or null if fail
*/
PFCNTFS FcNtfs_FinalizeMerge2(_In_ VMM_HANDLE H, _In_ PFCNTFS_SETUP_CONTEXT ctx)
{
    PFCNTFS pe, pRootOrphan, pRootGlobal = NULL;
    PDWORD pdwRecordNumberArray = NULL;
    DWORD cMax = 0, i, c = 0, dwLastRecord = -1, iDirCount = 0;
    CHAR uszDirName[16];
    // 1: Generate sorted array of ParentRecordNumber and move Orphan into DirFile Set.
    if(!(pdwRecordNumberArray = LocalAlloc(0, sizeof(DWORD) * ObSet_Size(ctx->psOrphan)))) { goto fail; }
    while((pe = (PFCNTFS)ObSet_Pop(ctx->psOrphan))) {
        ObSet_Push(ctx->psDirFile, (QWORD)pe);
        pdwRecordNumberArray[cMax++] = pe->Setup.dwParentRecordNumber;
    }
    qsort(pdwRecordNumberArray, cMax, sizeof(DWORD), Util_qsort_DWORD);
    // 2: Create "synthetic" root directories
    if(!(pRootGlobal = FcNtfs_FinalizeCreateSynthenticDir(ctx, (DWORD)-1, "", FALSE))) { goto fail; }
    if(!(pRootOrphan = FcNtfs_FinalizeCreateSynthenticDir(ctx, (DWORD)-2, "ORPHAN", TRUE))) { goto fail; }
    // 3: Create "syntethic" directories for enties if record# count >= 3
    for(i = 0; i < cMax; i++) {
        if(dwLastRecord != pdwRecordNumberArray[i]) {
            dwLastRecord = pdwRecordNumberArray[i];
            c = 0;
        } else {
            c++;
            if(c == 3) {
                _snprintf_s(uszDirName, 16, _TRUNCATE, "$%i", ++iDirCount);
                if((pe = FcNtfs_FinalizeCreateSynthenticDir(ctx, dwLastRecord, uszDirName, FALSE))) {
                    pe->pParent = pRootOrphan;
                    pe->pSibling = pRootOrphan->pChild;
                    pRootOrphan->pChild = pe;
                }
            }
        }
    }
    // 4: Merge onto synth dirs
    FcNtfs_FinalizeMerge1(ctx);
    // 5: Merge remaining orphans onto orphan root
    while((pe = (PFCNTFS)ObSet_Pop(ctx->psOrphan))) {
        pe->pParent = pRootOrphan;
        pe->pSibling = pRootOrphan->pChild;
        pRootOrphan->pChild = pe;
    }
    // 6: move onto global root
    while((pe = (PFCNTFS)ObSet_Pop(ctx->psRoot))) {
        pe->pParent = pRootGlobal;
        pe->pSibling = pRootGlobal->pChild;
        pRootGlobal->pChild = pe;
    }
fail:
    LocalFree(pdwRecordNumberArray);
    return pRootGlobal;
}

/*
* Add a file system entry to the database.
*/
VOID FcNtfs_Finalize_DatabaseAdd(_In_ VMM_HANDLE H, _In_ PFCNTFS_FINALIZE_CONTEXT ctx, _In_ PFCNTFS pe, _In_ LPSTR uszPathName)
{
    QWORD id = pe->iMap;
    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, id);
    sqlite3_bind_int64(ctx->st, 2, pe->pParent ? pe->pParent->iMap : (QWORD)-1);
    sqlite3_bind_int64(ctx->st, 3, SqlStrInsert.id);
    sqlite3_bind_int64(ctx->st, 4, pe->qwHashThis);
    sqlite3_bind_int64(ctx->st, 5, pe->pParent ? pe->pParent->qwHashThis : (QWORD)-1);
    sqlite3_bind_int64(ctx->st, 6, pe->pa);
    sqlite3_bind_int64(ctx->st, 7, pe->dwMftRecordNumber);
    sqlite3_bind_int64(ctx->st, 8, pe->Flags);
    sqlite3_bind_int64(ctx->st, 9, pe->iDirDepth);
    sqlite3_bind_int64(ctx->st, 10, pe->cbFileSize);
    sqlite3_bind_int64(ctx->st, 11, pe->cbFileSizeMftResident);
    sqlite3_bind_int64(ctx->st, 12, pe->ftCreate);
    sqlite3_bind_int64(ctx->st, 13, pe->ftModify);
    sqlite3_bind_int64(ctx->st, 14, pe->ftRead);
    sqlite3_bind_int64(ctx->st, 15, pe->wName_SeqNbr);
    sqlite3_bind_int64(ctx->st, 16, ctx->cbUtf8Total + id * M_NTFS_INFO_LINELENGTH_UTF8);
    sqlite3_bind_int64(ctx->st, 17, ctx->cbJsonTotal + id * M_NTFS_INFO_LINELENGTH_JSON);
    sqlite3_step(ctx->st);
    ctx->cbUtf8Total += SqlStrInsert.cbu;
    ctx->cbJsonTotal += SqlStrInsert.cbj;
}

DWORD FcNtfs_FinalizeFinish(_In_ VMM_HANDLE H, _In_ PFCNTFS_FINALIZE_CONTEXT ctx, _In_ POB_SET psHashPath, _In_ PFCNTFS peNtfs, _In_ DWORD iMap, _In_ BYTE iDirDepth, _In_reads_(2048) LPSTR uszPath, _In_ DWORD cuszPath)
{
    DWORD dwHashName, cuszName;
    QWORD qwHashTotal;
    while(peNtfs) {
        // update/set path
        cuszName = (DWORD)strlen(peNtfs->uszName);
        if(cuszPath + cuszName + 2 >= 2048) { break; }
        uszPath[cuszPath] = '\\';
        memcpy(&uszPath[cuszPath + 1], peNtfs->uszName, cuszName + 1ULL);
        // update/set path hash
        while(TRUE) {
            qwHashTotal = peNtfs->pParent ? peNtfs->pParent->qwHashThis : 0;
            dwHashName = CharUtil_HashNameFsU(peNtfs->uszName, peNtfs->wName_SeqNbr);
            qwHashTotal = dwHashName + ((qwHashTotal >> 13) | (qwHashTotal << 51));
            if(!ObSet_Exists(psHashPath, qwHashTotal) || (peNtfs->wName_SeqNbr >= 100)) { break; }
            peNtfs->wName_SeqNbr++;
        }
        ObSet_Push(psHashPath, qwHashTotal);
        peNtfs->qwHashThis = qwHashTotal;
        peNtfs->iDirDepth = iDirDepth;
        peNtfs->iMap = iMap++;
        FcNtfs_Finalize_DatabaseAdd(H, ctx, peNtfs, uszPath);
        iMap = FcNtfs_FinalizeFinish(H, ctx, psHashPath, peNtfs->pChild, iMap, iDirDepth + 1, uszPath, cuszPath + cuszName + 1);
        peNtfs = peNtfs->pSibling;
    }
    uszPath[cuszPath] = 0;
    return iMap;
}

/*
* 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 FcNtfs_IngestFinalize(_In_ VMM_HANDLE H, _In_opt_ PVOID ctxfc)
{
    PFCNTFS_SETUP_CONTEXT ctx = (PFCNTFS_SETUP_CONTEXT)ctxfc;
    FCNTFS_FINALIZE_CONTEXT ctxFinal = { 0 };
    CHAR uszPath[2048] = { 0 };
    POB_SET psObHashPath = NULL;
    PFCNTFS pNtfsGlobalRoot;
    int rc;
    // initialize general
    if(!ctx) { goto fail; }
    if(!(psObHashPath = ObSet_New(H))) { goto fail; }
    // merge ingested items and retrieve global root
    FcNtfs_FinalizeMerge1(ctx);
    pNtfsGlobalRoot = FcNtfs_FinalizeMerge2(H, ctx);
    if(!pNtfsGlobalRoot) { goto fail; }
    // SETUP FINISH:
    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, mft_flags, depth, size_file, size_fileres, 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);
    DWORD DEBUG_NUM = FcNtfs_FinalizeFinish(H, &ctxFinal, psObHashPath, pNtfsGlobalRoot, 0, 0, uszPath, 0);
    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);
}

/*
* Timeline data by executing a partial SQL query on pre-existing data.
* -- H
* -- ctxfc
* -- hTimeline
* -- pfnAddEntry
* -- pfnEntryAddBySql
*/
VOID FcNtfs_SetupTimeline(
    _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_ LPSTR uszText),
    _In_ VOID(*pfnEntryAddBySql)(_In_ VMM_HANDLE H, _In_ HANDLE hTimeline, _In_ DWORD cEntrySql, _In_ LPSTR *pszEntrySql)
) {
    LPSTR 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);
}



//-----------------------------------------------------------------------------
// 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 qwIdParent;
    QWORD pa;
    QWORD qwFileSize;
    DWORD dwFileSizeResident;
    QWORD ftCreate;
    QWORD ftModify;
    QWORD ftRead;
    DWORD dwMftFlags;
    DWORD dwMftId;
    BOOL fDir;
    DWORD dwDirDepth;
    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 FcNtfs_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;
    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(pcbDataRead) {
                *pcbDataRead = pa->AttrLength;
            }
            if(cbData != pa->AttrLength) { return FALSE; }
            if(pbData) {
                memcpy(pbData, (pbMftEntry + oA + pa->AttrOffset), pa->AttrLength);
            }
            return TRUE;
        }
        oA += pa->Length;
    }
    return FALSE;
}

#define FCNTFS_SQL_SELECT_FIELDS " sz, id, id_parent, addr_phys, inode, mft_flags, depth, name_seq, time_create, time_modify, time_read, size_file, size_fileres, oln_u, oln_j "

_Success_(return)
BOOL FcNtfsMap_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->qwIdParent = sqlite3_column_int64(hStmt, 2);
        pe->pa = sqlite3_column_int64(hStmt, 3);
        pe->dwMftId = sqlite3_column_int(hStmt, 4);
        pe->dwMftFlags = sqlite3_column_int(hStmt, 5);
        pe->fDir = (pe->dwMftFlags & 2) ? TRUE : FALSE;
        pe->dwDirDepth = sqlite3_column_int(hStmt, 6);
        pe->dwTextSeq = sqlite3_column_int(hStmt, 7);
        pe->ftCreate = sqlite3_column_int64(hStmt, 8);
        pe->ftModify = sqlite3_column_int64(hStmt, 9);
        pe->ftRead = sqlite3_column_int64(hStmt, 10);
        pe->qwFileSize = sqlite3_column_int64(hStmt, 11);
        pe->dwFileSizeResident = sqlite3_column_int(hStmt, 12);
        pe->cszuOffset = sqlite3_column_int64(hStmt, 13);
        pe->cszjOffset = sqlite3_column_int64(hStmt, 14);
    }
    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 FcNtfsMap_GetFromHash(_In_ VMM_HANDLE H, _In_ QWORD qwHash, _Out_ PFCOB_MAP_NTFS * ppObNtfsMap)
{
    return FcNtfsMap_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 FcNtfsMap_GetFromHashParent(_In_ VMM_HANDLE H, _In_ QWORD qwHashParent, _Out_ PFCOB_MAP_NTFS * ppObNtfsMap)
{
    return FcNtfsMap_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 FcNtfsMap_GetFromIdRange(_In_ VMM_HANDLE H, _In_ QWORD qwId, _In_ QWORD cId, _Out_ PFCOB_MAP_NTFS * ppObNtfsMap)
{
    QWORD v[] = { qwId, qwId + cId };
    return FcNtfsMap_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 FcNtfs_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 FcNtfs_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 M_FcNtfs_ReadInfoSingle(_In_ VMM_HANDLE H, _In_ PFC_MAP_NTFSENTRY peNtfs, _Out_ PDWORD pcsz)
{
    LPSTR sz = NULL;
    LPSTR uszTextName;
    DWORD cszHexAscii;
    SIZE_T csz = 0;
    BYTE pbr[0x400];
    BYTE szHexAscii[0xC00];
    //----
    LPSTR uszRecordFileName;
    BYTE pbBuffer[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; }
        // $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\nAttribute 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" : ""),
            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 M_FcNtfs_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(!FcNtfs_GetIdFromPosition(H, cbOffset, FALSE, &qwIdBase)) { goto fail; }
    if(!FcNtfs_GetIdFromPosition(H, cbOffset + cb, FALSE, &qwIdTop)) { goto fail; }
    cId = min(cb / M_NTFS_INFO_LINELENGTH_UTF8, qwIdTop - qwIdBase) + 1;
    if(!FcNtfsMap_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 %s : %s %12llx %3x %c %s\n",
            pe->qwId,
            pe->pa,
            (pe->dwMftId & 0xf0000000 ? 0 : pe->dwMftId),
            szTimeCreate,
            szTimeModify,
            pe->qwFileSize,
            pe->dwFileSizeResident,
            pe->fDir ? '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 M_FcNtfs_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 M_FcNtfs_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 M_FcNtfs_ReadInfoAll(H, pb, cb, pcbRead, cbOffset);
    }
    M_FcNtfs_PathStripMftInfo(H, ctxP->uszPath, uszPathStripped, &fMeta, NULL, &fMetaTxt, &fMetaMem, &fMetaBin);
    qwHashPath = CharUtil_HashPathFsU(uszPathStripped);
    if(FcNtfsMap_GetFromHash(H, qwHashPath, &pObNtfsMap) && pObNtfsMap->cMap) {
        peNtfs = pObNtfsMap->pMap + 0;
        if(!fMeta || fMetaBin) {
            if(peNtfs->qwFileSize && (peNtfs->dwFileSizeResident == peNtfs->qwFileSize) && (peNtfs->qwFileSize < 0x400)) {
                if(FcNtfs_GetMftResidentData(H, peNtfs, pbNtfsRecordMax, peNtfs->dwFileSizeResident, NULL)) {
                    nt = Util_VfsReadFile_FromPBYTE(pbNtfsRecordMax, peNtfs->dwFileSizeResident, 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 = M_FcNtfs_ReadInfoSingle(H, peNtfs, &cbInfoTxt))) {
            nt = Util_VfsReadFile_FromPBYTE(pbInfoTxt, cbInfoTxt, pb, cb, pcbRead, cbOffset);
            LocalFree(pbInfoTxt);
        }
    }
    Ob_DECREF(pObNtfsMap);
    return nt;
}

VOID M_FcNtfs_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];
    LPSTR uszTextName;
    QWORD qwHashPath;
    FileExInfo.dwVersion = VMMDLL_VFS_FILELIST_EXINFO_VERSION;
    M_FcNtfs_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(FcNtfsMap_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 = M_FcNtfs_ReadInfoSingle(H, pe, &cbInfoTxt))) {
                    VMMDLL_VfsList_AddFile(pFileList, "mftinfo.txt", cbInfoTxt, &FileExInfo);
                    LocalFree(pbInfoTxt);
                }
                VMMDLL_VfsList_AddFile(pFileList, "mftdata.mem", 0x400, &FileExInfo);
                if(!pe->fDir) {
                    VMMDLL_VfsList_AddFile(pFileList, "mftfile.bin", pe->qwFileSize, &FileExInfo);
                }
            }
        }
        Ob_DECREF(pObNtfsMap);
        return;
    }
    // ordinary directory or metadata directory
    if(!FcNtfsMap_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->fDir || fMeta) {
            if(pe->pa || !fMeta) {
                FileExInfo.fCompressed = FALSE;
                VMMDLL_VfsList_AddDirectory(pFileList, uszNameFix, &FileExInfo);
            }
        } else {
            FileExInfo.fCompressed = !pe->qwFileSize || (pe->qwFileSize != pe->dwFileSizeResident);
            VMMDLL_VfsList_AddFile(pFileList, uszNameFix, pe->qwFileSize, &FileExInfo);
        }
    }
    Ob_DECREF(pObNtfsMap);
}

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

VOID M_FcNtfs_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 != VMM_SYSTEM_WINDOWS_X64) && (pRI->tpSystem != VMM_SYSTEM_WINDOWS_X86)) { 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 = M_FcNtfs_List;                                        // List function supported
    pRI->reg_fn.pfnRead = M_FcNtfs_Read;                                        // Read function supported
    pRI->reg_fn.pfnNotify = M_FcNtfs_Notify;                                    // Notify function supported
    pRI->reg_fnfc.pfnInitialize = FcNtfs_Initialize;                            // Forensic initialize function supported
    pRI->reg_fnfc.pfnFinalize = FcNtfs_Finalize;                                // Forensic ingest finalize function supported
    pRI->reg_fnfc.pfnIngestPhysmem = FcNtfs_IngestPhysmem;                      // Forensic physmem ingest supported
    pRI->reg_fnfc.pfnIngestFinalize = FcNtfs_IngestFinalize;                    // Forensic ingest finalize function supported
    pRI->reg_fnfc.pfnTimeline = FcNtfs_SetupTimeline;                           // Forensic timelining supported
    memcpy(pRI->reg_info.sTimelineNameShort, "NTFS", 5);
    strncpy_s(pRI->reg_info.uszTimelineFile, 32, "timeline_ntfs", _TRUNCATE);
    pRI->pfnPluginManager_Register(H, pRI);
}
