File: System\Configuration\ClientConfigPaths.cs
Project: ndp\fx\src\Configuration\System.Configuration.csproj (System.Configuration)
//------------------------------------------------------------------------------
// <copyright file="ClientConfigPaths.cs" company="Microsoft">
//     Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
//------------------------------------------------------------------------------
 
namespace System.Configuration {
    using System;
    using System.Collections;
    using System.IO;
    using System.Reflection;
    using System.Runtime.CompilerServices;
    using System.Runtime.InteropServices;
    using System.Runtime.Serialization.Formatters.Binary;
    using System.Security;
    using System.Security.Cryptography;
    using System.Security.Policy;
    using System.Security.Permissions;
    using System.Text;
    using System.Globalization;
    using Microsoft.Win32;
    using Diagnostics.CodeAnalysis;
 
    class ClientConfigPaths {
        internal const string       UserConfigFilename = "user.config";
        
        const string                ClickOnceDataDirectory = "DataDirectory";
        const string                ConfigExtension = ".config";
        const int                   MAX_PATH = 260;
        const int                   MAX_UNICODESTRING_LEN = short.MaxValue;
        const int                   ERROR_INSUFFICIENT_BUFFER = 122; //https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx
        const int                   MAX_LENGTH_TO_USE = 25;
        const string                FILE_URI_LOCAL = "file:///";
        const string                FILE_URI_UNC = "file://";
        const string                FILE_URI = "file:";
        const string                HTTP_URI = "http://";
        const string                StrongNameDesc = "StrongName";
        const string                UrlDesc = "Url";
        const string                PathDesc = "Path";
 
        static Char[] s_Base32Char   = {
                'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 
                'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
                'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 
                'y', 'z', '0', '1', '2', '3', '4', '5'};
 
        static  volatile ClientConfigPaths  s_current;
        static  volatile bool               s_currentIncludesUserConfig;
        static  volatile SecurityPermission          s_serializationPerm;
        static  volatile SecurityPermission          s_controlEvidencePerm;
 
