October 1999
Figure 3   Listing Files Without Journal Data

BOOL EnumerateMft(HANDLE hcj, USN usnLow, USN usnHigh)
   DWORD cb;
   BYTE Buffer[sizeof(DWORDLONG) + 16384]; // read in 16KB chunks

   // Enumerate MFT for files with 'Last USN'
   // between usnLow and usnHigh
   MFT_ENUM_DATA med;
   med.StartFileReferenceNumber = 0;
   med.LowUsn = usnLow;
   med.HighUsn = usnHigh;

   while(DeviceIoControl(hcj, FSCTL_ENUM_USN_DATA, &med, sizeof(med),
                         &Buffer, sizeof(Buffer), &cb, NULL)) {
      USN_RECORD *pRecord = (USN_RECORD *) &Buffer[sizeof(USN)];
      while ((PBYTE) pRecord  (pData + cb)) {
         // Examine record - this is not actually a journal record
         // pRecord->FileReferenceNumber will tell us what FRN we've
         // retrieved, and pRecord->Usn is its 'Last USN'

         // Valid members are
         //  pRecord->RecordLength
         //  pRecord->FileReferenceNumber
         //  pRecord->ParentFileReferenceNumber
         //  pRecord->Usn
         //  pRecord->FileAttributes
         //  pRecord->FileNameOffset
         //  pRecord->FileNameLength

         // Move to next record
         pRecord = (PUSN_RECORD) (((PBYTE) pRecord) + pRecord->RecordLength);
      }
      // The next call uses the FRN in the first 8 bytes
      // of the output buffer
      med.StartFileReferenceNumber = * ((DWORDLONG *) Buffer);
   }
   return(GetLastError() == ERROR_HANDLE_EOF);
}

Figure 4   Journal Events

Your service runs
USN
File
Reason
128
File1
created
256
File2
created
384
File3
created
512
File4
created
Your application shuts down (NextUSNis 640)
USN
File
Reason
640
File1
deleted
768
File2
changed
896
File3
changed
1024
File5
created
System purges records
USN
File
Reason
1152
File3
changed
1280
File4
changed
Your application runs again (FirstUSNis 1152)


Figure 5   Directory to FRN


BOOL FRNFromPath(LPCTSTR pszPath, DWORDLONG *pFRN) {
   HANDLE hdir = CreateFile(pszPath, 0, FILE_SHARE_READ | FILE_SHARE_WRITE,
                            NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 
                            NULL);
   if (hdir == INVALID_HANDLE_VALUE)
      return(FALSE);

   BY_HANDLE_FILE_INFORMATION fi;
   GetFileInformationByHandle(hdir, &fi);
   CloseHandle(hdir);

   // Fill in the FRN
   *pFRN = (((DWORDLONG) fi.nFileIndexHigh) < 32) | fi.nFileIndexLow; 
   return(TRUE);
}

Figure 6   FRN to Full Path

// Assume that you have a CString class for string manipulation

// Your implementation of the 'directory database' must expose
// a function that returns the stored information for a
// directory's FRN.
// For this sample, assume the prototype below represents your
// implementation of this function.
// Parameters:
//   frn - the FRN of a directory you wish to query
//   pParentFRN - will be filled in with the directory's parent FRN
//   name - will be filled in with the directory's name (such as 'system32')
BOOL InfoFromFRN(DWORDLONG frn, DWORDLONG* pParentFRN, CString &name);

// PathFromFRN - Convert FRN of a path to a full path
//   frn - the FRN of the path
//   frnRoot - the FRN on the root directory
//   pathname - full path returned
BOOL PathFromFRN(DWORDLONG frn, DWORDLONG frnRoot, CString &pathname) {
   DWORDLONG frnTemp;

   // Get the directory's name
   InfoFromFRN(frn, &frnTemp, path name);

   // If the caller passed us the FRN of the root
   // directory, we are already finished - The call to
   // InfoFromFRN should have filled in the path name using
   // the format - D:\
   if (frn == frnRoot)
      return(TRUE);

   // Loop through all its parents 
   while (TRUE) {
      CString nameTemp;
      DWORDLONG frnTemp;
      InfoFromFRN(frn, &frnTemp, nameTemp);

      // Prepend its parent's name
      pathname = nameTemp + __T("\\") + pathname;

      // If this was the root, we're done
      if (frn == frnRoot)
         break;

      // Move to its parent
      frn = frnTemp;
   }
   return(TRUE);
}

Figure 7   Maintaining a Directory Database

// Your directory database must support the following functions
//  AddRecord(DWORDLONG frn, DWORDLONG parentfrn, CString name);
//  ChangeRecord(DWORDLONG frn, DWORDLONG newparentfrn, CString newname);
//  DeleteRecord(DWORDLONG frn);

