//
// SafeFile.cpp
//
//		Functions to help prevent opening unsafe files.
//
// History:
//
//		2002-03-18  KenSh     Created
//
// Copyright (c) 2002 Microsoft Corporation
//

#include "stdafx.h"
#include "SafeFile.h"
#include <strsafe.h>

//
// Hopefully most projects already define these; if not, ensure we still compile
//
#ifndef ASSERT
#define ASSERT(x)
#endif
#ifndef ARRAYSIZE
#define ARRAYSIZE(ar) (sizeof(ar)/sizeof((ar)[0]))
#endif

//
// Eliminate an unnecessary function call on Unicode builds
//
#ifndef CHARNEXT
#ifdef UNICODE
#define CHARNEXT(psz) (psz+1)
#else
#define CHARNEXT CharNextA
#endif
#endif

//
// Local function declarations
//
static inline BOOL IsSlashOrBackslash(IN TCHAR ch);
static inline BOOL IsSlashOrBackslash(IN TCHAR ch);
static BOOL SkipLangNeutralPrefix(IN LPCTSTR pszString, IN LPCTSTR pszPrefix, OUT LPCTSTR* ppszResult);
static BOOL MyPathFindNextComponent(IN LPCTSTR pszFileName, IN BOOL fAllowForwardSlash, OUT LPCTSTR* ppszResult);
static BOOL SkipPathDrivePart(IN LPCTSTR pszFileName, OUT OPTIONAL int* pcchDrivePart, OUT OPTIONAL BOOL* pfUNC, OUT OPTIONAL BOOL* pfExtendedSyntax);
static HRESULT CheckValidDriveType(IN LPCTSTR pszFileName, IN BOOL fAllowNetworkDrive, IN BOOL fAllowRemovableDrive);
static BOOL WINAPI DoesPathContainDotDot(IN LPCTSTR pszFileName);
static BOOL DoesPathContainStreamSyntax(IN LPCTSTR pszFileName);
static HRESULT CheckReparsePointPermissions(IN DWORD dwReparseType);


//============================================================================

static inline HRESULT GetLastErrorAsHresult()
{
	DWORD dwErr = GetLastError();
	return HRESULT_FROM_WIN32(dwErr);
}

// IsSlashOrBackslash [private]
//
//		Helper function to simplify code that checks for path separators.
//		Most places where backslash is valid, forward slash is also valid.
//
static inline BOOL IsSlashOrBackslash(IN TCHAR ch)
{
	return (ch == _T('\\') || ch == _T('/'));
}


// StrLenWithMax [private]
//
//		Returns the equivalent of min(lstrlen(pszString), cchMax)
//		but avoids most of the lstrlen when cchMax is small.
//
static int StrLenWithMax(IN LPCTSTR pszString, IN int cchMax)
{
	int cch = 0;
	while (*pszString && cch < cchMax)
		cch++;
	return cch;
}


// SkipLangNeutralPrefix [private]
//
//		Sets the out param to the new string pointer after skipping the prefix,
//		if the string starts with the prefix (case-insensitive). Otherwise sets
//		the out param to the start of the input string.
//
//		Returns TRUE if the prefix was found & skipped, otherwise FALSE.
//
static BOOL SkipLangNeutralPrefix(IN LPCTSTR pszString, IN LPCTSTR pszPrefix, OUT LPCTSTR* ppszResult)
{
	int cchPrefix = lstrlen(pszPrefix);
	int cchString = StrLenWithMax(pszString, cchPrefix);
	BOOL fResult = FALSE;

	if (CSTR_EQUAL == CompareString(MAKELCID(LANG_ENGLISH, SORT_DEFAULT), NORM_IGNORECASE,
							pszString, cchString, pszPrefix, cchPrefix))
	{
		fResult = TRUE;
		pszString += cchPrefix;
	}

	*ppszResult = pszString;
	return fResult;
}


