diff --git a/src/libraries/Common/src/Interop/OSX/Interop.CoreFoundation.CFBundle.cs b/src/libraries/Common/src/Interop/OSX/Interop.CoreFoundation.CFBundle.cs new file mode 100644 index 00000000000000..d2c002d0f0aa8e --- /dev/null +++ b/src/libraries/Common/src/Interop/OSX/Interop.CoreFoundation.CFBundle.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class CoreFoundation + { + internal const uint kCFStringEncodingUTF8 = 0x08000100; + + [LibraryImport(Libraries.CoreFoundationLibrary)] + internal static partial IntPtr CFBundleGetIdentifier(IntPtr bundle); + + [LibraryImport(Libraries.CoreFoundationLibrary)] + internal static partial IntPtr CFBundleGetMainBundle(); + + [LibraryImport(Libraries.CoreFoundationLibrary)] + internal static unsafe partial byte CFStringGetCString( + IntPtr value, + byte* buffer, + IntPtr bufferSize, + uint encoding); + + [LibraryImport(Libraries.CoreFoundationLibrary)] + internal static partial IntPtr CFStringGetLength(IntPtr value); + + [LibraryImport(Libraries.CoreFoundationLibrary)] + internal static partial IntPtr CFStringGetMaximumSizeForEncoding(IntPtr length, uint encoding); + } +} diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetCurrentPackageFamilyName.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetCurrentPackageFamilyName.cs new file mode 100644 index 00000000000000..e9d3678275fbc0 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetCurrentPackageFamilyName.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Kernel32 + { + [LibraryImport(Libraries.Kernel32)] + internal static unsafe partial int GetCurrentPackageFamilyName(uint* packageFamilyNameLength, char* packageFamilyName); + } +} diff --git a/src/libraries/System.Configuration.ConfigurationManager/src/Interop/Interop.Libraries.cs b/src/libraries/System.Configuration.ConfigurationManager/src/Interop/Interop.Libraries.cs new file mode 100644 index 00000000000000..9704b0eacd13f7 --- /dev/null +++ b/src/libraries/System.Configuration.ConfigurationManager/src/Interop/Interop.Libraries.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +internal static partial class Interop +{ + internal static partial class Libraries + { + internal const string CoreFoundationLibrary = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation"; + internal const string Kernel32 = "kernel32.dll"; + } +} diff --git a/src/libraries/System.Configuration.ConfigurationManager/src/System.Configuration.ConfigurationManager.csproj b/src/libraries/System.Configuration.ConfigurationManager/src/System.Configuration.ConfigurationManager.csproj index 7e2a56b0409616..500cccd6e26800 100644 --- a/src/libraries/System.Configuration.ConfigurationManager/src/System.Configuration.ConfigurationManager.csproj +++ b/src/libraries/System.Configuration.ConfigurationManager/src/System.Configuration.ConfigurationManager.csproj @@ -20,6 +20,10 @@ + + + + @@ -249,6 +253,7 @@ + diff --git a/src/libraries/System.Configuration.ConfigurationManager/src/System/Configuration/AppleApplication.cs b/src/libraries/System.Configuration.ConfigurationManager/src/System/Configuration/AppleApplication.cs new file mode 100644 index 00000000000000..1a3e3ecdd9cacf --- /dev/null +++ b/src/libraries/System.Configuration.ConfigurationManager/src/System/Configuration/AppleApplication.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Text; + +namespace System.Configuration +{ + internal static class AppleApplication + { + internal static unsafe string GetMainBundleIdentifier() + { + IntPtr bundle = Interop.CoreFoundation.CFBundleGetMainBundle(); + IntPtr identifier = bundle == IntPtr.Zero + ? IntPtr.Zero + : Interop.CoreFoundation.CFBundleGetIdentifier(bundle); + if (identifier == IntPtr.Zero) + { + return null; + } + + IntPtr length = Interop.CoreFoundation.CFStringGetLength(identifier); + long maximumByteCount = Interop.CoreFoundation.CFStringGetMaximumSizeForEncoding( + length, + Interop.CoreFoundation.kCFStringEncodingUTF8).ToInt64(); + if (maximumByteCount < 0 || maximumByteCount >= int.MaxValue) + { + return null; + } + + byte[] buffer = new byte[(int)maximumByteCount + 1]; + fixed (byte* bufferPtr = buffer) + { + if (Interop.CoreFoundation.CFStringGetCString( + identifier, + bufferPtr, + new IntPtr(buffer.Length), + Interop.CoreFoundation.kCFStringEncodingUTF8) == 0) + { + return null; + } + } + + int terminator = Array.IndexOf(buffer, (byte)0); + if (terminator < 0) + { + return null; + } + + return Encoding.UTF8.GetString(buffer, 0, terminator); + } + } +} diff --git a/src/libraries/System.Configuration.ConfigurationManager/src/System/Configuration/ClientConfigPaths.cs b/src/libraries/System.Configuration.ConfigurationManager/src/System/Configuration/ClientConfigPaths.cs index f8a034252762c7..d9c74a47b5b950 100644 --- a/src/libraries/System.Configuration.ConfigurationManager/src/System/Configuration/ClientConfigPaths.cs +++ b/src/libraries/System.Configuration.ConfigurationManager/src/System/Configuration/ClientConfigPaths.cs @@ -22,6 +22,8 @@ internal sealed class ClientConfigPaths private const string StrongNameDesc = "StrongName"; private const string UrlDesc = "Url"; private const string PathDesc = "Path"; + private const string BundleIdentifierDesc = "BundleIdentifier"; + private const string PackageFamilyNameDesc = "PackageFamilyName"; private static ClientConfigPaths s_current; private static volatile bool s_currentIncludesUserConfig; @@ -142,10 +144,22 @@ private ClientConfigPaths(string exePath, bool includeUserConfig) string applicationUriLower = !string.IsNullOrEmpty(ApplicationUri) ? ApplicationUri.ToLowerInvariant() : null; - string hashSuffix = GetTypeAndHashSuffix(applicationUriLower, isSingleFile); + GetStableApplicationIdentity(out string stableIdentityType, out string stableIdentity); + string hashSuffix = GetApplicationIdentitySuffix( + applicationUriLower, + isSingleFile, + stableIdentityType, + stableIdentity); string part2 = !string.IsNullOrEmpty(namePrefix) && !string.IsNullOrEmpty(hashSuffix) ? namePrefix + hashSuffix : null; + LegacyConfigDirectoryPrefix = !string.IsNullOrEmpty(stableIdentity) && + !string.IsNullOrEmpty(namePrefix) + ? namePrefix + "_" + : null; + StableConfigDirectoryName = LegacyConfigDirectoryPrefix is not null + ? part2 + : null; // (3) The product version string part3 = Validate(ProductVersion, limitSize: false); @@ -191,6 +205,10 @@ private ClientConfigPaths(string exePath, bool includeUserConfig) internal string ProductVersion { get; private set; } + internal string LegacyConfigDirectoryPrefix { get; } + + internal string StableConfigDirectoryName { get; } + internal static ClientConfigPaths GetPaths(string exePath, bool includeUserConfig) { ClientConfigPaths result; @@ -231,6 +249,58 @@ private static string CombineIfValid(string path1, string path2) } } + internal static string GetApplicationIdentitySuffix( + string exePath, + bool isSingleFile, + string stableIdentityType, + string stableIdentity) + { + if (!string.IsNullOrEmpty(stableIdentityType) && !string.IsNullOrEmpty(stableIdentity)) + { + try + { + string hash = IdentityHelper.GetStrongHashSuitableForObjectName(stableIdentity); + return "_" + stableIdentityType + "_" + hash; + } + catch (PlatformNotSupportedException) + { + } + } + + return GetTypeAndHashSuffix(exePath, isSingleFile); + } + + private static void GetStableApplicationIdentity(out string identityType, out string identity) + { + if (IsAlwaysSandboxedAppleMobile()) + { + identityType = BundleIdentifierDesc; + identity = AppleApplication.GetMainBundleIdentifier(); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + identityType = PackageFamilyNameDesc; + try + { + identity = WindowsApplication.GetCurrentPackageFamilyName(); + } + catch (EntryPointNotFoundException) + { + identity = null; + } + } + else + { + identityType = null; + identity = null; + } + + if (string.IsNullOrEmpty(identity)) + { + identityType = null; + } + } + // Returns a type and hash suffix based on what used to come from 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. @@ -287,6 +357,13 @@ private static string GetTypeAndHashSuffix(string exePath, bool isSingleFile) return suffix; } + private static bool IsAlwaysSandboxedAppleMobile() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Create("TVOS")) || + (RuntimeInformation.IsOSPlatform(OSPlatform.Create("IOS")) && + !RuntimeInformation.IsOSPlatform(OSPlatform.Create("MACCATALYST"))); + } + private void SetNamesAndVersion(Assembly exeAssembly, bool isHttp) { Type mainType = null; diff --git a/src/libraries/System.Configuration.ConfigurationManager/src/System/Configuration/LocalFileSettingsProvider.cs b/src/libraries/System.Configuration.ConfigurationManager/src/System/Configuration/LocalFileSettingsProvider.cs index b2a2ee7085f080..6286c4fdb9d6d4 100644 --- a/src/libraries/System.Configuration.ConfigurationManager/src/System/Configuration/LocalFileSettingsProvider.cs +++ b/src/libraries/System.Configuration.ConfigurationManager/src/System/Configuration/LocalFileSettingsProvider.cs @@ -288,63 +288,214 @@ private string GetPreviousConfigFileName(bool isRoaming) string userConfigPath = isRoaming ? ConfigurationManagerInternalFactory.Instance.ExeRoamingConfigDirectory : ConfigurationManagerInternalFactory.Instance.ExeLocalConfigDirectory; + prevConfigFile = FindPreviousConfigFile( + userConfigPath, + ConfigurationManagerInternalFactory.Instance.ExeProductVersion, + ConfigurationManagerInternalFactory.Instance.UserConfigFilename, + ClientConfigPaths.Current.LegacyConfigDirectoryPrefix, + ClientConfigPaths.Current.StableConfigDirectoryName); - Version currentVersion; - if (!Version.TryParse(ConfigurationManagerInternalFactory.Instance.ExeProductVersion, out currentVersion)) + // Cache for future use. + if (isRoaming) + { + _prevRoamingConfigFileName = prevConfigFile; + } + else { - return null; + _prevLocalConfigFileName = prevConfigFile; } + } + + return prevConfigFile; + } + + internal static string FindPreviousConfigFile( + string currentConfigDirectory, + string currentVersionString, + string userConfigFilename, + string legacyDirectoryPrefix, + string stableConfigDirectoryName) + { + if (string.IsNullOrEmpty(currentConfigDirectory) || + string.IsNullOrEmpty(userConfigFilename) || + !Version.TryParse(currentVersionString, out Version currentVersion)) + { + return null; + } + + if (string.IsNullOrEmpty(legacyDirectoryPrefix) || + string.IsNullOrEmpty(stableConfigDirectoryName)) + { + return FindPreviousConfigFileUsingExistingBehavior( + currentConfigDirectory, + currentVersion, + userConfigFilename); + } + + DirectoryInfo currentVersionDirectory = new DirectoryInfo(currentConfigDirectory); + if (!string.Equals(currentVersionDirectory.Name, currentVersionString, StringComparison.Ordinal)) + { + return null; + } + + DirectoryInfo currentIdentityDirectory = currentVersionDirectory.Parent; + if (currentIdentityDirectory is null) + { + return null; + } - Version previousVersion = null; - DirectoryInfo previousDirectory = null; - string file = null; + string previousConfigFile = FindPreviousConfigFile( + currentIdentityDirectory, + currentVersion, + userConfigFilename, + out _); + if (previousConfigFile is not null) + { + return previousConfigFile; + } + + if (!string.Equals(currentIdentityDirectory.Name, stableConfigDirectoryName, StringComparison.Ordinal)) + { + return null; + } + + DirectoryInfo companyDirectory = currentIdentityDirectory.Parent; + if (companyDirectory is null || !companyDirectory.Exists) + { + return null; + } - DirectoryInfo parentDirectory = Directory.GetParent(userConfigPath); + Version highestVersion = null; + string highestVersionConfigFile = null; + bool highestVersionIsAmbiguous = false; - if (parentDirectory.Exists) + foreach (DirectoryInfo identityDirectory in companyDirectory.GetDirectories()) + { + if (!IsLegacyIdentityDirectoryName(identityDirectory.Name, legacyDirectoryPrefix)) { - foreach (DirectoryInfo directory in parentDirectory.GetDirectories()) - { - Version tempVersion; + continue; + } - if (Version.TryParse(directory.Name, out tempVersion) && tempVersion < currentVersion) - { - if (previousVersion == null) - { - previousVersion = tempVersion; - previousDirectory = directory; - } - else if (tempVersion > previousVersion) - { - previousVersion = tempVersion; - previousDirectory = directory; - } - } - } + string candidateConfigFile = FindPreviousConfigFile( + identityDirectory, + currentVersion, + userConfigFilename, + out Version candidateVersion); + if (candidateConfigFile is null) + { + continue; + } - if (previousDirectory != null) - { - file = Path.Combine(previousDirectory.FullName, ConfigurationManagerInternalFactory.Instance.UserConfigFilename); - } + if (highestVersion is null || candidateVersion > highestVersion) + { + highestVersion = candidateVersion; + highestVersionConfigFile = candidateConfigFile; + highestVersionIsAmbiguous = false; + } + else if (candidateVersion == highestVersion) + { + highestVersionIsAmbiguous = true; + } + } - if (File.Exists(file)) - { - prevConfigFile = file; - } + return highestVersionIsAmbiguous ? null : highestVersionConfigFile; + } + + private static string FindPreviousConfigFileUsingExistingBehavior( + string currentConfigDirectory, + Version currentVersion, + string userConfigFilename) + { + DirectoryInfo identityDirectory = Directory.GetParent(currentConfigDirectory); + if (identityDirectory is null || !identityDirectory.Exists) + { + return null; + } + + Version previousVersion = null; + DirectoryInfo previousVersionDirectory = null; + + foreach (DirectoryInfo versionDirectory in identityDirectory.GetDirectories()) + { + if (Version.TryParse(versionDirectory.Name, out Version version) && + version < currentVersion && + (previousVersion is null || version > previousVersion)) + { + previousVersion = version; + previousVersionDirectory = versionDirectory; } + } - // Cache for future use. - if (isRoaming) + if (previousVersionDirectory is null) + { + return null; + } + + string configFile = Path.Combine(previousVersionDirectory.FullName, userConfigFilename); + return File.Exists(configFile) ? configFile : null; + } + + private static string FindPreviousConfigFile( + DirectoryInfo identityDirectory, + Version currentVersion, + string userConfigFilename, + out Version previousVersion) + { + previousVersion = null; + string previousConfigFile = null; + + if (!identityDirectory.Exists) + { + return null; + } + + foreach (DirectoryInfo versionDirectory in identityDirectory.GetDirectories()) + { + if (!Version.TryParse(versionDirectory.Name, out Version version) || + version >= currentVersion || + (previousVersion is not null && version <= previousVersion)) { - _prevRoamingConfigFileName = prevConfigFile; + continue; } - else + + string configFile = Path.Combine(versionDirectory.FullName, userConfigFilename); + if (File.Exists(configFile)) { - _prevLocalConfigFileName = prevConfigFile; + previousVersion = version; + previousConfigFile = configFile; } } - return prevConfigFile; + return previousConfigFile; + } + + private static bool IsLegacyIdentityDirectoryName(string directoryName, string prefix) + { + return IsIdentityDirectoryName(directoryName, prefix + "StrongName_") || + IsIdentityDirectoryName(directoryName, prefix + "Url_") || + IsIdentityDirectoryName(directoryName, prefix + "Path_"); + } + + private static bool IsIdentityDirectoryName(string directoryName, string prefix) + { + const int Sha1Base32Length = 32; + + if (!directoryName.StartsWith(prefix, StringComparison.Ordinal) || + directoryName.Length != prefix.Length + Sha1Base32Length) + { + return false; + } + + for (int i = prefix.Length; i < directoryName.Length; i++) + { + char c = directoryName[i]; + if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '5'))) + { + return false; + } + } + + return true; } /// diff --git a/src/libraries/System.Configuration.ConfigurationManager/src/System/Configuration/WindowsApplication.cs b/src/libraries/System.Configuration.ConfigurationManager/src/System/Configuration/WindowsApplication.cs new file mode 100644 index 00000000000000..9d5739b0f6e003 --- /dev/null +++ b/src/libraries/System.Configuration.ConfigurationManager/src/System/Configuration/WindowsApplication.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel; + +namespace System.Configuration +{ + internal static class WindowsApplication + { + private const int APPMODEL_ERROR_NO_PACKAGE = 15700; + private const int ERROR_INSUFFICIENT_BUFFER = 122; + private const int ERROR_SUCCESS = 0; + + internal static unsafe string GetCurrentPackageFamilyName() + { + uint length = 0; + int error = Interop.Kernel32.GetCurrentPackageFamilyName(&length, null); + if (error == APPMODEL_ERROR_NO_PACKAGE) + { + return null; + } + + if (error != ERROR_INSUFFICIENT_BUFFER) + { + throw new Win32Exception(error); + } + + char[] buffer = new char[length]; + fixed (char* bufferPtr = buffer) + { + error = Interop.Kernel32.GetCurrentPackageFamilyName(&length, bufferPtr); + } + + if (error != ERROR_SUCCESS) + { + throw new Win32Exception(error); + } + + return new string(buffer, 0, (int)length - 1); + } + } +} diff --git a/src/libraries/System.Configuration.ConfigurationManager/tests/System/Configuration/LocalFileSettingsProviderTests.cs b/src/libraries/System.Configuration.ConfigurationManager/tests/System/Configuration/LocalFileSettingsProviderTests.cs index 3e56c3b5340924..c42d1bdd417d91 100644 --- a/src/libraries/System.Configuration.ConfigurationManager/tests/System/Configuration/LocalFileSettingsProviderTests.cs +++ b/src/libraries/System.Configuration.ConfigurationManager/tests/System/Configuration/LocalFileSettingsProviderTests.cs @@ -3,6 +3,8 @@ using System; using System.Configuration; +using System.IO; +using System.Reflection; using Xunit; namespace System.ConfigurationTests @@ -74,5 +76,237 @@ public void GetPropertyValues_DefaultValueApplied(object defaultValue) Assert.Equal(1, propertyValues.Count); Assert.Equal(defaultValue, propertyValues["Test"].PropertyValue); } + + [Theory] + [InlineData("BundleIdentifier", "com.contoso.test")] + [InlineData("PackageFamilyName", "Microsoft.Windows.Photos_8wekyb3d8bbwe")] + public void StableIdentity_UsesBoundedHashAcrossInstallationPaths(string identityType, string identity) + { + const string FirstPath = "/var/containers/Bundle/Application/F67E5161-EBAA-4084-B89C-2D17C837D315/Test.app/Test.dll"; + const string SecondPath = "/var/containers/Bundle/Application/9293AD65-BCC7-453C-8D42-8902B64FF19E/Test.app/Test.dll"; + + string first = GetApplicationIdentitySuffix( + FirstPath, + isSingleFile: false, + identityType, + identity); + string second = GetApplicationIdentitySuffix( + SecondPath, + isSingleFile: false, + identityType, + identity); + + Assert.Equal(first, second); + Assert.StartsWith("_" + identityType + "_", first); + Assert.Equal(identityType.Length + 34, first.Length); + } + + [Fact] + public void NoStableIdentity_PreservesExistingBehavior() + { + const string ApplicationPath = "/Applications/Test/Test.dll"; + + string expected = GetApplicationIdentitySuffix( + ApplicationPath, + isSingleFile: false, + stableIdentityType: null, + stableIdentity: null); + string actual = GetApplicationIdentitySuffix( + ApplicationPath, + isSingleFile: false, + stableIdentityType: null, + stableIdentity: "com.contoso.ignored"); + + Assert.Equal(expected, actual); + Assert.DoesNotContain("BundleIdentifier", actual); + Assert.DoesNotContain("PackageFamilyName", actual); + } + + [PlatformSpecific(TestPlatforms.iOS | TestPlatforms.tvOS)] + [Fact] + public void AppleMobileBundleIdentifier_IsAvailable() + { + Type appleApplication = typeof(LocalFileSettingsProvider).Assembly.GetType("System.Configuration.AppleApplication"); + MethodInfo getMainBundleIdentifier = appleApplication.GetMethod( + "GetMainBundleIdentifier", + BindingFlags.NonPublic | BindingFlags.Static); + + Assert.False(string.IsNullOrEmpty((string)getMainBundleIdentifier.Invoke(null, null))); + } + + [Fact] + public void FindPreviousConfigFile_StableHierarchyTakesPrecedence() + { + using var temp = new TempDirectory(); + string companyDirectory = temp.Path; + string stableIdentity = Path.Combine(companyDirectory, StableIdentityName); + string currentDirectory = CreateVersion(stableIdentity, "3.0.0.0", createConfig: false); + string expected = Path.Combine(CreateVersion(stableIdentity, "1.0.0.0"), UserConfigFilename); + CreateVersion(Path.Combine(companyDirectory, LegacyIdentityName('a')), "2.0.0.0"); + + string actual = FindPreviousConfigFile( + currentDirectory, + "3.0.0.0", + UserConfigFilename, + LegacyPrefix, + StableIdentityName); + + Assert.Equal(expected, actual); + } + + [Fact] + public void FindPreviousConfigFile_WithoutLegacyPrefixPreservesExistingSelection() + { + using var temp = new TempDirectory(); + string identityDirectory = Path.Combine(temp.Path, "TestApp_Url_" + new string('a', 32)); + string currentDirectory = CreateVersion(identityDirectory, "3.0.0.0", createConfig: false); + CreateVersion(identityDirectory, "1.0.0.0"); + CreateVersion(identityDirectory, "2.0.0.0", createConfig: false); + + string actual = FindPreviousConfigFile( + currentDirectory, + "3.0.0.0", + UserConfigFilename, + legacyDirectoryPrefix: null, + stableConfigDirectoryName: null); + + Assert.Null(actual); + } + + [Fact] + public void FindPreviousConfigFile_LegacyHierarchySelectsHighestValidPriorVersion() + { + using var temp = new TempDirectory(); + string companyDirectory = temp.Path; + string currentDirectory = CreateVersion( + Path.Combine(companyDirectory, StableIdentityName), + "4.0.0.0", + createConfig: false); + CreateVersion(Path.Combine(companyDirectory, LegacyIdentityName('a')), "1.0.0.0"); + string expected = Path.Combine( + CreateVersion(Path.Combine(companyDirectory, LegacyIdentityName('b')), "3.0.0.0"), + UserConfigFilename); + CreateVersion(Path.Combine(companyDirectory, LegacyIdentityName('c')), "4.0.0.0"); + CreateVersion(Path.Combine(companyDirectory, LegacyIdentityName('d')), "5.0.0.0"); + + string actual = FindPreviousConfigFile( + currentDirectory, + "4.0.0.0", + UserConfigFilename, + LegacyPrefix, + StableIdentityName); + + Assert.Equal(expected, actual); + } + + [Fact] + public void FindPreviousConfigFile_ExcludesMalformedAndUnrelatedDirectories() + { + using var temp = new TempDirectory(); + string companyDirectory = temp.Path; + string currentDirectory = CreateVersion( + Path.Combine(companyDirectory, StableIdentityName), + "3.0.0.0", + createConfig: false); + CreateVersion(Path.Combine(companyDirectory, "OtherApp_Url_" + new string('a', 32)), "2.0.0.0"); + CreateVersion(Path.Combine(companyDirectory, LegacyPrefix + "Url_" + new string('6', 32)), "2.0.0.0"); + CreateVersion(Path.Combine(companyDirectory, LegacyPrefix + "Unknown_" + new string('a', 32)), "2.0.0.0"); + CreateVersion(Path.Combine(companyDirectory, LegacyPrefix + "Url_short"), "2.0.0.0"); + CreateVersion(Path.Combine(companyDirectory, LegacyIdentityName('a')), "not-a-version"); + CreateVersion(Path.Combine(companyDirectory, LegacyIdentityName('b')), "2.0.0.0", createConfig: false); + + string actual = FindPreviousConfigFile( + currentDirectory, + "3.0.0.0", + UserConfigFilename, + LegacyPrefix, + StableIdentityName); + + Assert.Null(actual); + } + + [Fact] + public void FindPreviousConfigFile_AmbiguousHighestLegacyVersionIsNotSelected() + { + using var temp = new TempDirectory(); + string companyDirectory = temp.Path; + string currentDirectory = CreateVersion( + Path.Combine(companyDirectory, StableIdentityName), + "3.0.0.0", + createConfig: false); + CreateVersion(Path.Combine(companyDirectory, LegacyIdentityName('a')), "2.0.0.0"); + CreateVersion(Path.Combine(companyDirectory, LegacyIdentityName('b')), "2.0.0.0"); + + string actual = FindPreviousConfigFile( + currentDirectory, + "3.0.0.0", + UserConfigFilename, + LegacyPrefix, + StableIdentityName); + + Assert.Null(actual); + } + + private const string LegacyPrefix = "TestApp_"; + private const string StableIdentityName = LegacyPrefix + "BundleIdentifier_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + private const string UserConfigFilename = "user.config"; + + private static string GetApplicationIdentitySuffix( + string applicationPath, + bool isSingleFile, + string stableIdentityType, + string stableIdentity) + { + Type clientConfigPaths = typeof(LocalFileSettingsProvider).Assembly.GetType("System.Configuration.ClientConfigPaths"); + MethodInfo getApplicationIdentitySuffix = clientConfigPaths.GetMethod( + "GetApplicationIdentitySuffix", + BindingFlags.NonPublic | BindingFlags.Static); + + return (string)getApplicationIdentitySuffix.Invoke( + null, + new object[] { applicationPath, isSingleFile, stableIdentityType, stableIdentity }); + } + + private static string FindPreviousConfigFile( + string currentConfigDirectory, + string currentVersion, + string userConfigFilename, + string legacyDirectoryPrefix, + string stableConfigDirectoryName) + { + MethodInfo findPreviousConfigFile = typeof(LocalFileSettingsProvider).GetMethod( + "FindPreviousConfigFile", + BindingFlags.NonPublic | BindingFlags.Static, + binder: null, + new[] { typeof(string), typeof(string), typeof(string), typeof(string), typeof(string) }, + modifiers: null); + + return (string)findPreviousConfigFile.Invoke( + null, + new object[] + { + currentConfigDirectory, + currentVersion, + userConfigFilename, + legacyDirectoryPrefix, + stableConfigDirectoryName + }); + } + + private static string LegacyIdentityName(char hashCharacter) + { + return LegacyPrefix + "Url_" + new string(hashCharacter, 32); + } + + private static string CreateVersion(string identityDirectory, string version, bool createConfig = true) + { + string versionDirectory = Directory.CreateDirectory(Path.Combine(identityDirectory, version)).FullName; + if (createConfig) + { + File.WriteAllText(Path.Combine(versionDirectory, UserConfigFilename), ""); + } + + return versionDirectory; + } } }