// This code should be inside your function that processes
// all the journal's 'close' records.
 
// This code assumes you are using the following variables:
//  PUSN_RECORD pRecord - a pointer to the current record
//  LPCTSTR name - a zero-terminated pointer to the name derived from
//                 pRecord->FileNameOffset and pRecord->FileNameLength

// When we see the 'close' reason, we can update our database
// with any new, deleted, or renamed files or directories.
if(pRecord->Reason & USN_REASON_CLOSE) {
   DWORDLONG frn = pRecord->FileReferenceNumber;
   DWORDLONG parentfrn = pRecord->ParentFileReferenceNumber;
  
   if ((pRecord->Reason & USN_REASON_FILE_CREATE) != 0)
      AddRecord(frn, parentfrn, name);
   if ((pRecord->Reason & USN_REASON_RENAME_NEW_NAME) != 0)
      ChangeRecord(frn, parentfrn, name);
   if ((pRecord->Reason & USN_REASON_FILE_DELETE) != 0)
      DeleteRecord(frn, parentfrn, name);
}


Figure 8   Adding to a Named Stream

// Get a handle to the 'C' volume
HANDLE hcj = CreateFile("\\\\.\\C:", GENERIC_READ | GENERIC_WRITE
   FILE_SHARE_READ | FILE_SHARE_WRITE,
   NULL, OPEN_EXISTING, 0, NULL);

// Create private stream on some file
HANDLE hstream = CreateFile("C:\\somefile.txt:SecretInfo",
   GENERIC_READ | GENERIC_WRITE
   0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTES_NORMAL, NULL);

// Tell the journal that we're creating a private stream that
// no one else should care about
DWORD cbRead;
MARK_HANDLE_INFO mhi;
mhi.UsnSourceInfo = USN_SOURCE_AUXILIARY_DATA; // adding private stream
mhi.VolumeHandle = hcj;
mhi.HandleInfo = 0;

DeviceIoControl(hstream, FSCTL_MARK_HANDLE, &mhi, sizeof(mhi),
    NULL, 0, &cbRead, NULL);

// At this point, your application can write to the stream.
// Journal entries will be created, but the SourceInfo member
// of the records will include USN_SOURCE_AUXILIARY_DATA.
// Any application that is monitoring the journal can use this
// information to decide if it cares that the file has changed.

CloseHandle(hcj);
CloseHandle(hstream);

Figure 10   CJTest

CJTest.cpp


/******************************************************************************
Module name: CJTest.cpp
Written by: Jeffrey Cooperstein & Jeffrey Richter
******************************************************************************/

#include "stdafx.h"
#include "Resource.H"
#include "ChangeJrnl.h"
#include "PathDB.h"

///////////////////////////////////////////////////////////////////////////////

// This is the file where we will store our directory database. It will be
// in the root directory of the volume we are working with
#define DBSTORAGEFILE TEXT("\\CJTestPathDBFile.dat")

#define HIGHPART(x) ((DWORD) ((x) >> 32))
#define LOPART(x)   ((DWORD) (x))

// Global variables
struct APPGLOBALS {
   APPGLOBALS() {
      m_LastFileReferenceNumber = 0;
      m_LastSourceInfo = 0;
   }

   CChangeJrnl m_cj; // Change Journal
   CPathDB     m_db; // Directory Database

   // Members that help format display output
   DWORDLONG   m_LastFileReferenceNumber;
   DWORD       m_LastSourceInfo;

   // Flag that indicates that we've processed all records
   BOOL        m_fUpToDate; 
};

APPGLOBALS g_Globals;

// Prototypes
void ProcessAvailableRecords(HWND hwnd, BOOL fShowRecords);
void InitialzeForMonitoring(HWND hwnd);

///////////////////////////////////////////////////////////////////////////////