// MyPathFindNextComponent [private]
//
//		Skips past the next component of the given path, including the slash or
//		backslash that follows it.
//
//		Sets the out param to the beginning of the next path component, or to
//		the end of string if there is no next path component.
//
//		Returns TRUE if a slash or backslash was found and skipped. Note that
//		the out param can be "" even if function returns TRUE.
//
static BOOL MyPathFindNextComponent
	(
		IN  LPCTSTR pszFileName,
		IN  BOOL    fAllowForwardSlash,
		OUT LPCTSTR* ppszResult
	)
{
	// This is a string-parsing helper function; params should never be NULL
	ASSERT(pszFileName != NULL);
	ASSERT(ppszResult != NULL);

	LPCTSTR pszStart = pszFileName;
	TCHAR chSlash2 = (fAllowForwardSlash ? _T('/') : _T('\\'));
	BOOL fResult = FALSE;

	for (;;)
	{
		TCHAR ch = *pszFileName;
		if (ch == _T('\0'))
			break; // didn't find a path separator; we'll return FALSE

		// Advance to next char, even if current char is path separator (\ or /)
		pszFileName = CHARNEXT(pszFileName);

		if (ch == _T('\\') || ch == chSlash2)
		{
			fResult = TRUE;
			break;
		}
	}

	*ppszResult = pszFileName;
	return fResult;
}


// SkipPathDrivePart [private]
//
//		Parses the filename to determine the length of the base drive portion of
//		the filename, and to determine what syntax the name is in.
//
//		This function does not actually examine the drive or file to ensure existence,
//		or to recognize that a drive letter like X:\ might be a network drive.
//
//		Returns:
//			TRUE  - if the input is a full path
//			FALSE - if input param is not a full path, or is bogus. The pcchDrivePart
//			        out param is set to 0 in this case.
//
static BOOL SkipPathDrivePart
	(
		IN LPCTSTR pszFileName,             // input path name (full or relative path)
		OUT OPTIONAL int* pcchDrivePart,    // # of TCHARs used by drive part
		OUT OPTIONAL BOOL* pfUNC,           // TRUE if path is UNC (not incl mapped drive)
		OUT OPTIONAL BOOL* pfExtendedSyntax // TRUE if path is \\?\ syntax
	)
{
	BOOL fFullPath = FALSE;
	LPCTSTR pszOriginalFileName = pszFileName;
	int fUNC = FALSE;
	int fExtendedSyntax = FALSE;

	if (!pszFileName)
		goto done;

	// BLOCK
	{
		//
		// Skip \\?\ if present. (This part must use backslashes, not forward slashes)
		//
#ifdef UNICODE
		if (SkipLangNeutralPrefix(pszFileName, _T("\\\\?\\"), &pszFileName))
		{
			fExtendedSyntax = TRUE;

			if (SkipLangNeutralPrefix(pszFileName, _T("UNC\\"), &pszFileName))
			{
				fUNC = TRUE; // Found "\\?\UNC\..."
			}
			else if (SkipLangNeutralPrefix(pszFileName, _T("Volume{"), &pszFileName))
			{
				// Found "\\?\Volume{1f3b3813-ddbf-11d5-ab2e-806d6172696f}\".
				// Skip the rest of the volume name.
				fFullPath = MyPathFindNextComponent(pszFileName, FALSE, &pszFileName);
				goto done;
			}
			// else continue normal parsing starting at updated pszFileName pointer
		}
#endif // UNICODE

		//
		// Check for path of the form C:\ 
		//
		TCHAR chFirstUpper = (TCHAR)CharUpper((LPTSTR)(pszFileName[0]));
		if (chFirstUpper >= _T('A') && chFirstUpper <= _T('Z') &&
			pszFileName[1] == _T(':') && pszFileName[2] == _T('\\'))
		{
			pszFileName += 3;
			fFullPath = TRUE;
			goto done;
		}

		//
		// Check for UNC of the form \\server\share\ 
		//
		if (!fExtendedSyntax &&
			pszFileName[0] == _T('\\') &&
			pszFileName[1] == _T('\\'))
		{
			fUNC = TRUE;
			pszFileName += 2; // skip the "\\"
		}
		if (fUNC) // may be \\server\share\ or \\?\UNC\server\share\ 
		{
			// Skip past server and share names. Trailing backslash is NOT optional.
			if (!MyPathFindNextComponent(pszFileName, TRUE, &pszFileName) ||
				!MyPathFindNextComponent(pszFileName, TRUE, &pszFileName))
			{
				goto done; // incomplete UNC path -> return failure
			}

			fFullPath = TRUE;
		}
	}

done:
	if (pcchDrivePart)
		*pcchDrivePart = fFullPath ? (int)(pszFileName - pszOriginalFileName) : 0;
	if (pfUNC)
		*pfUNC = fUNC;
	if (pfExtendedSyntax)
		*pfExtendedSyntax = fExtendedSyntax;

	return fFullPath;
}


