
/******************************************************************************\
*       This is a part of the Microsoft Source Code Samples. 
*       Copyright (C) 1993 Microsoft Corporation.
*       All rights reserved. 
*       This source code is only intended as a supplement to 
*       Microsoft Development Tools and/or WinHelp documentation.
*       See these sources for detailed information regarding the 
*       Microsoft samples programs.
\******************************************************************************/

/****************************************************************************\
* MODULE:       sidclean.c
*
*               NT never deletes SIDs, so the name of this sample is most
*                 accurately be intrepreted as "Clean up SID ownership and
*                 ACE's that relate to SIDs that still (and always will)
*                 exist, but for which the corresponding user account has been
*                 deleted"
*
*
* PURPOSE:      Demonstrate some of the Win32 security api(s), and provide a
*                 sample of how a utility could be written that recovers
*                 on-disk resources remaining allocated to deleted user
*                 accounts.  The on-disk resources recovered are 1) Files that
*                 are still owned by accounts that have been deleted are
*                 assigned ownership to the account logged on when this sample
*                 is run, and 2) ACE's for deleted accounts are edited
*                 (deleted) out of the ACLs of files to which the deleted
*                 accounts had been granted authorizations (eg., Read access)
*
*               It may be that running this sample as a utility has no
*                 practical value in many environments, as the number of files
*                 belonging to deleted user accounts will often be quite
*                 small, and the number of bytes recovered on disk by editing
*                 out ACEs for deleted accounts may well not be worth the time
*                 it takes to run this sample.  The time it takes to run this
*                 sample may be quite significant when processing an entire
*                 hard disk or partition
*
*               This sample is not a supported utility
*
*
* TO RUN:       You must log on using an account, such as Administrator, that
*                 has the priviledges to take file ownership and edit ACls
*
*               The ACL editing part of this sample can only be excercised for
*                 files on a partition that has ACLs NT processes:  NTFS
*
*               Typical test scenario:  Create a user account or two, log on
*                 as each of these accounts in turn, while logged on for each
*                 account, go to an NTFS partition, create a couple of files
*                 so the test accounts each own a few files, use the file
*                 manager to edit permissions for those files so that each
*                 test user has some authorities (e.g., Read) explicitly
*                 granted for those files.  Logon as Administrator, authorize
*                 each test user to a few Administrator-owned files.  Delete
*                 the test accounts.  Run the sample in the directories where
*                 you put the files the test accounts owned or were authorized
*                 to
*
*
* OVERALL APPROACH: The command line interface is kept inflexible to simplify
*                 it's parsing in this sample.  The user must pass in a switch
*                 argument, a directory spec, and a file search pattern
*
*               The sample positions the current directory (of the process the
*                 sample runs in) to the dir spec, and uses FindFirstFile and
*                 FindNextFile to walk through the directory specified looking
*                 for files that match the file pattern specified
*
*               The switch argument can cause subdirectories to be searched
*                 recursively
*
*               The switch argument lets the user choose only to take
*                 ownerships, only to edit ACLs, do both, or do neither, in
*                 which case the sample merely reports on what ownerships
*                 would have been taken, and what ACE's would have been
*                 deleted
*
*               As the directories are walked, each file that matches the
*                 file pattern is processed right then
*
*               Note that we process files in a directory, and we also process
*                 the directory itself that contains the files.  We process
*                 directories because they can also be owned by deleted
*                 accounts, or could have ACEs that will no longer be used
*
*               Note also that we process all directories that we check for
*                 files, regardless of the spelling of the directory name
*
*               Counters are kept of file ownerships taken, ACEs deleted and
*                 total files checked, to print a summary line at the end of
*                 the run
*
*               The sample considers it perfectly acceptable if 0 files match
*                 the file pattern for the entire run
*
*
* FUNCTIONS:  DoMatchingFilesInOneDir
*
*               Look in one dir or sub-dir for files that match the file
*                 pattern.  For each match call DoOneFileOrDir
*
*             DoAllDirsInOneDir
*
*               For all the sub-dirs in a dir, set the current directory to be
*                 that directory, check for files that match the file pattern,
*                 and if any match, call DoMatchingFilesInOneDir to process
*                 those.  Then reset the current directory
*
*             GetFullFileOrDirName
*
*               Get the full name of the file or dir for simplified processing
*                 (and for display on the console)
*
*             DoOneFileOrDir
*
*               Get the file's SD (Security Descriptor), and call
*                 TakeOwnershipIfAppropriate and/or DeleteACEsAsAppropriate as
*                 needed
*
*             TakeOwnershipIfAppropriate
*
*               Get the owning SID of the file from the file's SD that
*                 DoOneFileOrDir passed in, check that SID to see if the
*                 account is deleted.  If so, edit into the file's SD a new
*                 owning SID (the SID of the process running the sample).
*                 Then write the modified file SD to disk
*
*             DeleteACEsAsAppropriate
*
*               Get the DACL of the file from the file's SD that
*                 DoOneFileOrDir passed in.  Walk through the ACE list for
*                 that DACL, checking each ACE to see what SID the ACE refers
*                 to.  For the SID referred to, check to see if the account is
*                 deleted.  If so, delete that ACE from the DACL.  When all
*                 ACE's have been examined, write the new DACL into the file's
*                 SD.  Then write the modified file SD to disk
*
*             GetProcessSid
*
*               Retrieve into a global variable the SID of the user account
*                 logged on when this sample is run.  This is the SID used by
*                 TakeOwnershipIfAppropriate
*
*               NOTE:  This routine has the notable side-effect on the access
*                 token of the curent process of enabling two privileges:
*                 SeTakeOwnershipPrivilege, and SeSecurityPrivilege.
*                 SeTakeOwnershipPrivilege is needed to ensure we can take
*                 ownership in spite of any DACL on the file.
*                 SeSecurityPrivilege is needed to work with SACLs
*
*             CrackArgs
*
*               Process the command line, cracking (parsing/decoding) the
*                 switch argument into boolean global variables (see below).
*                 Call DisplayHelp if anything illegal is found in the command
*                 line, or if the user asked for help
*
*             DisplayHelp
*
*               Display help text on the console
*
*
* GLOBAL VARS:
*             BOOL  bTakeOwnership
*             BOOL  bEditACLs
*             BOOL  bRecurse
*             BOOL  bJustCount
*
*               These store the values the user specified on the command
*                 line's first argument (the switches argument).
*                 Respectively, they record whether we are to do the
*                 processing to Take Ownerships, Edit ACLs, whether we are to
*                 recurse into all subdirectories, and whether we are just
*                 counting up what would be processed (in which case we take
*                 no ownerships and edit no ACLs)
*
*             DWORD dwFilesChecked
*             DWORD dwFilesOwned
*             DWORD dwACEsDeleted
*
*               These count, respecively, the total files we checked, the
*                 number of files we took ownership of (or would have if we
*                 had not been told only to count), and the number of ACEs we
*                 deleted (or would have if we had not been told only to
*                 count).  Note that the total number of files checked does
*                 not include files in the directories we process that do not
*                 match the file pattern
*
*               Note, however, that we process directories regardless of
*                 whether they match the file pattern
*
*             UCHAR ucProcessSIDBuf
*             PSID  psidProcessOwnerSID
*
*               These store the SID of the account logged on as this sample
*                 runs, and a pointer to that SID
*
\****************************************************************************/


