File: system\io\pathhelper.cs
Project: ndp\clr\src\bcl\mscorlib.csproj (mscorlib)
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 struct 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_length = 0;
            this.m_sb = null;
 
            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
        [System.Security.SecurityCritical]
        internal PathHelper(int capacity, int maxPath)
        {
            this.m_length = 0;
            this.m_arrayPtr = null;
            this.useStackAlloc = false;
 
            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';
        }
 
    }
}