// GetReparsePointType [public]
//
//		Given the full path of a file or directory, determines what type of 
//		reparse point the path represents.
//
//		Returns S_OK if the type of reparse point could be determined, or
//		an appropriate error code if not.
//
//		The out param is set to the reparse point type, or 0 if none.
//		The value for both volume mount points and junction points is
//		IO_REPARSE_TAG_MOUNT_POINT. (Use GetVolumeNameForVolumeMountPoint
//		to distinguish, if necessary.)
//
HRESULT WINAPI GetReparsePointType
	(
		IN LPCTSTR pszFileName,           // full path to folder to check
		OUT DWORD* pdwReparsePointType    // set to reparse point type, or 0 if none
	)
{
	HRESULT hr = S_OK;
	DWORD dwReparseType = 0;

	ASSERT(pdwReparsePointType);

	// BLOCK
	{
		if (!pszFileName)
		{
			hr = E_INVALIDARG;
			goto done;
		}

		DWORD dwAttrib = GetFileAttributes(pszFileName);
		if (dwAttrib == INVALID_FILE_ATTRIBUTES)
			goto win32_error;

		if (dwAttrib & FILE_ATTRIBUTE_REPARSE_POINT)
		{
			WIN32_FIND_DATA Find;
			HANDLE hFind = FindFirstFile(pszFileName, &Find);
			if (hFind == INVALID_HANDLE_VALUE)
				goto win32_error;

			dwReparseType = Find.dwReserved0;
			FindClose(hFind);
		}
		goto done;
	}

win32_error:
	hr = GetLastErrorAsHresult();

done:
	*pdwReparsePointType = dwReparseType;
	ASSERT(hr != E_INVALIDARG);
	return hr;
}


// CheckReparsePointPermissions [private]
//
//		Determines whether or not it's ok to trust the given reparse type.
//		Returns S_OK if it's safe, or an appropriate error message if not.
//
static HRESULT CheckReparsePointPermissions(IN DWORD dwReparseType)
{
	HRESULT hr = S_OK;

	// REVIEW: Any reason to worry about these other types of reparse points?
	//   IO_REPARSE_TAG_HSM, IO_REPARSE_TAG_SIS, IO_REPARSE_TAG_DFS, etc.
	if (dwReparseType == IO_REPARSE_TAG_MOUNT_POINT)
	{
		hr = HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED);
	}

	return hr;
}


// CheckValidDriveType [private]
//
//		Gets the volume name associated with the given file, and checks the
//		return value from GetDriveType() to see whether or not operations
//		are allowed on the file.
//
static HRESULT CheckValidDriveType
	(
		IN LPCTSTR pszFileName,       // full path of a file whose drive we want to check
		IN BOOL fAllowNetworkDrive,   // determines whether or not net drives are allowed
		IN BOOL fAllowRemovableDrive  // determines whether or not removable drives are allowed
	)
{
	HRESULT hr = E_INVALIDARG;
	LPTSTR pszVolumePath = NULL;

	// BLOCK
	{
		if (!pszFileName)
		{
			goto done;  // hr is already E_INVALIDARG
		}

		int cchFileName = lstrlen(pszFileName);
		pszVolumePath = (LPTSTR)SafeFileMalloc(sizeof(TCHAR) * (cchFileName+1));
		if (!pszVolumePath)
		{
			hr = E_OUTOFMEMORY;
			goto done;
		}

#ifdef UNICODE
		if (!GetVolumePathName(pszFileName, pszVolumePath, cchFileName+1))
		{
			hr = GetLastErrorAsHresult();
			goto done;
		}
#else
		int cchDrivePart;
		if (!SkipPathDrivePart(pszFileName, &cchDrivePart, NULL, NULL))
		{
			hr = HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND);
			goto done;
		}
		StringCchCopy(pszVolumePath, cchDrivePart+1, pszFileName);
#endif

		UINT uDriveType = GetDriveType(pszVolumePath);
		switch (uDriveType)
		{
		case DRIVE_FIXED:
			hr = S_OK;
			break;

		case DRIVE_REMOVABLE:
		case DRIVE_CDROM:
		case DRIVE_UNKNOWN:
		case DRIVE_RAMDISK:
			hr = fAllowRemovableDrive ? S_OK : E_ACCESSDENIED;
			break;

		case DRIVE_REMOTE:
			hr = fAllowNetworkDrive ? S_OK : E_ACCESSDENIED;
			break;

		default:
			hr = E_INVALIDARG;
			break;
		}
	}