        bool    _hasEntryAssembly;
        bool    _includesUserConfig;
        string  _applicationUri;
        string  _applicationConfigUri;
        string  _roamingConfigDirectory;
        string  _roamingConfigFilename;
        string  _localConfigDirectory;
        string  _localConfigFilename;
        string  _companyName;
        string  _productName;
        string  _productVersion;
 
        
        [FileIOPermission(SecurityAction.Assert, AllFiles=FileIOPermissionAccess.PathDiscovery | FileIOPermissionAccess.Read)]
        [SecurityPermission(SecurityAction.Assert, UnmanagedCode=true)]
        private ClientConfigPaths(string exePath, bool includeUserConfig) {
 
            _includesUserConfig = includeUserConfig;
 
            Assembly    exeAssembly = null;
            string      applicationUri = null;
            string      applicationFilename = null;
            
            // get the assembly and applicationUri for the file
            if (exePath == null) {
                // First check if a configuration file has been set for this app domain. If so, we will use that.
                // The CLR would already have normalized this, so no further processing necessary.
                AppDomain domain = AppDomain.CurrentDomain;
                AppDomainSetup setup = domain.SetupInformation;
                _applicationConfigUri = setup.ConfigurationFile;
 
                // Now figure out the application path.
                exeAssembly = Assembly.GetEntryAssembly();
                if (exeAssembly != null) {
                    _hasEntryAssembly = true;
                    applicationUri = exeAssembly.CodeBase;
 
                    bool isFile = false;
 
                    // If it is a local file URI, convert it to its filename, without invoking Uri class.
                    // example: "file:///C:/WINNT/Microsoft.NET/Framework/v2.0.x86fre/csc.exe"
                    if (StringUtil.StartsWithIgnoreCase(applicationUri, FILE_URI_LOCAL)) {
                        isFile = true;
                        applicationUri = applicationUri.Substring(FILE_URI_LOCAL.Length);
                    }
                    // If it is a UNC file URI, convert it to its filename, without invoking Uri class.
                    // example: "file://server/share/csc.exe"
                    else if (StringUtil.StartsWithIgnoreCase(applicationUri, FILE_URI_UNC)) {
                        isFile = true;
                        applicationUri = applicationUri.Substring(FILE_URI.Length);
                    }
 
                    if (isFile) {
                        applicationUri = applicationUri.Replace('/', '\\');
                        applicationFilename = applicationUri;
                    }
                    else {
                        applicationUri = exeAssembly.EscapedCodeBase;
                    }
                }
                else {
                    StringBuilder sb = new StringBuilder(MAX_PATH);
                    int noOfTimes = 1;
                    int length = 0;
                    // Iterating by allocating chunk of memory each time we find the length is not sufficient.
                    // Performance should not be an issue for current MAX_PATH length due to this change.
                    while (((length = UnsafeNativeMethods.GetModuleFileName(new HandleRef(null, IntPtr.Zero), sb, sb.Capacity)) == sb.Capacity) 
                            && Marshal.GetLastWin32Error() == ERROR_INSUFFICIENT_BUFFER 
                            && sb.Capacity < MAX_UNICODESTRING_LEN) {
                        noOfTimes += 2; // increasing buffer size by 520 in each iteration - perf.
                        int capacity = noOfTimes * MAX_PATH < MAX_UNICODESTRING_LEN ? noOfTimes * MAX_PATH : MAX_UNICODESTRING_LEN;
                        sb.EnsureCapacity(capacity);
                    }
                    sb.Length = length;
                    applicationUri = Path.GetFullPath(sb.ToString());
                    applicationFilename = applicationUri;
                }
            }
            else {
                applicationUri = Path.GetFullPath(exePath);
                if (!FileUtil.FileExists(applicationUri, false))
                    throw ExceptionUtil.ParameterInvalid("exePath");
 
                applicationFilename = applicationUri;
            }
 
            // Fallback if we haven't set the app config file path yet.
            if (_applicationConfigUri == null) {
                _applicationConfigUri = applicationUri + ConfigExtension;
            }
 
            // Set application path
            _applicationUri = applicationUri;
 
            // In the case when exePath was explicitly supplied, we will not be able to 
            // construct user.config paths, so quit here.
            if (exePath != null) {
                return;
            }
 
            // Skip expensive initialization of user config file information if requested.
            if (!_includesUserConfig) {
                return;
            }
 
            bool isHttp = StringUtil.StartsWithIgnoreCase(_applicationConfigUri, HTTP_URI);
 
            SetNamesAndVersion(applicationFilename, exeAssembly, isHttp);
 
            // Check if this is a clickonce deployed application. If so, point the user config
            // files to the clickonce data directory.
            if (this.IsClickOnceDeployed(AppDomain.CurrentDomain)) {
                string dataPath = AppDomain.CurrentDomain.GetData(ClickOnceDataDirectory) as string;
                string versionSuffix = Validate(_productVersion, false);
 
                // NOTE: No roaming config for clickonce - not supported.
                if (Path.IsPathRooted(dataPath)) {
                    _localConfigDirectory = CombineIfValid(dataPath, versionSuffix);
                    _localConfigFilename  = CombineIfValid(_localConfigDirectory, UserConfigFilename);
                }
 
            }
            else if (!isHttp) {
                // If we get the config from http, we do not have a roaming or local config directory,
                // as it cannot be edited by the app in those cases because it does not have Full Trust.
                
                // suffix for user config paths
 
                string part1 = Validate(_companyName, true);
 
                string validAppDomainName = Validate(AppDomain.CurrentDomain.FriendlyName, true);
                string applicationUriLower = !String.IsNullOrEmpty(_applicationUri) ? _applicationUri.ToLower(CultureInfo.InvariantCulture) : null;
                string namePrefix = !String.IsNullOrEmpty(validAppDomainName) ? validAppDomainName : Validate(_productName, true);
                string hashSuffix = GetTypeAndHashSuffix(AppDomain.CurrentDomain, applicationUriLower);
                
                string part2 = (!String.IsNullOrEmpty(namePrefix) && !String.IsNullOrEmpty(hashSuffix)) ? namePrefix + hashSuffix : null;
                
                string part3 = Validate(_productVersion, false);
 
                string dirSuffix = CombineIfValid(CombineIfValid(part1, part2), part3);
    
                string roamingFolderPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
                if (Path.IsPathRooted(roamingFolderPath)) {
                    _roamingConfigDirectory = CombineIfValid(roamingFolderPath, dirSuffix);
                    _roamingConfigFilename = CombineIfValid(_roamingConfigDirectory, UserConfigFilename);
                }
    
                string localFolderPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
                if (Path.IsPathRooted(localFolderPath)) { 
                    _localConfigDirectory = CombineIfValid(localFolderPath, dirSuffix);
                    _localConfigFilename = CombineIfValid(_localConfigDirectory, UserConfigFilename);
                }
            }
        }
 
        internal static ClientConfigPaths GetPaths(string exePath, bool includeUserConfig) {
            ClientConfigPaths result = null;
 
            if (exePath == null) {
                if (s_current == null || (includeUserConfig && !s_currentIncludesUserConfig)) {
                    s_current = new ClientConfigPaths(null, includeUserConfig);
                    s_currentIncludesUserConfig = includeUserConfig;
                }
 
                result = s_current;
            }
            else {
                result = new ClientConfigPaths(exePath, includeUserConfig);
            }
 
            return result;
        }
 