/****************************************************************************\
*  INCLUDES, DEFINES, TYPEDEFS
\****************************************************************************/
#define STRICT
#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define PERR(api) printf("%s: Error %d from %s on line %d\n",  \
    __FILE__, GetLastError(), api, __LINE__);
#define PMSG(msg) printf("%s line %d: %s\n",  \
    __FILE__, __LINE__, msg);

#define PrintAppStyleAPIError(ApiTxt,MsgTxt) {                     \
  DWORD dwLastError;                                               \
  dwLastError = GetLastError();                                    \
  switch (dwLastError)                                             \
  { case ERROR_FILE_NOT_FOUND :                                    \
      printf("\nFile not found (%s) line %d",MsgTxt,__LINE__);     \
      break;                                                       \
    case ERROR_INVALID_NAME   :                                    \
      printf("\nInvalid name (%s) line %d",MsgTxt,__LINE__);       \
      break;                                                       \
    case ERROR_PATH_NOT_FOUND :                                    \
      printf("\nError path not found (%s) line %d",MsgTxt,__LINE__); \
      break;                                                       \
    case ERROR_SHARING_VIOLATION :                                 \
      printf("\nSharing violation - shut down net and/or stop other sessions (%s) line %d",MsgTxt,__LINE__); \
      break;                                                       \
    case ERROR_ACCESS_DENIED  :                                    \
      printf("\nAccess denied (%s) line %d",MsgTxt,__LINE__);      \
      break;                                                       \
    default                   :                                    \
      printf("\n" #ApiTxt " - unexpected return code=%d (%s) line %d",dwLastError,MsgTxt,__LINE__); \
      break;                                                       \
  }                                                                \
  }

/****************************************************************************\
* GLOBAL VARIABLES
\****************************************************************************/

BOOL  bTakeOwnership  = FALSE;
BOOL  bEditACLs       = FALSE;
BOOL  bRecurse        = FALSE;
BOOL  bJustCount      = FALSE;

DWORD dwFilesChecked  = 0;
DWORD dwFilesOwned    = 0;
DWORD dwACEsDeleted   = 0;

PSID  psidProcessOwnerSID;


/****************************************************************************\
* FUNCTION PROTOTYPES
\****************************************************************************/

BOOL DoMatchingFilesInOneDir(HANDLE          hFound,
                             WIN32_FIND_DATA ffdFoundData);
BOOL DoAllDirsInOneDir(char *FilePattern);
BOOL GetFullFileOrDirName(LPTSTR lpszFileName);
BOOL DoOneFileOrDir(LPTSTR lpszFullName);
BOOL TakeOwnershipIfAppropriate(PSECURITY_DESCRIPTOR psdFileSD,
                                LPTSTR  lpszFullName);
BOOL DeleteACEsAsAppropriate   (PSECURITY_DESCRIPTOR psdFileSD,
                                LPTSTR  lpszFullName);
BOOL GetProcessSid(VOID);
BOOL CrackArgs(UINT argc, char *argv[]);
VOID DisplayHelp(VOID);

/****************************************************************************\
*
* FUNCTION: Main
*
\****************************************************************************/

UINT main(UINT argc, char *argv[])
{
  WIN32_FIND_DATA ffdFoundData;
  HANDLE          hFound;
  #define                   SZ_NAME_BUF MAX_PATH
  UCHAR           ucPathBuf[SZ_NAME_BUF];
  LPTSTR          lpszFullName = (LPTSTR)&ucPathBuf;

  /**************************************************************************\
  *
  * Store the process's SID in a global variable for later use (in taking
  *   ownership).
  *
  \**************************************************************************/

  if (!GetProcessSid())
  { PERR("Can't proceed without process SID - see earlier error messages");
    return(1);
  }

  if (!CrackArgs(argc,argv))
    return(1);

  /**************************************************************************\
  *
  * CrackArgs has set our global processing switches, and proven argv[2] and
  *   argv[3] are our non-blank dir-spec and file-pattern strings.  Now we
  *   must see that the file-spec is acceptable to the Win32 api's.  Argv[2]
  *   is the file-spec to pass to SetCurrentDirectory, and argv[3] is the
  *   file-pattern to pass to FindFirstFile
  *
  * First we have to expand the dir-spec in argv[2], because if we set the
  *   current directory to it before expansion,the expansion will have a
  *   different result if argv[2] is something like ..\..
  *
  \**************************************************************************/

  strcpy(lpszFullName,argv[2]);

  if (!GetFullFileOrDirName(lpszFullName))
  { PERR("Failed to expand to full name the 2nd argument (directory specification)");
    return(1);
  }

  /**************************************************************************\
  *
  * Now we pass the un-expanded argv[2] to SetCurrentDirectory for validity
  *   checking.  GetFullPathName (called by GetFullFileOrDirName) does not
  *   validity check
  *
  \**************************************************************************/

  if (!SetCurrentDirectory(argv[2]))
  { PrintAppStyleAPIError(SetCurrentDirectory,"2nd argument (directory specification)");
    return(1);
  }

  /**************************************************************************\
  *
  * We begin processing with the current directory, using the expanded form we
  *   got before.  We have to use the expanded form, because if we set to
  *   ..\.. and then try to process the string ..\.. as a dir name, instead of
  *   processing the dir two levels up from where we are we'll process the dir
  *   four levels up
  *
  \**************************************************************************/

  if (!DoOneFileOrDir(lpszFullName))
    return(1);

  /**************************************************************************\
  *
  * It's OK to get no hits.  The files-checked counter will show how many
  *   files we looked at, and it's OK to look at 0
  *
  * On the else branch, Argv[3] has been verified, and we have a good handle.
  *   We now pass to DoMatchingFilesInOneDir for processing the handle and
  *   found data we just got from FindFirstFile
  *
  \**************************************************************************/

  hFound = FindFirstFile(argv[3],
                         (LPWIN32_FIND_DATA)&ffdFoundData);
  if ((HANDLE)(-1) == hFound)
  { if (GetLastError() != ERROR_FILE_NOT_FOUND)
    { PrintAppStyleAPIError(FindFirstFile,"3rd argument");
      return(1);
    }
  }
  else if (!DoMatchingFilesInOneDir(hFound,ffdFoundData))
    return(1);

  /**************************************************************************\
  *
  * Pass the original file pattern for recursive calling to DoAllDirsInOneDir
  *
  \**************************************************************************/

  if (!DoAllDirsInOneDir(argv[3]))
    return(1);

  if (bJustCount)
    printf("\nChecked %d files, would have taken ownership of %d files, would have deleted %d ACEs\n",
           dwFilesChecked,dwFilesOwned,dwACEsDeleted);
  else
    printf("\nChecked %d files, took ownership of %d files, deleted %d ACEs\n",
           dwFilesChecked,dwFilesOwned,dwACEsDeleted);

  free(psidProcessOwnerSID);

  return(0);
}

