401 lines
17 KiB
C#
401 lines
17 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Text;
|
|
using Microsoft.Win32;
|
|
using System.Runtime.InteropServices;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Globalization;
|
|
using System.Runtime.Versioning;
|
|
using System.Security;
|
|
using System.Security.Permissions;
|
|
using System.Diagnostics.Contracts;
|
|
|
|
namespace System.IO {
|
|
|
|
// ABOUT:
|
|
// Helps with path normalization; support allocating on the stack or heap
|
|
//
|
|
// PathHelper can't stackalloc the array for obvious reasons; you must pass
|
|
// in an array of chars allocated on the stack.
|
|
//
|
|
// USAGE:
|
|
// Suppose you need to represent a char array of length len. Then this is the
|
|
// suggested way to instantiate PathHelper:
|
|
// ***************************************************************************
|
|
// PathHelper pathHelper;
|
|
// if (charArrayLength less than stack alloc threshold == Path.MaxPath)
|
|
// char* arrayPtr = stackalloc char[Path.MaxPath];
|
|
// pathHelper = new PathHelper(arrayPtr);
|
|
// else
|
|
// pathHelper = new PathHelper(capacity, maxPath);
|
|
// ***************************************************************************
|
|
//
|
|
// note in the StringBuilder ctor:
|
|
// - maxPath may be greater than Path.MaxPath (for isolated storage)
|
|
// - capacity may be greater than maxPath. This is even used for non-isolated
|
|
// storage scenarios where we want to temporarily allow strings greater
|
|
// than Path.MaxPath if they can be normalized down to Path.MaxPath. This
|
|
// can happen if the path contains escape characters "..".
|
|
//
|
|
unsafe internal class PathHelper { // should not be serialized
|
|
|
|
// maximum size, max be greater than max path if contains escape sequence
|
|
private int m_capacity;
|
|
// current length (next character position)
|
|
private int m_length;
|
|
// max path, may be less than capacity
|
|
private int m_maxPath;
|
|
|
|
// ptr to stack alloc'd array of chars
|
|
[SecurityCritical]
|
|
private char* m_arrayPtr;
|
|
|
|
// StringBuilder
|
|
private StringBuilder m_sb;
|
|
|
|
// whether to operate on stack alloc'd or heap alloc'd array
|
|
private bool useStackAlloc;
|
|
|
|
// Whether to skip calls to Win32Native.GetLongPathName becasue we tried before and failed:
|
|
private bool doNotTryExpandShortFileName;
|
|
|
|
// Instantiates a PathHelper with a stack alloc'd array of chars
|
|
[System.Security.SecurityCritical]
|
|
internal PathHelper(char* charArrayPtr, int length) {
|
|
Contract.Requires(charArrayPtr != null);
|
|
// force callers to be aware of this
|
|
Contract.Requires(length == Path.MaxPath);
|
|
|
|
this.m_arrayPtr = charArrayPtr;
|
|
this.m_capacity = length;
|
|
this.m_maxPath = Path.MaxPath;
|
|
useStackAlloc = true;
|
|
doNotTryExpandShortFileName = false;
|
|
}
|
|
|
|
// Instantiates a PathHelper with a heap alloc'd array of ints. Will create a StringBuilder
|
|
internal PathHelper(int capacity, int maxPath) {
|
|
this.m_sb = new StringBuilder(capacity);
|
|
this.m_capacity = capacity;
|
|
this.m_maxPath = maxPath;
|
|
doNotTryExpandShortFileName = false;
|
|
}
|
|
|
|
internal int Length {
|
|
get {
|
|
if (useStackAlloc) {
|
|
return m_length;
|
|
}
|
|
else {
|
|
return m_sb.Length;
|
|
}
|
|
}
|
|
set {
|
|
if (useStackAlloc) {
|
|
m_length = value;
|
|
}
|
|
else {
|
|
m_sb.Length = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
internal int Capacity {
|
|
get {
|
|
return m_capacity;
|
|
}
|
|
}
|
|
|
|
internal char this[int index] {
|
|
[System.Security.SecurityCritical]
|
|
get {
|
|
Contract.Requires(index >= 0 && index < Length);
|
|
if (useStackAlloc) {
|
|
return m_arrayPtr[index];
|
|
}
|
|
else {
|
|
return m_sb[index];
|
|
}
|
|
}
|
|
[System.Security.SecurityCritical]
|
|
set {
|
|
Contract.Requires(index >= 0 && index < Length);
|
|
if (useStackAlloc) {
|
|
m_arrayPtr[index] = value;
|
|
}
|
|
else {
|
|
m_sb[index] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
[System.Security.SecurityCritical]
|
|
internal unsafe void Append(char value) {
|
|
if (Length + 1 >= m_capacity)
|
|
throw new PathTooLongException(Environment.GetResourceString("IO.PathTooLong"));
|
|
|
|
if (useStackAlloc) {
|
|
m_arrayPtr[Length] = value;
|
|
m_length++;
|
|
}
|
|
else {
|
|
m_sb.Append(value);
|
|
}
|
|
}
|
|
|
|
[System.Security.SecurityCritical]
|
|
internal unsafe int GetFullPathName() {
|
|
if (useStackAlloc) {
|
|
char* finalBuffer = stackalloc char[Path.MaxPath + 1];
|
|
int result = Win32Native.GetFullPathName(m_arrayPtr, Path.MaxPath + 1, finalBuffer, IntPtr.Zero);
|
|
|
|
// If success, the return buffer length does not account for the terminating null character.
|
|
// If in-sufficient buffer, the return buffer length does account for the path + the terminating null character.
|
|
// If failure, the return buffer length is zero
|
|
if (result > Path.MaxPath) {
|
|
char* tempBuffer = stackalloc char[result];
|
|
finalBuffer = tempBuffer;
|
|
result = Win32Native.GetFullPathName(m_arrayPtr, result, finalBuffer, IntPtr.Zero);
|
|
}
|
|
|
|
// Full path is genuinely long
|
|
if (result >= Path.MaxPath)
|
|
throw new PathTooLongException(Environment.GetResourceString("IO.PathTooLong"));
|
|
|
|
Contract.Assert(result < Path.MaxPath, "did we accidently remove a PathTooLongException check?");
|
|
if (result == 0 && m_arrayPtr[0] != '\0') {
|
|
__Error.WinIOError();
|
|
}
|
|
|
|
else if (result < Path.MaxPath) {
|
|
// Null terminate explicitly (may be only needed for some cases such as empty strings)
|
|
// GetFullPathName return length doesn't account for null terminating char...
|
|
finalBuffer[result] = '\0'; // Safe to write directly as result is < Path.MaxPath
|
|
}
|
|
|
|
// We have expanded the paths and GetLongPathName may or may not behave differently from before.
|
|
// We need to call it again to see:
|
|
doNotTryExpandShortFileName = false;
|
|
|
|
String.wstrcpy(m_arrayPtr, finalBuffer, result);
|
|
// Doesn't account for null terminating char. Think of this as the last
|
|
// valid index into the buffer but not the length of the buffer
|
|
Length = result;
|
|
return result;
|
|
}
|
|
else {
|
|
StringBuilder finalBuffer = new StringBuilder(m_capacity + 1);
|
|
int result = Win32Native.GetFullPathName(m_sb.ToString(), m_capacity + 1, finalBuffer, IntPtr.Zero);
|
|
|
|
// If success, the return buffer length does not account for the terminating null character.
|
|
// If in-sufficient buffer, the return buffer length does account for the path + the terminating null character.
|
|
// If failure, the return buffer length is zero
|
|
if (result > m_maxPath) {
|
|
finalBuffer.Length = result;
|
|
result = Win32Native.GetFullPathName(m_sb.ToString(), result, finalBuffer, IntPtr.Zero);
|
|
}
|
|
|
|
// Fullpath is genuinely long
|
|
if (result >= m_maxPath)
|
|
throw new PathTooLongException(Environment.GetResourceString("IO.PathTooLong"));
|
|
|
|
Contract.Assert(result < m_maxPath, "did we accidentally remove a PathTooLongException check?");
|
|
if (result == 0 && m_sb[0] != '\0') {
|
|
if (Length >= m_maxPath) {
|
|
throw new PathTooLongException(Environment.GetResourceString("IO.PathTooLong"));
|
|
}
|
|
__Error.WinIOError();
|
|
}
|
|
|
|
// We have expanded the paths and GetLongPathName may or may not behave differently from before.
|
|
// We need to call it again to see:
|
|
doNotTryExpandShortFileName = false;
|
|
|
|
m_sb = finalBuffer;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
[System.Security.SecurityCritical]
|
|
internal unsafe bool TryExpandShortFileName() {
|
|
|
|
if (doNotTryExpandShortFileName)
|
|
return false;
|
|
|
|
if (useStackAlloc) {
|
|
NullTerminate();
|
|
char* buffer = UnsafeGetArrayPtr();
|
|
char* shortFileNameBuffer = stackalloc char[Path.MaxPath + 1];
|
|
|
|
int r = Win32Native.GetLongPathName(buffer, shortFileNameBuffer, Path.MaxPath);
|
|
|
|
// If success, the return buffer length does not account for the terminating null character.
|
|
// If in-sufficient buffer, the return buffer length does account for the path + the terminating null character.
|
|
// If failure, the return buffer length is zero
|
|
if (r >= Path.MaxPath)
|
|
throw new PathTooLongException(Environment.GetResourceString("IO.PathTooLong"));
|
|
|
|
if (r == 0) {
|
|
// Note: GetLongPathName will return ERROR_INVALID_FUNCTION on a
|
|
// path like \\.\PHYSICALDEVICE0 - some device driver doesn't
|
|
// support GetLongPathName on that string. This behavior is
|
|
// by design, according to the Core File Services team.
|
|
// We also get ERROR_NOT_ENOUGH_QUOTA in SQL_CLR_STRESS runs
|
|
// intermittently on paths like D:\DOCUME~1\user\LOCALS~1\Temp\
|
|
|
|
// We do not need to call GetLongPathName if we know it will fail becasue the path does not exist:
|
|
int lastErr = Marshal.GetLastWin32Error();
|
|
if (lastErr == Win32Native.ERROR_FILE_NOT_FOUND || lastErr == Win32Native.ERROR_PATH_NOT_FOUND)
|
|
doNotTryExpandShortFileName = true;
|
|
|
|
return false;
|
|
}
|
|
|
|
// Safe to copy as we have already done Path.MaxPath bound checking
|
|
String.wstrcpy(buffer, shortFileNameBuffer, r);
|
|
Length = r;
|
|
// We should explicitly null terminate as in some cases the long version of the path
|
|
// might actually be shorter than what we started with because of Win32's normalization
|
|
// Safe to write directly as bufferLength is guaranteed to be < Path.MaxPath
|
|
NullTerminate();
|
|
return true;
|
|
}
|
|
else {
|
|
StringBuilder sb = GetStringBuilder();
|
|
|
|
String origName = sb.ToString();
|
|
String tempName = origName;
|
|
bool addedPrefix = false;
|
|
if (tempName.Length > Path.MaxPath) {
|
|
tempName = Path.AddLongPathPrefix(tempName);
|
|
addedPrefix = true;
|
|
}
|
|
sb.Capacity = m_capacity;
|
|
sb.Length = 0;
|
|
int r = Win32Native.GetLongPathName(tempName, sb, m_capacity);
|
|
|
|
if (r == 0) {
|
|
// Note: GetLongPathName will return ERROR_INVALID_FUNCTION on a
|
|
// path like \\.\PHYSICALDEVICE0 - some device driver doesn't
|
|
// support GetLongPathName on that string. This behavior is
|
|
// by design, according to the Core File Services team.
|
|
// We also get ERROR_NOT_ENOUGH_QUOTA in SQL_CLR_STRESS runs
|
|
// intermittently on paths like D:\DOCUME~1\user\LOCALS~1\Temp\
|
|
|
|
// We do not need to call GetLongPathName if we know it will fail becasue the path does not exist:
|
|
int lastErr = Marshal.GetLastWin32Error();
|
|
if (Win32Native.ERROR_FILE_NOT_FOUND == lastErr || Win32Native.ERROR_PATH_NOT_FOUND == lastErr)
|
|
doNotTryExpandShortFileName = true;
|
|
|
|
sb.Length = 0;
|
|
sb.Append(origName);
|
|
return false;
|
|
}
|
|
|
|
if (addedPrefix)
|
|
r -= 4;
|
|
|
|
// If success, the return buffer length does not account for the terminating null character.
|
|
// If in-sufficient buffer, the return buffer length does account for the path + the terminating null character.
|
|
// If failure, the return buffer length is zero
|
|
if (r >= m_maxPath)
|
|
throw new PathTooLongException(Environment.GetResourceString("IO.PathTooLong"));
|
|
|
|
|
|
sb = Path.RemoveLongPathPrefix(sb);
|
|
Length = sb.Length;
|
|
return true;
|
|
|
|
}
|
|
}
|
|
|
|
[System.Security.SecurityCritical]
|
|
internal unsafe void Fixup(int lenSavedName, int lastSlash) {
|
|
if (useStackAlloc) {
|
|
char* savedName = stackalloc char[lenSavedName];
|
|
String.wstrcpy(savedName, m_arrayPtr + lastSlash + 1, lenSavedName);
|
|
Length = lastSlash;
|
|
NullTerminate();
|
|
doNotTryExpandShortFileName = false;
|
|
bool r = TryExpandShortFileName();
|
|
// Clean up changes made to the newBuffer.
|
|
Append(Path.DirectorySeparatorChar);
|
|
if (Length + lenSavedName >= Path.MaxPath)
|
|
throw new PathTooLongException(Environment.GetResourceString("IO.PathTooLong"));
|
|
String.wstrcpy(m_arrayPtr + Length, savedName, lenSavedName);
|
|
Length = Length + lenSavedName;
|
|
|
|
}
|
|
else {
|
|
String savedName = m_sb.ToString(lastSlash + 1, lenSavedName);
|
|
Length = lastSlash;
|
|
doNotTryExpandShortFileName = false;
|
|
bool r = TryExpandShortFileName();
|
|
// Clean up changes made to the newBuffer.
|
|
Append(Path.DirectorySeparatorChar);
|
|
if (Length + lenSavedName >= m_maxPath)
|
|
throw new PathTooLongException(Environment.GetResourceString("IO.PathTooLong"));
|
|
m_sb.Append(savedName);
|
|
}
|
|
}
|
|
|
|
[System.Security.SecurityCritical]
|
|
internal unsafe bool OrdinalStartsWith(String compareTo, bool ignoreCase) {
|
|
if (Length < compareTo.Length)
|
|
return false;
|
|
|
|
if (useStackAlloc) {
|
|
NullTerminate();
|
|
if (ignoreCase) {
|
|
String s = new String(m_arrayPtr, 0, compareTo.Length);
|
|
return compareTo.Equals(s, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
else {
|
|
for (int i = 0; i < compareTo.Length; i++) {
|
|
if (m_arrayPtr[i] != compareTo[i]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
else {
|
|
if (ignoreCase) {
|
|
return m_sb.ToString().StartsWith(compareTo, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
else {
|
|
return m_sb.ToString().StartsWith(compareTo, StringComparison.Ordinal);
|
|
}
|
|
}
|
|
}
|
|
|
|
[System.Security.SecuritySafeCritical]
|
|
public override String ToString() {
|
|
if (useStackAlloc) {
|
|
return new String(m_arrayPtr, 0, Length);
|
|
}
|
|
else {
|
|
return m_sb.ToString();
|
|
}
|
|
}
|
|
|
|
[System.Security.SecurityCritical]
|
|
private unsafe char* UnsafeGetArrayPtr() {
|
|
Contract.Requires(useStackAlloc, "This should never be called for PathHelpers wrapping a StringBuilder");
|
|
return m_arrayPtr;
|
|
}
|
|
|
|
private StringBuilder GetStringBuilder() {
|
|
Contract.Requires(!useStackAlloc, "This should never be called for PathHelpers that wrap a stackalloc'd buffer");
|
|
return m_sb;
|
|
}
|
|
|
|
[System.Security.SecurityCritical]
|
|
private unsafe void NullTerminate() {
|
|
Contract.Requires(useStackAlloc, "This should never be called for PathHelpers wrapping a StringBuilder");
|
|
m_arrayPtr[m_length] = '\0';
|
|
}
|
|
|
|
}
|
|
}
|