        internal static void RefreshCurrent() {
            s_currentIncludesUserConfig = false;
            s_current = null;
        }
 
        internal static ClientConfigPaths Current {
            get {
                return GetPaths(null, true);
            }
        }
 
        internal bool HasEntryAssembly {
            get {
                return _hasEntryAssembly;
            }
        }
 
        internal string ApplicationUri {
            get {
                return _applicationUri;
            }
        }
 
        internal string ApplicationConfigUri {
            get {
                return _applicationConfigUri;
            }
        }
 
        internal string RoamingConfigFilename {
            get {
                return _roamingConfigFilename;
            }
        }
 
        internal string RoamingConfigDirectory {
            get {
                return _roamingConfigDirectory;
            }
        }
 
        internal bool HasRoamingConfig {
            get {
                // Assume we have roaming config if we haven't loaded user config file information.
                return RoamingConfigFilename != null || !_includesUserConfig;
            }
        }
 
        internal string LocalConfigFilename {
            get {
                return _localConfigFilename;
            }
        }
 
        internal string LocalConfigDirectory {
            get {
                return _localConfigDirectory;
            }
        }
 
        internal bool HasLocalConfig {
            get {
                // Assume we have roaming config if we haven't loaded user config file information.
                return LocalConfigFilename != null || !_includesUserConfig;
            }
        }
 
        internal string ProductName {
            get {
                return _productName;
            }
        }
 
        internal string ProductVersion {
            get {
                return _productVersion;
            }
        }
 
        private static SecurityPermission ControlEvidencePermission {
            get { 
                if (s_controlEvidencePerm == null) {
                    s_controlEvidencePerm = new SecurityPermission(SecurityPermissionFlag.ControlEvidence);
                }
                return s_controlEvidencePerm;
            } 
        }
 
        private static SecurityPermission SerializationFormatterPermission {
            get { 
                if (s_serializationPerm == null) {
                    s_serializationPerm = new SecurityPermission(SecurityPermissionFlag.SerializationFormatter);
                }
                return s_serializationPerm;
            } 
        }
 
        // Combines path2 with path1 if possible, else returns null.
        private string CombineIfValid(string path1, string path2) {
            string returnPath = null;
 
            if (path1 != null && path2 != null) {
                try {
                    string combinedPath = Path.Combine(path1, path2);
                    if (combinedPath.Length < MAX_PATH) {
                        returnPath = combinedPath;
                    }
                }
                catch {
                }
            }
            
            return returnPath;
        }
 
        // Returns a type and hash suffix based on app domain evidence. The evidence we use, in
        // priority order, is Strong Name, Url and Exe Path. If one of these is found, we compute a 
        // SHA1 hash of it and return a suffix based on that. If none is found, we return null.
        private string GetTypeAndHashSuffix(AppDomain appDomain, string exePath) {
            string suffix       = null;
            string typeName     = null;
            object evidenceObj  = null;
            
            evidenceObj = GetEvidenceInfo(appDomain, exePath, out typeName);
 
            if (evidenceObj != null && !String.IsNullOrEmpty(typeName)) {
                MemoryStream ms   = new MemoryStream();
                BinaryFormatter bSer = new BinaryFormatter();
                SerializationFormatterPermission.Assert();
                bSer.Serialize(ms, evidenceObj);
                ms.Position = 0;
                string evidenceHash = GetHash(ms);
 
                if (!String.IsNullOrEmpty(evidenceHash)) {
                    suffix = "_" + typeName + "_" + evidenceHash;
                }
            }
 
            return suffix;
        }
 
        // Mostly borrowed from IsolatedStorage, with some modifications
        private static object GetEvidenceInfo(AppDomain appDomain, string exePath, out string typeName) {
            ControlEvidencePermission.Assert();
            Evidence evidence = appDomain.Evidence;
            StrongName  sn   = null;
            Url         url  = null;
 
            if (evidence != null) {
                IEnumerator e = evidence.GetHostEnumerator();
                object      temp = null;
    
                while (e.MoveNext()) {
                    temp = e.Current;
    
                    if (temp is StrongName) {
                        sn = (StrongName) temp;
                        break;
                    }
                    else if (temp is Url) {
                        url = (Url) temp;
                    }
                }
            }
 
            object o = null;
 