/****************************************************************************\
*
* FUNCTION: DoMatchingFilesInOneDir
*
\****************************************************************************/

BOOL DoMatchingFilesInOneDir(HANDLE          hFound,
                             WIN32_FIND_DATA ffdFoundData)
{
  BOOL bDoneWithHandle = FALSE;

  /**************************************************************************\
  *
  * Process all files referred to by the handle, but not including
  *   directories, because directories are handled with separate calls to
  *   DoOneFileOrDir.  Such separate calls are made as we are setting the
  *   current directory to be the directory to be processed
  *
  \**************************************************************************/

  while (!bDoneWithHandle)
  {
    if (!(FILE_ATTRIBUTE_DIRECTORY & ffdFoundData.dwFileAttributes))
    {
      if (!DoOneFileOrDir(ffdFoundData.cFileName))
        return(FALSE);
    }

    if (!FindNextFile(hFound,
                      (LPWIN32_FIND_DATA)&ffdFoundData))
      if (GetLastError() == ERROR_NO_MORE_FILES)
        bDoneWithHandle = TRUE;
      else
      { PrintAppStyleAPIError(FindNextFile,"on FindNext");
        return(FALSE);
      }
  }
}

/****************************************************************************\
*
* FUNCTION: DoAllDirsInOneDir
*
\****************************************************************************/

BOOL DoAllDirsInOneDir(char *FilePattern)
{
  HANDLE          hFound;
  WIN32_FIND_DATA ffdFoundData;
  BOOL            bDoneWithHandle = FALSE;

  /**************************************************************************\
  *
  * If not recursing into dirs, simply return
  *
  \**************************************************************************/

  if (!bRecurse)
    return TRUE;

  /**************************************************************************\
  *
  * Since we are recursing, get a handle that points to entire directory, and
  *   walk the handle picking off only directories to recurse into
  *
  \**************************************************************************/

  hFound = FindFirstFile("*.*",
                         (LPWIN32_FIND_DATA)&ffdFoundData);
  if ((HANDLE)(-1) == hFound)
  { PrintAppStyleAPIError(FindFirstFile,"on dir *.* FindFirst");
    return(FALSE);
  }

  while (!bDoneWithHandle)
  {
    /************************************************************************\
    *
    * We only do dirs here, and we only do directories with textual names
    *   (i.e., not "." and not "..")
    *
    \************************************************************************/

    if (   (FILE_ATTRIBUTE_DIRECTORY & ffdFoundData.dwFileAttributes)
        && (0 != strcmp("." ,ffdFoundData.cFileName))
        && (0 != strcmp("..",ffdFoundData.cFileName)))
    {
      HANDLE          hFile2;
      WIN32_FIND_DATA ffdFound2;

      /**********************************************************************\
      *
      * We begin processing the new current directory by processing it itself,
      *   then setting the current dir to be the dir itself
      *
      \**********************************************************************/

      if (!DoOneFileOrDir(ffdFoundData.cFileName))
        return(FALSE);

      if (!SetCurrentDirectory(ffdFoundData.cFileName))
      { PrintAppStyleAPIError(SetCurrentDirectory,"recursive set");
        return(FALSE);
      }

      /**********************************************************************\
      *
      * It's OK to get no hits.  The files-checked counter will show how many
      *   files we looked at, and it's OK to look at 0
      *
      \**********************************************************************/

      hFile2 = FindFirstFile(FilePattern,
                             (LPWIN32_FIND_DATA)&ffdFound2);
      if ((HANDLE)(-1) == hFile2)
      { if (GetLastError() != ERROR_FILE_NOT_FOUND)
        { PrintAppStyleAPIError(FindFirstFile,"during recursion");
          return(FALSE);
        }
      }
      else if (!DoMatchingFilesInOneDir(hFile2,ffdFound2))
        return(FALSE);

      if (!DoAllDirsInOneDir(FilePattern))
        return(FALSE);

      if (!SetCurrentDirectory(".."))
      { PrintAppStyleAPIError(SetCurrentDirectory,"un-recursive set");
        return(FALSE);
      }
    }

    /************************************************************************\
    *
    * Get next recursion candidate (file or dir at this point, however at loop
    *   top files are screened out)
    *
    \************************************************************************/

    if (!FindNextFile(hFound,
                      (LPWIN32_FIND_DATA)&ffdFoundData))
      if (GetLastError() == ERROR_NO_MORE_FILES)
        bDoneWithHandle = TRUE;
      else
      { PrintAppStyleAPIError(FindNextFile,"on dir *.* FindNext");
        return(FALSE);
      }
  }
  return(TRUE);
}

/****************************************************************************\
*
* FUNCTION: GetFullFileOrDirName
*
\****************************************************************************/

BOOL GetFullFileOrDirName(LPTSTR lpszFileName)
{
  UCHAR   ucPathBuf[SZ_NAME_BUF];
  DWORD   dwSzReturned;
  LPTSTR  lpszLastNamePart;
  LPTSTR  lpszFullName;

  dwSzReturned = GetFullPathName
                   (lpszFileName,
                    (DWORD)SZ_NAME_BUF,
                    (LPTSTR)&ucPathBuf,
                    (LPTSTR *)&lpszLastNamePart);
  if (0 == dwSzReturned)
    switch (GetLastError())
    { case ERROR_INVALID_NAME   :
        printf("\nError invalid file full-name (on GetFullPathName)");
        return(FALSE);
      default                   :
        PERR("GetFullPathName - unexpected return code");
        return(FALSE);
    }

  if (dwSzReturned > SZ_NAME_BUF)
  { PERR("GetFullPathName - buffer too small");
    return(FALSE);
  }

  lpszFullName = CharLower((LPTSTR)&ucPathBuf);

  if (!lpszFullName)
  { PERR("CharLower failure");
    return(FALSE);
  }

  /**************************************************************************\
  *
  * Copy the expanded and upper-case-shifted name to the buffer pointed to by
  *   the input argument
  *
  \**************************************************************************/

  strcpy(lpszFileName,lpszFullName);
}