done:
	SafeFileFree(pszVolumePath);

	ASSERT(hr != E_INVALIDARG);
	return hr;
}


// IsFullPathName [public]
//
//		Determines whether the given filename is a full path including a drive
//		or UNC. Filenames such as \\?\ are supported, and can be considered
//		valid or not depending on the dwSafeFlags parameter.
//
//		Returns:
//			TRUE  - if the filename is a full path
//			FALSE - if filename is NULL, isn't a full path, or fails to meet
//			        the criteria given in the dwSafeFlags parameter.
//
BOOL WINAPI IsFullPathName
	(
		IN LPCTSTR pszFileName,              // full or relative path to a file
		OUT OPTIONAL BOOL* pfUNC,            // TRUE path is UNC (int incl mapped drive)
		OUT OPTIONAL BOOL* pfExtendedSyntax  // TRUE if path is \\?\ syntax
	)
{
	return SkipPathDrivePart(pszFileName, NULL, pfUNC, pfExtendedSyntax);
}


// DoesPathContainDotDot [private]
//
//		Returns TRUE if the path contains any ".." references, else FALSE.
//
static BOOL WINAPI DoesPathContainDotDot(IN LPCTSTR pszFileName)
{
	if (!pszFileName)
		return FALSE;

	while (*pszFileName)
	{
		// Flag path components that consist exactly of ".." (nothing following)
		if (pszFileName[0] == _T('.') && pszFileName[1] == _T('.') &&
			(pszFileName[2] == _T('/') || pszFileName[2] == _T('\\') || pszFileName[2] == _T('\0')))
		{
			return TRUE;
		}

		MyPathFindNextComponent(pszFileName, TRUE, &pszFileName);
	}

	return FALSE;
}


// DoesPathContainStreamSyntax [private]
//
//		Returns TRUE if the path contains any characters that could cause it
//		to refer to an alternate NTFS stream (namely any ":" characters beyond
//		the drive specification).
//
static BOOL DoesPathContainStreamSyntax(IN LPCTSTR pszFileName)
{
	if (!pszFileName)
		return FALSE;

	int cchSkip;
	SkipPathDrivePart(pszFileName, &cchSkip, NULL, NULL);

	for (LPCTSTR pch = pszFileName + cchSkip; *pch; pch = CHARNEXT(pch))
	{
		if (*pch == _T(':'))
			return TRUE;
	}

	return FALSE;
}


// SafeCreateFile [public]
//
//		Opens the given file, ensuring that it meets certain path standards (e.g.
//		doesn't contain "..") and that it is a file, not a device or named pipe.
//
HRESULT WINAPI SafeCreateFile
	(
		OUT HANDLE* phFileResult,       // receives handle to opened file, or INVALID_HANDLE_VALUE
		IN DWORD dwSafeFlags,           // zero or more SCF_* flags
		IN LPCTSTR pszFileName,         // same as CreateFile
		IN DWORD dwDesiredAccess,       // same as CreateFile
		IN DWORD dwShareMode,           // same as CreateFile
		IN LPSECURITY_ATTRIBUTES lpSecurityAttributes, // same as CreateFile
		IN DWORD dwCreationDisposition, // same as CreateFile
		IN DWORD dwFlagsAndAttributes,  // same as CreateFile + (SECURITY_SQOS_PRESENT|SECURITY_ANONYMOUS)
		IN HANDLE hTemplateFile         // same as CreateFile
	)
{
	HANDLE hFile = INVALID_HANDLE_VALUE;
	HRESULT hr = S_OK;

	// BLOCK
	{
		if (!pszFileName || !phFileResult ||
			(dwSafeFlags & ~(SCF_ALLOW_NETWORK_DRIVE | SCF_ALLOW_REMOVABLE_DRIVE | SCF_ALLOW_ALTERNATE_STREAM)))
		{
			hr = E_INVALIDARG;
			goto done;
		}

		// We require a full pathname.
		if (!IsFullPathName(pszFileName))
		{
			hr = HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND);
			goto done;
		}

		// Ensure path doesn't contain ".." references
		if (DoesPathContainDotDot(pszFileName))
		{
			hr = HRESULT_FROM_WIN32(ERROR_BAD_PATHNAME);
			goto done;
		}

		// Ensure filename doesn't refer to alternate stream unless allowed
		if (!(dwSafeFlags & SCF_ALLOW_ALTERNATE_STREAM) &&
			DoesPathContainStreamSyntax(pszFileName))
		{
			hr = HRESULT_FROM_WIN32(ERROR_INVALID_NAME);
			goto done;
		}

		// Check drive type to ensure it's allowed by dwSafeFlags
		if (FAILED(hr = CheckValidDriveType(pszFileName, (dwSafeFlags & SCF_ALLOW_NETWORK_DRIVE),
							(dwSafeFlags & SCF_ALLOW_REMOVABLE_DRIVE))))
		{
			goto done;
		}

		// Open the file w/ extra security attributes
		dwFlagsAndAttributes |= (SECURITY_SQOS_PRESENT | SECURITY_ANONYMOUS);
		hFile = CreateFile(pszFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes,
							dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile);
		if (hFile == INVALID_HANDLE_VALUE)
		{
			goto win32_error;
		}

		// Ensure it's really a file
		if (FILE_TYPE_DISK != GetFileType(hFile))
		{
			CloseHandle(hFile);
			hFile = INVALID_HANDLE_VALUE;
			hr = HRESULT_FROM_WIN32(ERROR_OPEN_FAILED);
		}
		goto done;

	} // end BLOCK