            // The order of preference is StrongName, Url, ExePath.
            if (sn != null) {
                o = MakeVersionIndependent(sn);
                typeName = StrongNameDesc;
            }
            else if (url != null) {
                // Extract the url string and normalize it to use as evidence
                o = url.Value.ToUpperInvariant();
                typeName = UrlDesc;
            }
            else if (exePath != null) {
                o = exePath;
                typeName = PathDesc;
            }
            else {
                typeName = null;
            }
 
            return o;
        }
 
        [SuppressMessage("Microsoft.Security.Cryptography", "CA5354:SHA1CannotBeUsed",
            Justification = "Input for the SHA1 hash computation is safe, it is a Strong Name, a Url or an Exe Path, and comes from Assembly.GetEntryAssembly(); or from AppDomain.CurrentDomain.Evidence. This hash is used to persist application settings between application runs, if we change algorithm, applications that rely on this feature will loose user settings.")]
        private static String GetHash(Stream s) {
            byte[] hash;
 
            using (SHA1 sha1 = new SHA1CryptoServiceProvider()) {
                hash = sha1.ComputeHash(s);
            }
 
            return ToBase32StringSuitableForDirName(hash);
        }
 
        private bool IsClickOnceDeployed(AppDomain appDomain) {
            // NOTE: For perf & servicing reasons, we don't want to introduce a dependency on
            //       System.Deployment.dll here. The following code is an alternative to calling
            //       ApplicationDeployment.IsNetworkDeployed.
 
            ActivationContext actCtx = appDomain.ActivationContext;
 
            // Ensures the app is running with a context from the store.
            if (actCtx != null && actCtx.Form == ActivationContext.ContextForm.StoreBounded) {
                string fullAppId = actCtx.Identity.FullName;
                if (!String.IsNullOrEmpty(fullAppId)) {
                    return true;
                }
            }
 
            return false;
        }
 
        private static StrongName MakeVersionIndependent(StrongName sn) {
            return new StrongName(sn.PublicKey, sn.Name, new Version(0,0,0,0));
        }
 
        private void SetNamesAndVersion(string applicationFilename, Assembly exeAssembly, bool isHttp) {
            Type        mainType = null;
 
            //
            // Get CompanyName, ProductName, and ProductVersion
            // First try custom attributes on the assembly.
            //
            if (exeAssembly != null) {
                object[] attrs = exeAssembly.GetCustomAttributes(typeof(AssemblyCompanyAttribute), false);
                if (attrs != null && attrs.Length > 0) {
                    _companyName = ((AssemblyCompanyAttribute)attrs[0]).Company;
                    if (_companyName != null) {
                        _companyName = _companyName.Trim();
                    }
                }
 
                attrs = exeAssembly.GetCustomAttributes(typeof(AssemblyProductAttribute), false);
                if (attrs != null && attrs.Length > 0) {
                    _productName = ((AssemblyProductAttribute)attrs[0]).Product;
                    if (_productName != null) {
                        _productName = _productName.Trim();
                    }
                }
 
                _productVersion = exeAssembly.GetName().Version.ToString();
                if (_productVersion != null) {
                    _productVersion = _productVersion.Trim();
                }
            }
 
            //
            // If we couldn't get custom attributes, try the Win32 file version
            // 
            if (!isHttp && (String.IsNullOrEmpty(_companyName) || String.IsNullOrEmpty(_productName) || String.IsNullOrEmpty(_productVersion))) {
                string versionInfoFileName = null;
 
                if (exeAssembly != null) {
                    MethodInfo entryPoint = exeAssembly.EntryPoint;
                    if (entryPoint != null) {
                        mainType = entryPoint.ReflectedType;
                        if (mainType != null) {
                            versionInfoFileName = mainType.Module.FullyQualifiedName;
                        }
                    }
                }
 
                if (versionInfoFileName == null) {
                    versionInfoFileName = applicationFilename;
                }
 
                if (versionInfoFileName != null) {
                    System.Diagnostics.FileVersionInfo version = System.Diagnostics.FileVersionInfo.GetVersionInfo(versionInfoFileName); 
                    if (version != null) {
                        if (String.IsNullOrEmpty(_companyName)) {
                            _companyName = version.CompanyName;
                            if (_companyName != null) {
                                _companyName = _companyName.Trim();
                            }
                        }
 
                        if (String.IsNullOrEmpty(_productName)) {
                            _productName = version.ProductName;
                            if (_productName != null) {
                                _productName = _productName.Trim();
                            }
                        }
 
                        if (String.IsNullOrEmpty(_productVersion)) {
                            _productVersion = version.ProductVersion;
                            if (_productVersion != null) {
                                _productVersion = _productVersion.Trim();
                            }
                        }
                    }
                }
            }
 
            if (String.IsNullOrEmpty(_companyName) || String.IsNullOrEmpty(_productName)) {
                string  ns = null;
                if (mainType != null) {
                    ns = mainType.Namespace;
                }
 
                // Desperate measures for product name
                if (String.IsNullOrEmpty(_productName)) {
                    // Try the remainder of the namespace
                    if (ns != null) {
                        int lastDot = ns.LastIndexOf(".", StringComparison.Ordinal);
                        if (lastDot != -1 && lastDot < ns.Length - 1) {
                            _productName = ns.Substring(lastDot+1);
                        }
                        else {
                            _productName = ns;
                        }
 
                        _productName = _productName.Trim();
                    }
 
                    // Try the type of the entry assembly
                    if (String.IsNullOrEmpty(_productName) && mainType != null) {
                        _productName = mainType.Name.Trim();
                    }
 
                    // give up, return empty string
                    if (_productName == null) {
                        _productName = string.Empty;
                    }
                }
 
                // Desperate measures for company name
                if (String.IsNullOrEmpty(_companyName)) {
                    // Try the first part of the namespace
                    if (ns != null) {
                        int firstDot = ns.IndexOf(".", StringComparison.Ordinal);
                        if (firstDot != -1) {
                            _companyName = ns.Substring(0, firstDot);
                        }
                        else {
                            _companyName = ns;
                        }
 
                        _companyName = _companyName.Trim();
                    }
 
                    // If that doesn't work, use the product name
                    if (String.IsNullOrEmpty(_companyName)) {
                        _companyName = _productName;
                    }
                }
            }
 
            // Desperate measures for product version - assume 1.0
            if (String.IsNullOrEmpty(_productVersion)) {
                _productVersion = "1.0.0.0";
            }
        }
 