/****************************************************************************\
*
* FUNCTION: DoOneFileOrDir
*
\****************************************************************************/

BOOL DoOneFileOrDir(LPTSTR lpszFullName)
{
  #define                                 SZ_REL_SD_BUF 1000
  #define                                 SZ_ABS_SD_BUF  500
  #define                                 SZ_DACL_BUF    500
  #define                                 SZ_SACL_BUF    500
  #define                                 SZ_SID_OWN_BUF 500
  #define                                 SZ_SID_PG_BUF  500
  UCHAR                ucBuf             [SZ_REL_SD_BUF];
  UCHAR                ucBufAbs          [SZ_ABS_SD_BUF];
  UCHAR                ucBufDacl         [SZ_DACL_BUF];
  UCHAR                ucBufSacl         [SZ_SACL_BUF];
  UCHAR                ucBufCtrl         [sizeof(PSECURITY_DESCRIPTOR_CONTROL)];
  UCHAR                ucBufSidOwn       [SZ_SID_OWN_BUF];
  UCHAR                ucBufSidPG        [SZ_SID_PG_BUF];
  DWORD                dwSDLength       = SZ_REL_SD_BUF;
  DWORD                dwDACLLength     = SZ_DACL_BUF;
  DWORD                dwSACLLength     = SZ_SACL_BUF;
  DWORD                dwSidOwnLength   = SZ_SID_OWN_BUF;
  DWORD                dwSidPGLength    = SZ_SID_PG_BUF;
  DWORD                dwSDLengthNeeded;
  PSECURITY_DESCRIPTOR psdSrelFileSD    = (PSECURITY_DESCRIPTOR)&ucBuf;
  PSECURITY_DESCRIPTOR psdAbsFileSD     = (PSECURITY_DESCRIPTOR)&ucBufAbs;
  PSECURITY_DESCRIPTOR_CONTROL psdcCtrl = (PSECURITY_DESCRIPTOR_CONTROL)&ucBufCtrl;
  PACL                 paclDacl         = (PACL)&ucBufDacl;
  PACL                 paclSacl         = (PACL)&ucBufSacl;
  PSID                 psidSidOwn       = (PSID)&ucBufSidOwn;
  PSID                 psidSidPG        = (PSID)&ucBufSidPG;
  BOOL                 bDaclPresent;
  BOOL                 bDaclDefaulted;
  BOOL                 bSaclPresent;
  BOOL                 bSaclDefaulted;
  BOOL                 bOwnerDefaulted;
  BOOL                 bGroupDefaulted;
  BOOL                 bSDSelfRelative;
  DWORD                dwRevision;

  if (!GetFullFileOrDirName(lpszFullName))
    return(FALSE);

  /**************************************************************************\
  *
  * Now the input argument's name is accurate:  it is expanded and lower-case
  *
  \**************************************************************************/

  printf("\nChecking %s",lpszFullName);

  dwFilesChecked++;

  if (!GetFileSecurity
        (lpszFullName,
         (SECURITY_INFORMATION)( OWNER_SECURITY_INFORMATION
                               | GROUP_SECURITY_INFORMATION
                               | DACL_SECURITY_INFORMATION
                               | SACL_SECURITY_INFORMATION),
         psdSrelFileSD,
         dwSDLength,
         (LPDWORD)&dwSDLengthNeeded))
  { PERR("GetFileSecurity");
    return(FALSE);
  }

  /**************************************************************************\
  *
  * This validity check is here for demonstration pruposes.  It's not likely a
  *   real app would need to check the validity of this returned SD.  The
  *   validity check APIs are more intended to check validity after app code
  *   has manipulated the structure and is about to hand it back to the system
  *
  \**************************************************************************/

  if (!IsValidSecurityDescriptor(psdSrelFileSD))
  { PERR("IsValidSecurityDescriptor said bad SD");
    return(FALSE);
  }

  /**************************************************************************\
  *
  *  Build File SD in absolute format for potential later modification
  *
  *  First Initialize a new SD, which is by definition in absolute format
  *
  *  Then Set in the fields from the relative format SD we just fetched
  *
  \**************************************************************************/

  if (!InitializeSecurityDescriptor(psdAbsFileSD,
                 SECURITY_DESCRIPTOR_REVISION))
  { PERR("InitializeSecurityDescriptor");
    return FALSE;
  }

  /**************************************************************************\
  *
  * Get Control from relative format File SD
  *
  * This control info isn't much queried in the code that follows, as the
  *   Get/Set calls are more convienent in this case, but it does give us a
  *   change to verify that the SD is in relative format
  *
  \**************************************************************************/

  if (!GetSecurityDescriptorControl(psdSrelFileSD,
          psdcCtrl,
          &dwRevision))
  { PERR("GetSecurityDescriptorControl");
    return FALSE;
  }

  bSDSelfRelative = (SE_SELF_RELATIVE & *psdcCtrl);

  /**************************************************************************\
  *
  * Set DACL into absolute format File SD
  *
  * Note that it is possible that a NULL DACL has been explictly specified.
  *   If so the Get/Set call pair will correctly map that into the absolute
  *   format SD
  *
  * The next if statement isn't necessary, it simply shows the relationship
  *   between SE_DACL_PRESENT and SE_DACL_DEFAULTED, and lets you trace
  *   through with the debugger
  *
  \**************************************************************************/

  if (bDaclPresent = (SE_DACL_PRESENT   & *psdcCtrl))
  {                // SE_DACL_DEFAULTED ignored if SE_DACL_PRESENT not set
    bDaclDefaulted = (SE_DACL_DEFAULTED & *psdcCtrl);
  }
  else
  { // No DACL at all
  }

  if (!GetSecurityDescriptorDacl(psdSrelFileSD,
          &bDaclPresent,      // fDaclPresent flag
          &paclDacl,
          &bDaclDefaulted))   // is/is not a default DACL
  { PERR("GetSecurityDescriptorDacl");
    return FALSE;
  }
  if (!SetSecurityDescriptorDacl(psdAbsFileSD,
          bDaclPresent,       // fDaclPresent flag
          paclDacl,
          bDaclDefaulted))    // is/is not a default DACL
  { PERR("SetSecurityDescriptorDacl");
    return FALSE;
  }

  /**************************************************************************\
  *
  * Set SACL into absolute format File SD
  *
  * Note that it is possible that a NULL SACL has been explictly specified.
  *   If so the Get/Set call pair will correctly map that into the absolute
  *   format SD
  *
  * The next if statement isn't necessary, it simply shows the relationship
  *   between SE_SACL_PRESENT and SE_SACL_DEFAULTED, and lets you trace
  *   through with the debugger
  *
  \**************************************************************************/

  if (bSaclPresent = (SE_SACL_PRESENT   & *psdcCtrl))
  {                // SE_SACL_DEFAULTED ignored if SE_SACL_PRESENT not set
    bSaclDefaulted = (SE_SACL_DEFAULTED & *psdcCtrl);
  }
  else
  { // No SACL at all
  }

  if (!GetSecurityDescriptorSacl(psdSrelFileSD,
          &bSaclPresent,      // fSaclPresent flag
          &paclSacl,
          &bSaclDefaulted))   // is/is not a default SACL
  { PERR("GetSecurityDescriptorSacl");
    return FALSE;
  }
  if (!SetSecurityDescriptorSacl(psdAbsFileSD,
          bSaclPresent,       // fSaclPresent flag
          paclSacl,
          bSaclDefaulted))    // is/is not a default SACL
  { PERR("SetSecurityDescriptorSacl");
    return FALSE;
  }

  /**************************************************************************\
  *
  * Set Owner into absolute format File SD
  *
  * The next if statement isn't necessary, it simply let's you trace through
  *   with the debugger
  *
  \**************************************************************************/

  bOwnerDefaulted = (SE_OWNER_DEFAULTED & *psdcCtrl);

  if (!GetSecurityDescriptorOwner(psdSrelFileSD,
          &psidSidOwn,
          &bOwnerDefaulted))   // is/is not a default Owner
  { PERR("GetSecurityDescriptorOwner");
    return FALSE;
  }
  if (!SetSecurityDescriptorOwner(psdAbsFileSD,
          psidSidOwn,
          bOwnerDefaulted))    // is/is not a default Owner
  { PERR("SetSecurityDescriptorOwner");
    return FALSE;
  }

  /**************************************************************************\
  *
  * Set Group into absolute format File SD
  *
  * The next if statement isn't necessary, it simply let's you trace through
  *   with the debugger
  *
  \**************************************************************************/

  bGroupDefaulted = (SE_GROUP_DEFAULTED & *psdcCtrl);

  if (!GetSecurityDescriptorGroup(psdSrelFileSD,
          &psidSidOwn,
          &bGroupDefaulted))   // is/is not a default Group
  { PERR("GetSecurityDescriptorGroup");
    return FALSE;
  }
  if (!SetSecurityDescriptorGroup(psdAbsFileSD,
          psidSidOwn,
          bGroupDefaulted))    // is/is not a default Group
  { PERR("SetSecurityDescriptorGroup");
    return FALSE;
  }

  /**************************************************************************\
  *
  * This validity check is here for demonstration pruposes.  It's not likely a
  *   real app would need to check the validity of the SD after it was just
  *   built into absolute format.  The validity check APIs are more intended
  *   to check validity after app code has manipulated the structure and is
  *   about to hand it back to the system
  *
  * One thing to notice is that IsValidSecurityDescriptor will succeed on both
  *   self-relative and absolute format SDs.  However, some other api's, such
  *   as SetSecurityDescriptorOwner, require the SD to be in a certain format,
  *   and will give a return code of Invalid SD if the SD passed to the api is
  *   valid, but in the wrong format.  In other words, when an api such as
  *   SetSecurityDescriptorOwner gives the retun code Invalid SD, this doesn't
  *   mean the SD passed in was necessarily invalid.  It might have been in
  *   the wrong format
  *
  \**************************************************************************/

  if (!IsValidSecurityDescriptor(psdAbsFileSD))
  { PERR("IsValidSecurityDescriptor said bad SD");
    return(FALSE);
  }

  if (bTakeOwnership)
    if (!TakeOwnershipIfAppropriate(psdAbsFileSD,lpszFullName))
      return(FALSE);

  if (bEditACLs)
    if (!DeleteACEsAsAppropriate   (psdAbsFileSD,lpszFullName))
      return(FALSE);

  return(TRUE);
}