win32_error:
	hr = GetLastErrorAsHresult();

done:
	if (phFileResult)
		*phFileResult = hFile;
	ASSERT(hr != E_INVALIDARG);
	return hr;
}


// SafeRemoveFileAttributes [public]
//
//		Given a filename and that file's current attributes, checks whether
//		any of the bits in dwRemoveAttrib need to be removed from the file,
//		and if necessary calls SetFileAttributes() to remove them.
//
//		Designed to check for invalid dwCurAttrib and call GetLastError()
//		for you, so you can pass GetFileAttributes() directly as a parameter.
//
HRESULT WINAPI SafeRemoveFileAttributes
	(
		IN LPCTSTR pszFileName,    // full path to file whose attributes we will change
		IN DWORD   dwCurAttrib,    // current attributes of the file
		IN DWORD   dwRemoveAttrib  // attribute bits to remove
	)
{
	HRESULT hr = S_OK; // this is default if attrib doesn't need to be removed

	if (!pszFileName || !dwRemoveAttrib)
	{
		hr = E_INVALIDARG;
		goto done;
	}

	if (dwCurAttrib & dwRemoveAttrib) // note: always true if dwCurAttrib==INVALID_FILE_ATTRIBUTES
	{
		if (dwCurAttrib == INVALID_FILE_ATTRIBUTES ||
			!SetFileAttributes(pszFileName, dwCurAttrib & ~dwRemoveAttrib))
		{
			hr = GetLastErrorAsHresult();
		}
	}

done:
	ASSERT(hr != E_INVALIDARG);
	return hr;
}