// Adds a string to an edit control
void AddText(HWND hwnd, LPCTSTR pszFormat, ...) {

   va_list argList;
   va_start(argList, pszFormat);

   TCHAR sz[20 * 1024];
   _vstprintf(sz, pszFormat, argList);
   va_end(argList);

   // If the edit control is longer than 20000 characters, 
   // chop off the first fifty lines.
   while (GetWindowTextLength(hwnd) > 20000) {
      Edit_SetSel(hwnd, 0, Edit_LineIndex(hwnd, 50));
      Edit_ReplaceSel(hwnd, TEXT("));
   }
   int nLen = GetWindowTextLength(hwnd);
   Edit_SetSel(hwnd, nLen, nLen);
   Edit_ReplaceSel(hwnd, sz);
   int nLenNew = GetWindowTextLength(hwnd);
   Edit_SetSel(hwnd, nLenNew, nLenNew);
   Edit_Scroll(hwnd, 1, 0);
}

///////////////////////////////////////////////////////////////////////////////

BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam) {
   
   // Have the results window use a fixed-pitch font
   SetWindowFont(GetDlgItem(hwnd, IDC_RESULTS), 
      GetStockFont(ANSI_FIXED_FONT), FALSE);

   // The app will monitor the journal that is on the current drive
   TCHAR szCurrentDirectory[MAX_PATH];
   GetCurrentDirectory(MAX_PATH, szCurrentDirectory);

   // Initialize the CChangeJrnl class with the current drive letter and tell
   // it to allocate a buffer of 10000 bytes to read journal records
   if (!g_Globals.m_cj.Init(szCurrentDirectory[0], 10000)) {
      MessageBox(hwnd, TEXT("CChangeJrnl Initialization Error"), 
                 NULL, MB_OK);
      EndDialog(hwnd, IDCANCEL);
      return(FALSE);
   }

   SetFocus(GetDlgItem(hwnd, IDC_RESULTS));
   AddText(GetDlgItem(hwnd, IDC_RESULTS), TEXT("Monitoring %c:\\\r\n"),
                      szCurrentDirectory[0]);

   TCHAR cDriveLetter;
   DWORDLONG CurUsnJournalID;
   USN CurUsn;

   // Try to load cached directory database from file
   BOOL fOk = g_Globals.m_db.StreamFromFile(DBSTORAGEFILE, &cDriveLetter,
                                            &CurUsnJournalID, &CurUsn);

   // If we were able to read cached data, and the data came from the correct
   // drive, we can pick up processing the journal where we left off.
   if (fOk && (cDriveLetter == szCurrentDirectory[0])) {

      g_Globals.m_fUpToDate = TRUE;

      // Start processing records where we left off
      g_Globals.m_cj.SeekToUsn(CurUsn, 0xFFFFFFFF, FALSE, CurUsnJournalID);

      // Process all available records (but don't output them to screen since
      // this could be very time-consuming).
      // The ProcessAvailableRecords function will handle scenarios where the
      // journal is not active or the journal ID has changed since we cached
      // the data to disk.
      ProcessAvailableRecords(hwnd, FALSE);
   } else {
      // Make sure the dialog is visible because the next function will bring
      // up a modal dialog
      ShowWindow(hwnd, SW_SHOW);

      // There's no cached data for the directory database. We need to make
      // sure the journal is in the correct state, and populate the directory
      // database.
      InitialzeForMonitoring(hwnd);
   }
   return(FALSE);
}

///////////////////////////////////////////////////////////////////////////////

BOOL Dlg_OnSize(HWND hwnd, UINT state, int cx, int cy) {

   // Size the output edit control to fill the entire dialog
   int n = LOWORD(GetDialogBaseUnits());
   HWND hwndCtl = GetDlgItem(hwnd, IDC_RESULTS);
   SetWindowPos(hwndCtl, NULL, n, n, cx - n - n, cy - n - n, SWP_NOZORDER);
   return(FALSE);
}

///////////////////////////////////////////////////////////////////////////////

void DumpJrnlStats(HWND hwnd) {

   // Dump the information about the volume's journal
   HWND h = GetDlgItem(hwnd, IDC_RESULTS);
   USN_JOURNAL_DATA ujd;
   if (!g_Globals.m_cj.Query(&ujd)) {
      AddText(h, 
         TEXT("*** Error while querying volume for journal information ***"));
      return;
   }

   // Convert journal ID to time string
   TCHAR szDateTime[1024];
   if (!CChangeJrnl::GetDateTimeFormatFromFileTime(ujd.UsnJournalID,
       szDateTime, sizeof(szDateTime) / sizeof(szDateTime[0]))) {
       lstrcpy(szDateTime, TEXT("ERROR"));
   }

   AddText(h, TEXT("**************************************\r\n"));
   AddText(h, TEXT("UsnJournalID time = %s\r\n"), szDateTime);
   AddText(h, TEXT("UsnJournalID    = 0x%016I64X\r\n"), ujd.UsnJournalID);
   AddText(h, TEXT("FirstUsn        = 0x%016I64X\r\n"), ujd.FirstUsn);
   AddText(h, TEXT("NextUsn         = 0x%016I64X\r\n"), ujd.NextUsn);
   AddText(h, TEXT("LowestValidUsn  = 0x%016I64X\r\n"), ujd.LowestValidUsn);
   AddText(h, TEXT("MaxUsn          = 0x%016I64X\r\n"), ujd.MaxUsn);
   AddText(h, TEXT("MaximumSize     = 0x%016I64X\r\n"), ujd.MaximumSize);
   AddText(h, TEXT("AllocationDelta = 0x%016I64X\r\n"), ujd.AllocationDelta);
   AddText(h, TEXT("**************************************\r\n"));
}

///////////////////////////////////////////////////////////////////////////////

void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) {

   switch (id) {
      case IDCANCEL:
         // If we've kept up to date with the journal, save the directory
         // database out to disk.
         if (g_Globals.m_fUpToDate) {
            g_Globals.m_db.StreamToFile(DBSTORAGEFILE, g_Globals.m_cj);
         }
         EndDialog(hwnd, id);
         break;

      case ID_JRNLINFO:
         DumpJrnlStats(hwnd);
         break;

      case ID_DELETEJRNL:
         if (IDYES == MessageBox(hwnd,
            TEXT("Are you sure you want to delete the current journal?\r\n")
            TEXT("(It will be automatically recreated by the application)"),
            TEXT("Delete Change Journal"), MB_YESNO)) {

            // Delete the journal - Don't wait for it to return. When we try to
            // read more records, we'll see that the journal is gone. At that
            // point, the InitializeForMonitoring function will recreate it
            USN_JOURNAL_DATA ujd;
            g_Globals.m_cj.Query(&ujd);
            g_Globals.m_cj.Delete(ujd.UsnJournalID, USN_DELETE_FLAG_DELETE);
         }
         break;
   }
}