/****************************************************************************\
*
* FUNCTION: TakeOwnershipIfAppropriate
*
\****************************************************************************/

BOOL TakeOwnershipIfAppropriate(PSECURITY_DESCRIPTOR psdFileSD,
                                LPTSTR  lpszFullName)
{
  PSID psidFileOwnerSID;
  {
    BOOL  bOwnerDefaulted;

    if (!GetSecurityDescriptorOwner
           (psdFileSD,
            (PSID *)&psidFileOwnerSID,
            (LPBOOL)&bOwnerDefaulted))
    { PERR("GetSecurityDescriptorOwner");
      return(FALSE);
    }

    /************************************************************************\
    *
    * This validity check is here for demonstration pruposes.  It's not likely
    *   a real app would need to check the validity of this returned SID.  The
    *   validity check APIs are more intended to check validity after app code
    *   has manipulated the structure and is about to hand it back to the
    *   system
    *
    \************************************************************************/

    if (!IsValidSid(psidFileOwnerSID))
    { PERR("IsValidSid said bad SID!");
      return(FALSE);
    }
  }

  {
    DWORD        dwLastError   = NO_ERROR;
    #define                      SZ_ACCT_NAME_BUF 1000
    UCHAR        ucNameBuf      [SZ_ACCT_NAME_BUF];
    DWORD        dwNameLength  = SZ_ACCT_NAME_BUF;
    #define                      SZ_DMN_NAME_BUF  1000
    UCHAR        ucDomainNmBuf  [SZ_DMN_NAME_BUF ];
    DWORD        dwDNameLength = SZ_DMN_NAME_BUF ;
    SID_NAME_USE peAcctNameUse;

    if (!LookupAccountSid
           ((LPTSTR)"",             // Look on local machine
           psidFileOwnerSID,
           (LPTSTR)&ucNameBuf,
           (LPDWORD)&dwNameLength,
           (LPTSTR)&ucDomainNmBuf,
           (LPDWORD)&dwDNameLength,
           (PSID_NAME_USE)&peAcctNameUse))
    { dwLastError = GetLastError();
      if (ERROR_NONE_MAPPED != dwLastError)
      { PERR("LookupAccountSID");
        return(FALSE);
      }
    }

    /************************************************************************\
    *
    * If deleted account, take ownership.  This routine's caller checked the
    *   global switches that said we are in take ownership mode
    *
    * In some cases, the account lookup will fail with ERROR_NONE_MAPPED,
    *   meaning there is no deleted account mapped to the SID, in which case
    *   we also take ownership
    *
    *   We check that first to avoid referencing peAcctNameUse, which in that
    *     case is not set
    *
    \************************************************************************/

    if (  (ERROR_NONE_MAPPED == dwLastError)
       || (SidTypeDeletedAccount == peAcctNameUse))
    {
      dwFilesOwned++;

      if (bJustCount)
      { printf(" - would have taken ownership");
        return(TRUE);
      }
      else
      {
        /********************************************************************\
        *
        * Modify the SD in virtual memory.  No check on the new owning SID
        *   here, because we validity checked it when we fetched it in
        *   GetProcessSid
        *
        \********************************************************************/

        if (!SetSecurityDescriptorOwner
               (psdFileSD,
                psidProcessOwnerSID,
                FALSE))               // New owner explicitly specified
        { PERR("SetSecurityDescriptorOwner");
          return(FALSE);
        }

        /********************************************************************\
        *
        *  This validity check is something a real app might actually like to
        *    do.  We manupulated the SD, so before we write it back out to the
        *    file system, a check is worth considering.
        *
        \********************************************************************/

        if (!IsValidSecurityDescriptor(psdFileSD))
        { PERR("IsValidSecurityDescriptor said bad SD");
          return(FALSE);
        }

        /********************************************************************\
        *
        * Modify the SD on the hard disk
        *
        \********************************************************************/

        if (!SetFileSecurity
               (lpszFullName,
                (SECURITY_INFORMATION)OWNER_SECURITY_INFORMATION,
                psdFileSD))
        { PERR("SetFileSecurity");
          return(FALSE);
        }

        printf(" - took ownership");
      }
    }
  }

  return(TRUE);

}