// SafeDeleteFolderAndContentsHelper [private]
//
//		Does all work except the parameter validation for for
//		SafeDeleteFolderAndContents.
//
static HRESULT SafeDeleteFolderAndContentsHelper
	(
		IN  LPCTSTR pszFolderToDelete,  // folder in current level of recursion
		IN  DWORD dwSafeFlags,          // zero or more SDF_* flags
		OUT WIN32_FIND_DATA* pFind      // struct to use for FindFirst/FindNext (to avoid malloc)
	)
{
	HRESULT hr = S_OK;
	LPTSTR pszCurFile = NULL;
	HANDLE hFind = INVALID_HANDLE_VALUE;

	// Allocate room for folder + backslash + MAX_PATH (includes trailing null)
	int cchFolderName = lstrlen(pszFolderToDelete);
	int cchAllocCurFile = cchFolderName + 1 + MAX_PATH;
	pszCurFile = (LPTSTR)SafeFileMalloc(sizeof(TCHAR) * cchAllocCurFile);
	if (!pszCurFile)
	{
		hr = E_OUTOFMEMORY;
		goto done;
	}

	// Check for read-only base folder
	if (dwSafeFlags & SDF_DELETE_READONLY_FILES)
	{
		hr = SafeRemoveFileAttributes(pszFolderToDelete, GetFileAttributes(pszFolderToDelete), FILE_ATTRIBUTE_READONLY);
		if (FAILED(hr) && !(dwSafeFlags & SDF_CONTINUE_IF_ERROR))
			goto done;
	}

	// Build search path by appending "\*.*"
	StringCchCopy(pszCurFile, cchAllocCurFile, pszFolderToDelete);
	if (!IsSlashOrBackslash(pszCurFile[cchFolderName-1]))
		pszCurFile[cchFolderName++] = _T('\\');
	StringCchCopy(pszCurFile + cchFolderName, cchAllocCurFile - cchFolderName, _T("*.*"));

	// Iterate through all files in this folder
	hFind = FindFirstFile(pszCurFile, pFind);
	if (hFind == INVALID_HANDLE_VALUE)
	{
		hr = GetLastErrorAsHresult();  // probably doesn't exist, or not a folder
		goto done;
	}
	else
	{
		do
		{
			if (0 == lstrcmp(pFind->cFileName, _T(".")) ||
				0 == lstrcmp(pFind->cFileName, _T("..")))
			{
				continue;
			}

			StringCchCopy(pszCurFile + cchFolderName, cchAllocCurFile - cchFolderName, pFind->cFileName);
			HRESULT hrCur = S_OK;

			if (!(pFind->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) ||
				SUCCEEDED(hrCur = CheckReparsePointPermissions(pFind->dwReserved0)))
			{
				// Remove read-only attribute if allowed
				if (dwSafeFlags & SDF_DELETE_READONLY_FILES)
				{
					hrCur = SafeRemoveFileAttributes(pszCurFile, pFind->dwFileAttributes, FILE_ATTRIBUTE_READONLY);
				}

				if (SUCCEEDED(hrCur) || (dwSafeFlags & SDF_CONTINUE_IF_ERROR))
				{
					HRESULT hrCur2 = S_OK;

					if (pFind->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
					{
						// Recursively delete folder and contents
						// Note that pFind's contents are clobbered by this call
						hrCur2 = SafeDeleteFolderAndContentsHelper(pszCurFile, dwSafeFlags, pFind);
					}
					else
					{
						// Delete the file
						if (!DeleteFile(pszCurFile))
						{
							hrCur2 = GetLastErrorAsHresult();
						}
					}

					if (FAILED(hrCur2))
						hrCur = hrCur2;
				}
			}

			if (FAILED(hrCur))
				hr = hrCur;

			if (FAILED(hr) && !(dwSafeFlags & SDF_CONTINUE_IF_ERROR))
				goto done;

		} while (FindNextFile(hFind, pFind));
		FindClose(hFind);
		hFind = INVALID_HANDLE_VALUE;
	}

	// Delete the folder
	if (!RemoveDirectory(pszFolderToDelete))
	{
		if (SUCCEEDED(hr))
			hr = GetLastErrorAsHresult();
	}

done:
	if (hFind != INVALID_HANDLE_VALUE)
		FindClose(hFind);
	SafeFileFree(pszCurFile);
	return hr;
}


// SafeDeleteFolderAndContents [public]
//
//		Deletes the given folder and all of its contents, but refuses to walk
//		across reparse points.
//
HRESULT WINAPI SafeDeleteFolderAndContents
	(
		IN LPCTSTR pszFolderToDelete,  // full path of folder to delete
		IN DWORD   dwSafeFlags         // zero or more SDF_* flags
	)
{
	HRESULT hr = E_INVALIDARG;

	if (!pszFolderToDelete || !(*pszFolderToDelete) ||
		(dwSafeFlags & ~(SDF_ALLOW_NETWORK_DRIVE | SDF_DELETE_READONLY_FILES | SDF_CONTINUE_IF_ERROR)))
	{
		goto done;  // hr already set to E_INVALIDARG
	}

	//
	// Ensure it's a full path, but not the root of a drive
	//
	int cchDrivePart;
	if (!SkipPathDrivePart(pszFolderToDelete, &cchDrivePart, NULL, NULL) ||
		pszFolderToDelete[cchDrivePart] == _T('\0'))
	{
		hr = HRESULT_FROM_WIN32(ERROR_BAD_PATHNAME);
		goto done;
	}

	//
	// Ensure we're not deleting from a network drive unless allowed
	//
	if (FAILED(hr = CheckValidDriveType(pszFolderToDelete, (dwSafeFlags & SDF_ALLOW_NETWORK_DRIVE), TRUE)))
	{
		goto done;
	}

	//
	// Ensure starting point is not a reparse point
	//
	DWORD dwReparseType;
	if (FAILED(hr = GetReparsePointType(pszFolderToDelete, &dwReparseType)) ||
		FAILED(hr = CheckReparsePointPermissions(dwReparseType)))
	{
		goto done;
	}

	WIN32_FIND_DATA Find;
	hr = SafeDeleteFolderAndContentsHelper(pszFolderToDelete, dwSafeFlags, &Find);

done:
	ASSERT(hr != E_INVALIDARG);
	return hr;
}


// SafeFileCheckForReparsePoint [public]
//
//		Checks a subset of the given filename's component parts to ensure that
//		they are not reparse points (specifically, volume mount points or
//		junction points: see linkd.exe and mountvol.exe).
//
//		Normal return values are S_OK or HRESULT_FROM_WIN32(ERROR_REPARSE_TAG_MISMATCH).
//		Other values may be returned in exceptional cases such as out-of-memory.
//
HRESULT WINAPI SafeFileCheckForReparsePoint
	(
		IN LPCTSTR pszFileName,           // full path of a file
		IN int     nFirstUntrustedOffset, // char offset of first path component to check
		IN DWORD   dwSafeFlags            // zero or more SRP_* flags
	)
{
	HRESULT hr = E_INVALIDARG;
	LPTSTR pszMutableFileName = NULL;

	// BLOCK
	{
		if (!pszFileName || (dwSafeFlags & ~SRP_FILE_MUST_EXIST))
		{
			goto done;  // hr is already E_INVALIDARG
		}

		int cchFileName = lstrlen(pszFileName);
		if ((UINT)nFirstUntrustedOffset >= (UINT)cchFileName) // bad offset, or zero-length filename
		{
			goto done;  // hr is already E_INVALIDARG
		}

		pszMutableFileName = (LPTSTR)SafeFileMalloc(sizeof(TCHAR) * (cchFileName+1));
		if (!pszMutableFileName)
		{
			hr = E_OUTOFMEMORY;
			goto done;
		}
		StringCchCopy(pszMutableFileName, cchFileName+1, pszFileName);

		//
		// Always consider the drive part of the path to be trusted
		//
		int cchDrivePart;
		if (!SkipPathDrivePart(pszMutableFileName, &cchDrivePart, NULL, NULL))
		{
			hr = HRESULT_FROM_WIN32(ERROR_BAD_PATHNAME);
			goto done;
		}
		if (nFirstUntrustedOffset < cchDrivePart)
			nFirstUntrustedOffset = cchDrivePart;

		//
		// Validate left-to-right, starting after trusted base path
		//
		LPTSTR pszNextComponent = pszMutableFileName + nFirstUntrustedOffset;
		BOOL fMoreComponents = TRUE;
		do
		{
			//
			// Advance pszNextComponent; truncate after current path component
			//
			fMoreComponents = MyPathFindNextComponent(pszNextComponent, TRUE, (LPCTSTR*)&pszNextComponent);
			TCHAR chSave = *(pszNextComponent-1);
			if (fMoreComponents)
			{
				*(pszNextComponent-1) = _T('\0');
			}

			// Get reparse point type of truncated string, and undo the truncation
			DWORD dwReparseType;
			if (FAILED(hr = GetReparsePointType(pszMutableFileName, &dwReparseType)))
				goto done;
			*(pszNextComponent-1) = chSave;

			// Check for forbidden reparse point type, e.g. mounted drive
			if (FAILED(hr = CheckReparsePointPermissions(dwReparseType)))
				goto done;
		}
		while (fMoreComponents);

	} // end BLOCK

done:
	SafeFileFree(pszMutableFileName);

	// Ignore file-not-found errors, if requested in dwSafeFlags
	if (!(dwSafeFlags & SRP_FILE_MUST_EXIST) &&
	    (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) ||
	     hr == HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND)))
	{
		hr = S_OK;
	}

	ASSERT(hr != E_INVALIDARG);
	return hr;
}