///////////////////////////////////////////////////////////////////////////////

LRESULT Dlg_OnChangeJournalChanged(HWND hwnd, WPARAM wParam, LPARAM lParam) {

   // We get this notification if there are new records to process, or possibly
   // if the journal has been deleted. The ProcessAvailableRecords function
   // will handle both cases.
   ProcessAvailableRecords(hwnd, TRUE);
   return(0);
}

///////////////////////////////////////////////////////////////////////////////

INT_PTR WINAPI Dlg_Proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
   
   switch (uMsg) {
      chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog);
      chHANDLE_DLGMSG(hwnd, WM_SIZE,       Dlg_OnSize);
      chHANDLE_DLGMSG(hwnd, WM_COMMAND,    Dlg_OnCommand);
      case WM_CHANGEJOURNALCHANGED:
         Dlg_OnChangeJournalChanged(hwnd, wParam, lParam);
         break;
   }
   return(FALSE);
}

///////////////////////////////////////////////////////////////////////////////

int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE, LPTSTR pszCmdLine, int) {

   DialogBox(hinstExe, MAKEINTRESOURCE(IDD_CJTEST), NULL, Dlg_Proc);
   return(0);
}

///////////////////////////////////////////////////////////////////////////////

void ProcessAvailableRecords(HWND hwnd, BOOL fShowRecords /* = TRUE*/) {

   // Process all available journal records from the current location to the
   // end of the journal. Before this function is called, at least one call has
   // to be made to the SeekToUsn function. If this function completes without
   // error, it sets up a notification that waits for more data to become
   // available.

   HWND hwndOut = GetDlgItem(hwnd, IDC_RESULTS);
   SetWindowRedraw(hwndOut, FALSE);

   PUSN_RECORD pRecord;

   // Use EnumNext to loop through available records
   while (pRecord = g_Globals.m_cj.EnumNext()) {
      // Create a zero-terminated copy of the file name
      WCHAR szFile[MAX_PATH];
      LPWSTR pszFileName = (LPWSTR) 
         ((PBYTE) pRecord + pRecord->FileNameOffset);
      int cFileName = pRecord->FileNameLength / sizeof(WCHAR);
      wcsncpy(szFile, pszFileName, cFileName);
      szFile[cFileName] = 0;

      // If this is a close record for a directory, we may need to adjust 
      // our directory database
      if (   0 != (pRecord->FileAttributes & FILE_ATTRIBUTE_DIRECTORY)
          && 0 != (pRecord->Reason & USN_REASON_CLOSE)) {

         // Process newly created directories
         if (0 != (pRecord->Reason & USN_REASON_FILE_CREATE)) {
            g_Globals.m_db.Add(pRecord->FileReferenceNumber, szFile,
               pRecord->ParentFileReferenceNumber);
         }

         // Process renamed directories
         if (0 != (pRecord->Reason & USN_REASON_RENAME_NEW_NAME)) {
            g_Globals.m_db.Change(pRecord->FileReferenceNumber, szFile,
               pRecord->ParentFileReferenceNumber);
         }

         // Process deleted directories
         if (0 != (pRecord->Reason & USN_REASON_FILE_DELETE)) {
            g_Globals.m_db.Delete(pRecord->FileReferenceNumber);
         }
      }

      // Dump the record to screen if requested
      if (fShowRecords) {
         CString szPath;
         TCHAR szFullName[MAX_PATH];

         // Get the full path for this record from the directory database
         if (g_Globals.m_db.Get(pRecord->ParentFileReferenceNumber, szPath)) {
            _stprintf(szFullName, TEXT("%s\\%s"), (LPCTSTR) szPath,
               (LPCTSTR) szFile);
         } else {
            _stprintf(szFullName, 
               TEXT("%s can not find path for objects parent ID 0x%016I64X"),
               szFile, pRecord->ParentFileReferenceNumber);
         }

         // If this record is dealing with a different file than the last
         // record we displayed, print its full path.
         if (g_Globals.m_LastFileReferenceNumber != 
            pRecord->FileReferenceNumber) {

            g_Globals.m_LastSourceInfo = -1;
            AddText(hwndOut, TEXT("%s\r\n"), (LPCTSTR) szFullName);
         }

         // Store the FRN of the current record so we know if we need to
         // reprint its full path on the next record
         g_Globals.m_LastFileReferenceNumber = pRecord->FileReferenceNumber;

         // If the source information has changed since the last record we 
         // displayed,
         // print the new source information
         if (g_Globals.m_LastSourceInfo != pRecord->SourceInfo) {
            AddText(hwndOut, TEXT(" SourceInfo = %i\r\n"), 
                    pRecord->SourceInfo);
            g_Globals.m_LastSourceInfo = pRecord->SourceInfo;
         }

         // Show the reason codes
         TCHAR szReason[1024];
         if (!CChangeJrnl::GetReasonString(pRecord->Reason, szReason,
             sizeof(szReason) / sizeof(szReason[0]))) {
             lstrcpy(szReason, TEXT("ERROR"));
         }

         AddText(hwndOut, TEXT("  %s"), szReason);

         // If the record has a 'rename' flag set, append the file name
         if (0 != (pRecord->Reason & 
             (USN_REASON_RENAME_OLD_NAME | USN_REASON_RENAME_NEW_NAME))) {
             AddText(hwndOut, TEXT(" (%s)"), (LPCTSTR) szFullName);
         }

         AddText(hwndOut, TEXT("\r\n"));
      }
   }
   // EnumNext will return NULL with the 'last error' set to NO_ERROR if
   // we reached the end of available journal records without problems
   BOOL fOk = (S_OK == GetLastError());

   SetWindowRedraw(hwndOut, TRUE);
   InvalidateRect(hwndOut, NULL, FALSE);

   if (fOk) {
      // We successfully processed all new records
      // Wait for more data. Specify a delay of 500 milliseconds after new data
      // is available so that we don't pound the journal with read requests
      // every time a new record is added
      g_Globals.m_cj.NotifyMoreData(hwnd, 500);
   } else {
      // There was a problem. The journal could have been deleted, the id could
      // have changed, or records may have been purged before we got to them.
      // No matter what it was, we have to invalidate cached data and start
      // from scratch
      InitialzeForMonitoring(hwnd);
   }
}