/****************************************************************************\
*
* FUNCTION: DeleteACEsAsAppropriate
*
\****************************************************************************/

BOOL DeleteACEsAsAppropriate(PSECURITY_DESCRIPTOR psdFileSD,
                             LPTSTR  lpszFullName)
{
  PACL                 paclFile;
  BOOL                 bHasACL;
  BOOL                 bOwnerDefaulted;
  DWORD                dwAcl_i;
  DWORD                dwACEsDeletedBeforeNow;

  ACL_SIZE_INFORMATION                      asiAclSize;
  DWORD                dwBufLength = sizeof(asiAclSize);
  ACCESS_ALLOWED_ACE   *paaAllowedAce;

  if (!GetSecurityDescriptorDacl(psdFileSD,
                                 (LPBOOL)&bHasACL,
                                 (PACL *)&paclFile,
                                 (LPBOOL)&bOwnerDefaulted))
  { PERR("GetSecurityDescriptorDacl");
    return(FALSE);
  }

  if (!bHasACL)  // No ACL to process, so OK, return
    return(TRUE);

  /**************************************************************************\
  *
  * This validity check is here for demonstration pruposes.  It's not likely a
  *   real app would need to check the validity of this returned ACL.  The
  *   validity check APIs are more intended to check validity after app code
  *   has manipulated the structure and is about to hand it back to the system
  *
  \**************************************************************************/

  if (!IsValidAcl(paclFile))
  { PERR("IsValidAcl said bad ACL!");
    return(FALSE);
  }

  if (!GetAclInformation(paclFile,
                         (LPVOID)&asiAclSize,
                         (DWORD)dwBufLength,
                         (ACL_INFORMATION_CLASS)AclSizeInformation))
  { PERR("GetAclInformation");
    return(FALSE);
  }

  dwACEsDeletedBeforeNow = dwACEsDeleted;

  /**************************************************************************\
  *
  * We loop through in reverse order, because that's simpler, given that we
  *   potentially delete ACEs as we loop through.  If started at 0 and went
  *   up, if we deleted the 0th ACE, then the 1th ACE would become the 0th,
  *   and we'd have to check the 0th ACE again
  *
  \**************************************************************************/

  for (dwAcl_i = asiAclSize.AceCount-1;  ((int)dwAcl_i) >= 0;  dwAcl_i--)
  {
    /************************************************************************\
    *
    * It doesn't matter for this sample that we don't yet know the ACE type,
    *   because they all start with the header field and that's what we need
    *
    \************************************************************************/

    if (!GetAce(paclFile,
                dwAcl_i,
                (LPVOID *)&paaAllowedAce))
    { PERR("GetAce");
      return(FALSE);
    }

    /************************************************************************\
    *
    * There are only four Ace Types pre-defined, so this next check is
    *   redundant in a real app, but useful as a sanity check and a
    *   demonstration in a sample
    *
    \************************************************************************/

    if (!( (paaAllowedAce->Header.AceType == ACCESS_ALLOWED_ACE_TYPE)
         ||(paaAllowedAce->Header.AceType == ACCESS_DENIED_ACE_TYPE )
         ||(paaAllowedAce->Header.AceType == SYSTEM_AUDIT_ACE_TYPE  )
         ||(paaAllowedAce->Header.AceType == SYSTEM_ALARM_ACE_TYPE  )))
    { PERR("Invalid AceType");
      return(FALSE);
    }

    { // Find SID of ACE, check if acct deleted

      UCHAR        ucNameBuf      [SZ_ACCT_NAME_BUF];
      DWORD        dwNameLength  = SZ_ACCT_NAME_BUF;
      UCHAR        ucDomainNmBuf  [SZ_DMN_NAME_BUF];
      DWORD        dwDNameLength = SZ_DMN_NAME_BUF;
      SID_NAME_USE peAcctNameUse;
      DWORD        dwLastError   = NO_ERROR;

      /**********************************************************************\
      *
      * This validity check is here for demonstration pruposes.  It's not
      *   likely a real app would need to check the validity of the SID
      *   contained in the returned ACL.  The validity check APIs are more
      *   intended to check validity after app code has manipulated the
      *   structure and is about to hand it back to the system
      *
      \**********************************************************************/

      if (!IsValidSid((PSID)&(paaAllowedAce->SidStart)))
      { PERR("IsValidSid said bad SID!");
        return(FALSE);
      }

      if (!LookupAccountSid
             ((LPTSTR)"",         // Look on local machine
             (PSID)&(paaAllowedAce->SidStart),
             (LPTSTR)&ucNameBuf,
             (LPDWORD)&dwNameLength,
             (LPTSTR)&ucDomainNmBuf,
             (LPDWORD)&dwDNameLength,
             (PSID_NAME_USE)&peAcctNameUse))
      { dwLastError = GetLastError();
        if (ERROR_NONE_MAPPED != dwLastError)
        { PERR("LookupAccountSID");
          return(FALSE);
        }
      }

      if (  (ERROR_NONE_MAPPED == dwLastError)
         || (SidTypeDeletedAccount == peAcctNameUse))
      {
        dwACEsDeleted++;

        if (bJustCount)
        { printf(" - would have edited ACL");
          return(TRUE);
        }

        if (!DeleteAce(paclFile,dwAcl_i))
        { PERR("DeleteAce");
          return(FALSE);
        }
      }
    }
  }

  if (dwACEsDeletedBeforeNow < dwACEsDeleted)
  {
    /************************************************************************\
    *
    * This validity check is something a real app might actually like to do.
    *   We manupulated the ACL, so before we write it back into an SD, a check
    *   is worth considering
    *
    \************************************************************************/

    if (!IsValidAcl(paclFile))
    { PERR("IsValidAcl said bad ACL!");
      return(FALSE);
    }

    /************************************************************************\
    *
    * Modify the SD in virtual memory
    *
    \************************************************************************/

    if (!SetSecurityDescriptorDacl
           (psdFileSD,
            TRUE,                 // Yes, set the DACL
            paclFile,
            FALSE))               // New DACL explicitly specified
    { PERR("SetSecurityDescriptorDacl");
      return(FALSE);
    }

    /************************************************************************\
    *
    * This validity check is something a real app might actually like to do.
    *   We manupulated the SD, so before we write it back out to the file
    *   system, a check is worth considering
    *
    \************************************************************************/

    if (!IsValidSecurityDescriptor(psdFileSD))
    { PERR("IsValidSecurityDescriptor said bad SD");
      return(FALSE);
    }

    /************************************************************************\
    *
    * Modify the SD on the hard disk
    *
    \************************************************************************/

    if (!SetFileSecurity
           (lpszFullName,
            (SECURITY_INFORMATION)DACL_SECURITY_INFORMATION,
            psdFileSD))
    { PERR("SetFileSecurity");
      return(FALSE);
    }

    printf(" - edited ACL");
  }

  return(TRUE);

}