        // Borrowed from IsolatedStorage
        private static string ToBase32StringSuitableForDirName(byte[] buff) {
            StringBuilder sb = new StringBuilder();
            byte b0, b1, b2, b3, b4;
            int  l, i;
        
            l = buff.Length;
            i = 0;
        
            // Create l chars using the last 5 bits of each byte.  
            // Consume 3 MSB bits 5 bytes at a time.
        
            do
            {
                b0 = (i < l) ? buff[i++] : (byte)0;
                b1 = (i < l) ? buff[i++] : (byte)0;
                b2 = (i < l) ? buff[i++] : (byte)0;
                b3 = (i < l) ? buff[i++] : (byte)0;
                b4 = (i < l) ? buff[i++] : (byte)0;
        
                // Consume the 5 Least significant bits of each byte
                sb.Append(s_Base32Char[b0 & 0x1F]);
                sb.Append(s_Base32Char[b1 & 0x1F]);
                sb.Append(s_Base32Char[b2 & 0x1F]);
                sb.Append(s_Base32Char[b3 & 0x1F]);
                sb.Append(s_Base32Char[b4 & 0x1F]);
        
                // Consume 3 MSB of b0, b1, MSB bits 6, 7 of b3, b4
                sb.Append(s_Base32Char[(
                        ((b0 & 0xE0) >> 5) | 
                        ((b3 & 0x60) >> 2))]);
        
                sb.Append(s_Base32Char[(
                        ((b1 & 0xE0) >> 5) | 
                        ((b4 & 0x60) >> 2))]);
        
                // Consume 3 MSB bits of b2, 1 MSB bit of b3, b4
                
                b2 >>= 5;
        
                if ((b3 & 0x80) != 0)
                    b2 |= 0x08;
                if ((b4 & 0x80) != 0)
                    b2 |= 0x10;
        
                sb.Append(s_Base32Char[b2]);
        
            } while (i < l);
        
            return sb.ToString();
        }
 
        // Makes the passed in string suitable to use as a path name by replacing illegal characters
        // with underscores. Additionally, we do two things - replace spaces too with underscores and
        // limit the resultant string's length to MAX_LENGTH_TO_USE if limitSize is true.
        private string Validate(string str, bool limitSize) {
            string validated = str;
 
            if (!String.IsNullOrEmpty(validated)) {
                // First replace all illegal characters with underscores
                foreach (char c in Path.GetInvalidFileNameChars()) {
                    validated = validated.Replace(c, '_');
                }
    
                // Replace all spaces with underscores
                validated = validated.Replace(' ', '_');
 
                if (limitSize) {
                    validated = (validated.Length > MAX_LENGTH_TO_USE) ? validated.Substring(0, MAX_LENGTH_TO_USE) : validated;
                }
            }
 
            return validated;
        }
    }
}