// SafePathCombine [public]
//
//		Combines a path and filename, ensuring exactly one backslash between them.
//		The second "untrusted" half of the path is checked to ensure that it is
//		safe (doesn't contain ".." or ":", or point to existing reparse points).
//
//		File-not-found errors are ignored unless SPC_FILE_MUST_EXIST flag is specified.
//
//		It's ok for the base path and the output buffer to point to the same buffer.
//
//		Returns S_OK if successful, or an appropriate error code if not.
//
HRESULT WINAPI SafePathCombine
	(
		OUT LPTSTR  pszBuf,               // buffer where combined path will be stored
		IN  int     cchBuf,               // size of output buffer, in TCHARs
		IN  LPCTSTR pszTrustedBasePath,   // first half of path, all trusted
		IN  LPCTSTR pszUntrustedFileName, // second half of path, not trusted
		IN  DWORD   dwSafeFlags           // zero or more SPC_* flags
	)
{
	HRESULT hr = E_INVALIDARG;

	if (!pszBuf || cchBuf <= 0 || !pszTrustedBasePath || !pszUntrustedFileName ||
		(dwSafeFlags & ~(SPC_FILE_MUST_EXIST | SPC_ALLOW_ALTERNATE_STREAM)))
	{
		goto done;  // hr is already E_INVALIDARG
	}

	// BLOCK
	{
		int cchBasePath = lstrlen(pszTrustedBasePath);
		int cchFileName = lstrlen(pszUntrustedFileName);
		if (cchBasePath == 0 || cchFileName == 0)
		{
			goto done;  // hr is already E_INVALIDARG
		}

		// Ensure nothing bogus in the untrusted part of the filename
		if (DoesPathContainDotDot(pszUntrustedFileName))
		{
			hr = ERROR_BAD_PATHNAME;
			goto done;
		}

		if (!(dwSafeFlags & SPC_ALLOW_ALTERNATE_STREAM) &&
			DoesPathContainStreamSyntax(pszUntrustedFileName))
		{
			hr = HRESULT_FROM_WIN32(ERROR_INVALID_NAME);
			goto done;
		}

		//
		// Ensure room for the "\" that will be inserted.
		//
		int cchInsertSlash = 0;
		if (!IsSlashOrBackslash(pszTrustedBasePath[cchBasePath-1]))
		{
			cchInsertSlash = 1;
		}
		if (cchBasePath + cchInsertSlash + cchFileName >= cchBuf)
		{
			hr = HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER);
			goto done;
		}

		//
		// Build full path with a backslash between
		//
		if (pszBuf != pszTrustedBasePath)
			StringCchCopy(pszBuf, cchBuf, pszTrustedBasePath);
		int cchUsed = cchBasePath;
		if (cchInsertSlash > 0)
		{
			pszBuf[cchUsed++] = _T('\\');
		}
		StringCchCopy(pszBuf + cchUsed, cchBuf - cchUsed, pszUntrustedFileName);

		//
		// Ensure no junctions or volume mount points in untrusted portion
		//
		DWORD dwReparseFlags = (dwSafeFlags & SPC_FILE_MUST_EXIST) ? SRP_FILE_MUST_EXIST : 0;
		hr = SafeFileCheckForReparsePoint(pszBuf, cchUsed, dwReparseFlags);
	}