/****************************************************************************\
*
* FUNCTION: GetProcessSid
*
\****************************************************************************/

BOOL GetProcessSid(VOID)
{
  HANDLE               hProcess;
  PSECURITY_DESCRIPTOR psdProcessSD;
  PSID                 psidProcessOwnerSIDTemp;

  UCHAR                ucBuf       [SZ_REL_SD_BUF];
  DWORD                dwSDLength = SZ_REL_SD_BUF;
  DWORD                dwSDLengthNeeded;
  BOOL                 bOwnerDefaulted;

  hProcess = GetCurrentProcess();

  if (!hProcess)
  { PERR("GetCurrentProcess");
    return(FALSE);
  }

  psdProcessSD = (PSECURITY_DESCRIPTOR)ucBuf;

  if (!GetKernelObjectSecurity
         (hProcess,
          (SECURITY_INFORMATION)(OWNER_SECURITY_INFORMATION),
          psdProcessSD,
          dwSDLength,
          (LPDWORD)&dwSDLengthNeeded))
  { PERR("GetKernelObjectSecurity on current process handle");
    return(FALSE);
  }

  /**************************************************************************\
  *
  * This validity check is here for demonstration purposes.  It's not likely a
  *   real app would need to check the validity of this returned SD.  The
  *   validity check APIs are more intended to check validity after app code
  *   has manipulated the structure and is about to hand it back to the system
  *
  \**************************************************************************/

  if (!IsValidSecurityDescriptor(psdProcessSD))
  { PERR("IsValidSecurityDescriptor said bad SD");
    return(FALSE);
  }

  if (!GetSecurityDescriptorOwner
         (psdProcessSD,
          (PSID *)&psidProcessOwnerSIDTemp,
          (LPBOOL)&bOwnerDefaulted))
  { PERR("GetSecurityDescriptorOwner of current process");
    return(FALSE);
  }

  /**************************************************************************\
  *
  * This validity check is here for demonstration pruposes.  It's not likely a
  *   real app would need to check the validity of this returned SID.  The
  *   validity check APIs are more intended to check validity after app code
  *   has manipulated the structure and is about to hand it back to the system
  *
  \**************************************************************************/

  if (!IsValidSid(psidProcessOwnerSIDTemp))
  { PERR("IsValidSid said bad process SID!");
    return(FALSE);
  }

  /**************************************************************************\
  *
  * On the other hand, we are about to call GetLengthSid on the returned SID,
  *   and calling GetLengthSid with an invalid SID is a bad idea, since then
  *   GetLengthSid's result is undefined, and an undefined result is hard to
  *   handle cleanly.  So, even in a real app, the above check on SID validity
  *   is a good idea to ensure the result GetLengthSid returns is valid
  *
  * It should be clear that the reason why the CopySid below is needed is that
  *   in the current routine the SID of the current process is on the stack
  *   (in the SD structure), so we have to copy the SID to static storage
  *   before the current routine returns
  *
  \**************************************************************************/

  { DWORD dwSIDLengthNeeded;

    dwSIDLengthNeeded = GetLengthSid(psidProcessOwnerSIDTemp);

    psidProcessOwnerSID = malloc(dwSIDLengthNeeded);

    if (NULL == psidProcessOwnerSID)
      PERR("GetProcessSid - ran out of heap space");

    if (!CopySid(dwSIDLengthNeeded,
                 psidProcessOwnerSID,
                 psidProcessOwnerSIDTemp))
    { PERR("CopySid");
      return(FALSE);
    }
  }


  /**************************************************************************\
  *
  * This validity check is here for demonstration pruposes only (see above).
  *
  \**************************************************************************/

  if (!IsValidSid(psidProcessOwnerSID))
  { PERR("IsValidSid said bad process SID!");
    return(FALSE);
  }


  /**************************************************************************\
  *
  * Now ensure that two privileges are enabled in the access token of the
  *   current process
  *
  \**************************************************************************/

  { HANDLE           hAccessToken;
    LUID             luidPrivilegeLUID;
    TOKEN_PRIVILEGES tpTokenPrivilege;

    if (!OpenProcessToken(hProcess,
                          TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
                          &hAccessToken))
    { PERR("OpenProcessToken");
      return(FALSE);
    }

    /************************************************************************\
    *
    * Get LUID of SeTakeOwnershipPrivilege privilege
    *
    \************************************************************************/

    if (!LookupPrivilegeValue(NULL,
                              "SeTakeOwnershipPrivilege",
                              &luidPrivilegeLUID))
    { PERR("LookupPrivilegeValue");
      printf("\nThe above error means you need to use User Manager (menu item");
      printf("\n  Policies\\UserRights) to turn on the 'Take ownership of...' ");
      printf("\n  privilege, log off, log back on");
      return(FALSE);
    }

    /************************************************************************\
    *
    * Enable the SeTakeOwnershipPrivilege privilege using the LUID just
    *   obtained
    *
    \************************************************************************/

    tpTokenPrivilege.PrivilegeCount = 1;
    tpTokenPrivilege.Privileges[0].Luid = luidPrivilegeLUID;
    tpTokenPrivilege.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

    AdjustTokenPrivileges (hAccessToken,
                           FALSE,  // Do not disable all
                           &tpTokenPrivilege,
                           sizeof(TOKEN_PRIVILEGES),
                           NULL,   // Ignore previous info
                           NULL);  // Ignore previous info

    if ( GetLastError() != NO_ERROR )
    { PERR("AdjustTokenPrivileges");
      return(FALSE);
    }

    /************************************************************************\
    *
    * Get LUID of SeSecurityPrivilege privilege
    *
    \************************************************************************/

    if (!LookupPrivilegeValue(NULL,
                              "SeSecurityPrivilege",
                              &luidPrivilegeLUID))
    { PERR("LookupPrivilegeValue");
      printf("\nThe above error means you need to log on as an Administrator");
      return(FALSE);
    }

    /************************************************************************\
    *
    * Enable the SeSecurityPrivilege privilege using the LUID just
    *   obtained
    *
    \************************************************************************/

    tpTokenPrivilege.PrivilegeCount = 1;
    tpTokenPrivilege.Privileges[0].Luid = luidPrivilegeLUID;
    tpTokenPrivilege.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

    AdjustTokenPrivileges (hAccessToken,
                           FALSE,  // Do not disable all
                           &tpTokenPrivilege,
                           sizeof(TOKEN_PRIVILEGES),
                           NULL,   // Ignore previous info
                           NULL);  // Ignore previous info

    if ( GetLastError() != NO_ERROR )
    { PERR("AdjustTokenPrivileges");
      return(FALSE);
    }

  }

  return(TRUE);

}