///////////////////////////////////////////////////////////////////////////////

void InitialzeForMonitoring(HWND hwnd) {

   // This function ensures that the journal on the volume is active, and it
   // will also populate the directory database.
   HWND hwndOut = GetDlgItem(hwnd, IDC_RESULTS);

   BOOL fOk = TRUE;
   USN_JOURNAL_DATA ujd;

   // Try to query for current journal information
   while (fOk && !g_Globals.m_cj.Query(&ujd)) {
      switch(GetLastError()) {
      case ERROR_JOURNAL_DELETE_IN_PROGRESS:
         // The system is deleting a journal. We need to wait for it to finish
         // before trying to query it again.
         AddText(hwndOut,  TEXT("Waiting for Delete to finish\r\n"));
         InvalidateRect(hwndOut, NULL, FALSE);
         UpdateWindow(hwndOut);
         g_Globals.m_cj.Delete(0, USN_DELETE_FLAG_NOTIFY);
         AddText(hwndOut,  TEXT("Delete finished\r\n"));
         break;

      case ERROR_JOURNAL_NOT_ACTIVE:
         // The journal is not active on the volume. We need to create it and
         // then query for its information again
         AddText(hwndOut,  TEXT("Journal not active - creating new one\r\n"));
         g_Globals.m_cj.Create(0x800000, 0x100000);
         break;

      default:
         // Some other error happened while querying the journal information.
         // There is nothing we can do from here
         AddText(hwndOut,  TEXT("Unable to query journal\r\n"));
         AddText(hwndOut,  TEXT("MONITORING STOPPED!\r\n"));
         g_Globals.m_fUpToDate = FALSE;
         fOk = FALSE;
         break;
      }
   }
   if (!fOk) {
      // We were not able to query the volume for journal information
      return;
   }

   // Start processing records at the start of the journal
   g_Globals.m_cj.SeekToUsn(ujd.FirstUsn, 0xFFFFFFFF, FALSE, ujd.UsnJournalID);

   // Make sure the user is willing to perform a rescan of all the directories
   // to populate the directory database
   int id = MessageBox(hwnd,
      TEXT("Full rescan needed to build directory database\r\n")
      TEXT("Possible Reasons:\r\n1) No cached data\r\n")
      TEXT("2) Journal ID has changed\r\n")
      TEXT("3) Some records were purged before we processed them\r\n")
      TEXT("\r\nContinue?"), TEXT("Perform Full Rescan"), MB_YESNO);

   if (id == IDYES) {
      // Use 'method 2' to populate the database
//    g_Globals.m_db.PopulateMethod1(g_Globals.m_cj);
      g_Globals.m_db.PopulateMethod2(g_Globals.m_cj);

      g_Globals.m_fUpToDate = TRUE;

      // Process all available records (but don't output them to screen since
      // this could be very time consuming).
      ProcessAvailableRecords(hwnd, FALSE);
   } else {
      // The user did not want to spend the time to populate the directory
      // database. There's nothing else we can do.
      AddText(hwndOut, TEXT("No Longer monitoring the journal\r\n"));
      g_Globals.m_fUpToDate = FALSE;
   }
}