done:
	if (FAILED(hr) && pszBuf && cchBuf > 0)
		pszBuf[0] = _T('\0');

	ASSERT(hr != E_INVALIDARG);
	return hr;
}


// SafePathCombineAlloc [public]
//
//		See comments for SafePathCombine. The only difference is that this
//		function allocates a buffer of sufficient size and stores it in the
//		output parameter ppszResult. Caller is responsible for freeing the
//		buffer via SafeFileFree.
//
HRESULT WINAPI SafePathCombineAlloc
	(
		OUT LPTSTR* ppszResult,           // ptr to newly alloc'd buffer stored here
		IN  LPCTSTR pszTrustedBasePath,   // first half of path, all trusted
		IN  LPCTSTR pszUntrustedFileName, // second half of path, not trusted
		IN  DWORD   dwSafeFlags           // zero or more SPC_* flags
	)
{
	HRESULT hr = E_INVALIDARG;

	ASSERT(ppszResult);
	*ppszResult = NULL;

	if (!pszTrustedBasePath || !pszUntrustedFileName)
	{
		goto done; // hr already set to E_INVALIDARG
	}

	// Allocate room for the max possible length (includes room for "\" between parts)
	int cchMaxNeeded = lstrlen(pszTrustedBasePath) + lstrlen(pszUntrustedFileName) + 2;
	LPTSTR pszResult = (LPTSTR)SafeFileMalloc(sizeof(TCHAR) * cchMaxNeeded);
	if (!pszResult)
	{
		hr = E_OUTOFMEMORY;
		goto done;
	}

	hr = SafePathCombine(pszResult, cchMaxNeeded, pszTrustedBasePath, pszUntrustedFileName, dwSafeFlags);
	if (FAILED(hr))
	{
		SafeFileFree(pszResult);
	}
	else
	{
		*ppszResult = pszResult;
	}

done:
	ASSERT(hr != E_INVALIDARG);
	return hr;
}