/****************************************************************************\
*
* FUNCTION: CrackArgs
*
\****************************************************************************/

BOOL CrackArgs(UINT argc, char *argv[])
{
  char *p;

  /**************************************************************************\
  *
  * There must be three arguments
  *
  \**************************************************************************/

  if (argc != 4)
  { DisplayHelp();
    return(FALSE);
  }

  p=argv[1];

  /**************************************************************************\
  *
  * The switch argument must be 2-5 chars long
  *
  \**************************************************************************/

  if ((strlen(p) < 2) || (strlen(p) > 5))
  { DisplayHelp();
    return(FALSE);
  }

  /**************************************************************************\
  *
  * The first char in the switch argument must be /
  *
  \**************************************************************************/

  if ('/' != *p)
  { DisplayHelp();
    return(FALSE);
  }

  /**************************************************************************\
  *
  * Chars 2-5 of the switch argument must be O or A or R or C
  *
  \**************************************************************************/

  for (p=p+1; *p; p++)
    switch (*p)
    { case 'o':
      case 'O':
        bTakeOwnership = TRUE;
        break;
      case 'a':
      case 'A':
        bEditACLs      = TRUE;
        break;
      case 'r':
      case 'R':
        bRecurse       = TRUE;
        break;
      case 'c':
      case 'C':
        bJustCount     = TRUE;
        break;
      default :
        DisplayHelp();
        return(FALSE);
    }

  /**************************************************************************\
  *
  * Have to say one of O or A
  *
  \**************************************************************************/

  if (!(bTakeOwnership || bEditACLs))
  { DisplayHelp();
    return(FALSE);
  }

  return(TRUE);
}

/****************************************************************************\
*
* FUNCTION: DisplayHelp
*
\****************************************************************************/

VOID DisplayHelp(VOID)
{
  printf("\nTo run type SIDCLEAN and 3 parameters.  Syntax:");
  printf("\n  SIDCLEAN /roah dirspec filepattern");
  printf("\n           /r    Recursively process subdirectories");
  printf("\n           /o    For any files matching filepattern: Take ownership if");
  printf("\n                   file currently owned by any deleted SID");
  printf("\n           /a    For any files matching filepattern: Edit ACL, deleting");
  printf("\n                   ACEs associated with any deleted SID");
  printf("\n           /c    Overrides /o and /a, causes counts of /a or /o actions that");
  printf("\n                   would take place if /c not used.  Counts always displayed");
  printf("\n           /h    Override other switch values, just display this message\n");
  printf("\n                 . and .. syntax allowed in dirspec");
  printf("\n                 * and ? wildcards allowed in filepattern");
  printf("\n                 Switch letters can be in any order, upper or lower case");
  printf("\nExamples:");
  printf("\n  SIDCLEAN /o  .  *.*  Take ownership of all files (but not subdirs) in ");
  printf("\n                         current dir that are owned by any deleted SID");
  printf("\n  SIDCLEAN /a  .  *.*  For any file in current dir (but not subdirs), delete");
  printf("\n                         any ACL info that is associated with any deleted SID");
  printf("\n  SIDCLEAN /ro .  *.*  Same as first  example, but also recursively process");
  printf("\n                         subdirectories");
  printf("\n  SIDCLEAN /ar .  *.*  Same as second example, but also recursively process");
  printf("\n                         subdirectories");
  printf("\n  SIDCLEAN /O  \\  *.*  Same as first  example, but process files in root");
  printf("\n                         of current drive");
  printf("\n  SIDCLEAN /oC .. *.*  Same as first  example, but looks at files in dir");
  printf("\n                         containing current dir, processes nothing, just counts");
  printf("\n  SIDCLEAN /A d:\\ *.*  Same as second example, but process files in root");
  printf("\n                         of D: drive");
  printf("\n  SIDCLEAN             Displays this message");
  printf("\n  SIDCLEAN /h          Displays this message (so do ? -? /? -h -H /H)\n");
  printf("\nThis utility must be run while logged on as Administrator\n");

  return;
}