//////////////////////////////// End of File //////////////////////////////////
PathDB.cpp

/******************************************************************************
Module name: PathDB.cpp
Written by: Jeffrey Cooperstein & Jeffrey Richter
******************************************************************************/

#include "stdafx.h"
#include "PathDB.h"

///////////////////////////////////////////////////////////////////////////////

BOOL CPathDB::Add(DWORDLONG Index, LPCTSTR szName, DWORDLONG ParentIndex) {

  DWORD dwOffset;
  if (mapIndexToOffset.Lookup(Index, dwOffset)) {
     return(FALSE); // Index already in database
  }
  rgszNames.Add(szName);
  rgParentIndex.Add(ParentIndex);
  mapIndexToOffset[Index] = rgszNames.GetSize() - 1;
  return(TRUE);
}

///////////////////////////////////////////////////////////////////////////////

BOOL CPathDB::Empty() {

   rgszNames.RemoveAll();
   rgParentIndex.RemoveAll();
   mapIndexToOffset.RemoveAll();
   return(TRUE);
}


///////////////////////////////////////////////////////////////////////////////

BOOL CPathDB::Get(DWORDLONG Index, CString &sz) {

   sz = TEXT(");
   do {
      DWORD dwOffset;
      if (!mapIndexToOffset.Lookup(Index, dwOffset)) {
         return(FALSE); // did not find all the parents
      }
      sz = rgszNames[dwOffset] + 
         ((sz.GetLength() != 0) ? TEXT("\\") : TEXT(")) + sz;
      Index = rgParentIndex[dwOffset];
   } while (Index != 0);
   return(TRUE);
}

///////////////////////////////////////////////////////////////////////////////

BOOL CPathDB::Change(DWORDLONG Index, LPCTSTR szName, DWORDLONG ParentIndex) {

   DWORD dwOffset;
   if (!mapIndexToOffset.Lookup(Index, dwOffset)) {
      return(FALSE); // not found
   }
   rgszNames[dwOffset] = szName;
   rgParentIndex[dwOffset] = ParentIndex;
   return(TRUE);
}

///////////////////////////////////////////////////////////////////////////////

BOOL CPathDB::Delete(DWORDLONG Index) {

   DWORD dwOffset;
   if (!mapIndexToOffset.Lookup(Index, dwOffset)) {
      return(FALSE); // not found
   }
   // NOTE: We lose memory in the rgszNames and rgParentIndex array
   // since we cannot shift the rest of the offsets down
   // We could be more efficient here.

   // At the least, let's free the memory used by the string.
   rgszNames[dwOffset].Empty();
   mapIndexToOffset.RemoveKey(Index);
   return(TRUE);
}

///////////////////////////////////////////////////////////////////////////////

BOOL CPathDB::StreamToFile(LPCTSTR pszFile, CChangeJrnl &cj) { 

   // Save current state information to a file
   TCHAR cDriveLetter = cj.GetDriveLetter();
   DWORDLONG UsnJournalID = cj.GetCurUsnJrnlID();
   USN usn = cj.GetCurUsn();

   HANDLE hFile = CreateFile(pszFile, GENERIC_WRITE, 0, NULL,
      CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

   if (INVALID_HANDLE_VALUE == hFile) {
      return(FALSE);
   }

   BOOL fOk = TRUE;
   POSITION pos = mapIndexToOffset.GetStartPosition();
   DWORDLONG Index;
   DWORD dwOffset;
   DWORD dwWritten;
   fOk = fOk && WriteFile(hFile, &cDriveLetter, sizeof(cDriveLetter),
      &dwWritten, NULL);

   fOk = fOk && WriteFile(hFile, &UsnJournalID, sizeof(UsnJournalID),
      &dwWritten, NULL);

   fOk = fOk && WriteFile(hFile, &usn, sizeof(usn), &dwWritten, NULL);
   while ((pos != 0) && fOk) {
      mapIndexToOffset.GetNextAssoc(pos, Index, dwOffset);
      fOk = fOk && WriteFile(hFile, &Index, sizeof(Index),
         &dwWritten, NULL);

      fOk = fOk && WriteFile(hFile, &rgParentIndex[dwOffset],
         sizeof(rgParentIndex[dwOffset]), &dwWritten, NULL);

      DWORD dwLen = rgszNames[dwOffset].GetLength();
      fOk = fOk && WriteFile(hFile, &dwLen, sizeof(dwLen), &dwWritten, NULL);

      fOk = fOk && WriteFile(hFile, (LPCTSTR)rgszNames[dwOffset],
         dwLen * sizeof(rgszNames[dwOffset][0]), &dwWritten, NULL);
   }
   Index = -1;
   fOk = fOk && WriteFile(hFile, &Index, sizeof(Index), &dwWritten, NULL);
   CloseHandle(hFile);
   return(fOk);
}

///////////////////////////////////////////////////////////////////////////////

BOOL CPathDB::StreamFromFile(LPCTSTR pszFile, TCHAR *pcDriveLetter,
   DWORDLONG *pUsnJournalID, USN *pUsn) {

   // Load state information from a file
   HANDLE hFile = CreateFile(pszFile, GENERIC_READ, 0, NULL,
      OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, NULL);
   if (INVALID_HANDLE_VALUE == hFile) {
      return(FALSE);
   }

   BOOL fOk = TRUE;
   POSITION pos = mapIndexToOffset.GetStartPosition();
   DWORD dwWritten;
   fOk = fOk && ReadFile(hFile, pcDriveLetter, sizeof(*pcDriveLetter),
      &dwWritten, NULL);

   fOk = fOk && ReadFile(hFile, pUsnJournalID, sizeof(*pUsnJournalID),
      &dwWritten, NULL);

   fOk = fOk && ReadFile(hFile, pUsn, sizeof(*pUsn), &dwWritten, NULL);

   while (fOk) {
      DWORDLONG Index;
      DWORDLONG ParentIndex;
      DWORD dwLen;
      fOk = fOk && ReadFile(hFile, &Index, sizeof(Index), &dwWritten, NULL);
      if (-1 == Index) {
         break;
      }
      fOk = fOk && ReadFile(hFile, &ParentIndex, sizeof(ParentIndex),
         &dwWritten, NULL);

      fOk = fOk && ReadFile(hFile, &dwLen, sizeof(dwLen),
         &dwWritten, NULL);

      LPTSTR psz = new TCHAR[dwLen+1];
      psz[dwLen] = 0;
      fOk = fOk && ReadFile(hFile, psz, dwLen * sizeof(TCHAR), 
         &dwWritten, NULL);

      Add(Index, psz, ParentIndex);
      delete[] psz;
   }
   CloseHandle(hFile);
   return(fOk);
}

///////////////////////////////////////////////////////////////////////////////

void CPathDB::PopulateMethod1(CChangeJrnl &cj) {

   Empty();

   // Fill the database by walking all directories on the volume. The function
   // RecursePath uses FindFirstFile/FindNextFile to walk the volume, and
   // GetFileInformationByHandle to get the file reference numbers of every
   // directory
   TCHAR szCurrentPath[_MAX_PATH];
   wsprintf(szCurrentPath, TEXT("%c:"), cj.GetDriveLetter());
   RecursePath(szCurrentPath, szCurrentPath, 0, 0);
}

///////////////////////////////////////////////////////////////////////////////

void CPathDB::PopulateMethod2(CChangeJrnl &cj) {

   Empty();

   // Enumerate the MFT for all entries. Store the file reference numbers of
   // any directories in the database.
   USN_JOURNAL_DATA ujd;
   cj.Query(&ujd);

   // Get the FRN of the root directory
   // This had BETTER work, or we can't do anything
   TCHAR szRoot[_MAX_PATH];
   wsprintf(szRoot, TEXT("%c:\\"), cj.GetDriveLetter());
   HANDLE hDir = CreateFile(szRoot, 0, FILE_SHARE_READ | FILE_SHARE_WRITE,
      NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);

   BY_HANDLE_FILE_INFORMATION fi;
   GetFileInformationByHandle(hDir, &fi);
   CloseHandle(hDir);
   DWORDLONG IndexRoot = (((DWORDLONG) fi.nFileIndexHigh) < 32) 
      | fi.nFileIndexLow;
   wsprintf(szRoot, TEXT("%c:"), cj.GetDriveLetter());
   Add(IndexRoot, szRoot, 0);

   MFT_ENUM_DATA med;
   med.StartFileReferenceNumber = 0;
   med.LowUsn = 0;
   med.HighUsn = ujd.NextUsn;

   // Process MFT in 64K chunks
   BYTE pData[sizeof(DWORDLONG) + 0x10000];
   DWORDLONG fnLast = 0;
   DWORD cb;
   while (DeviceIoControl(cj, FSCTL_ENUM_USN_DATA, &med, sizeof(med),
      pData, sizeof(pData), &cb, NULL) != FALSE) {

      PUSN_RECORD pRecord = (PUSN_RECORD) &pData[sizeof(USN)];
      while ((PBYTE) pRecord < (pData + cb)) {
         if (0 != (pRecord->FileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {

            CString sz((LPCWSTR)
               ((PBYTE) pRecord + pRecord->FileNameOffset),
               pRecord->FileNameLength / sizeof(WCHAR));

            Add(pRecord->FileReferenceNumber, sz,
               pRecord->ParentFileReferenceNumber);
         }
         pRecord = (PUSN_RECORD) ((PBYTE) pRecord + pRecord->RecordLength);
      }
      med.StartFileReferenceNumber = * (DWORDLONG *) pData;
   }
}

///////////////////////////////////////////////////////////////////////////////

void CPathDB::RecursePath(LPCTSTR pszPath, LPCTSTR pszFile,
   DWORDLONG ParentIndex, DWORD dwVolumeSerialNumber) {

   // Enumerate the directores in the current path and store the file reference
   // numbers in the database. Call this function recursively to enumerate into
   // the subdirectores.

   CString szPath2(pszPath);
   szPath2 += TEXT("\\");
   HANDLE hDir = CreateFile(szPath2, 0, FILE_SHARE_READ | FILE_SHARE_WRITE,
      NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);

   // NOTE: If we cannot get a handle to this, there is no need to recurse into
   // it (even though it might be possible). Example - we couldn't open it, but
   // FindFirst(...) succeeds for some reason. We don't care about any extra
   // information. We can get by FindFirst inside this directory since we won't
   // have the FileIndex of this directory. Therefore, any objects under this
   // directory will not be able to have their names fully resolved (ie: when
   // we walk up the parent IDs for an object below this one, we won't be able
   // to find this ID)
   if (INVALID_HANDLE_VALUE == hDir) {
      return;
   }

   BY_HANDLE_FILE_INFORMATION fi;
   GetFileInformationByHandle(hDir, &fi);
   CloseHandle(hDir);

   if (0 == dwVolumeSerialNumber) {
      dwVolumeSerialNumber = fi.dwVolumeSerialNumber;
   }

   if (fi.nNumberOfLinks != 1) {
      ASSERT(FALSE);
   }

   if (dwVolumeSerialNumber != fi.dwVolumeSerialNumber) {
      // We've wandered onto another volume. This should only
      // happen if we recursed into a reparse point, but we check
      // for that below!
      ASSERT(FALSE);
      return;
   }

   LARGE_INTEGER index;
   index.LowPart = fi.nFileIndexLow;
   index.HighPart = fi.nFileIndexHigh;

   if (!Add(index.QuadPart, pszFile, ParentIndex)) {
      // Duplicate - The FRN for this directory was already
      // in our database. This shouldn't happen, but if it
      // does, there's no need to recurse into subdirectories
      return;
   }

   TCHAR szTempPath[_MAX_PATH];
   lstrcpy(szTempPath, pszPath);
   lstrcat(szTempPath, TEXT("\\*.*"));

   WIN32_FIND_DATA fd;
   HANDLE hSearch = FindFirstFile(szTempPath, &fd);
   if (INVALID_HANDLE_VALUE == hSearch) {
      // Something went wrong trying to enumerate the files and directories
      // in the current directory
      return;
   }

   do {
      TCHAR szTempRecursePath[_MAX_PATH];
      lstrcpy(szTempRecursePath, pszPath);
      lstrcat(szTempRecursePath, TEXT("\\"));
      lstrcat(szTempRecursePath, fd.cFileName);
      if (   (0 != (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
          && (0 != lstrcmp(fd.cFileName, TEXT(".")))
          && (0 != lstrcmp(fd.cFileName, TEXT("..")))
          && (0 == (fd.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)) ) {

         RecursePath(szTempRecursePath, fd.cFileName, index.QuadPart,
            dwVolumeSerialNumber);
      }
   } while (FindNextFile(hSearch, &fd) != FALSE);

   FindClose(hSearch);
}

///////////////////////////////// End Of File /////////////////////////////////