Skip to content

Commit

Permalink
Change PathInternal.IsCaseSensitive to a constant (#54340)
Browse files Browse the repository at this point in the history
* Return constants in PathInternal.GetIsCaseSensitive() on mobile platforms

   In particular on Android probing using I/O is slow and contributes to slow app startup.

   Fixes #54339

* Implement Path.IsCaseSensitive as PathInternal.IsCaseSensitive

   Also Path.StringComparison => PathInternal.StringComparison

* Add test for PathInternal.IsCaseSensitive

   Move GetIsCaseSensitiveByProbing to FileSystemTest

* Drop PathInternal.s_isCaseSensitive cache field

* Delete Path.IsCaseSensitive and Path.StringComparison

   update callers to use PathInternal.IsCaseSensitive and PathInternal.StringComparison

* Remove catch clause from GetIsCaseSensitiveByProbing

* Mark new test [OuterLoop]

* Apply suggestions from code review

Co-authored-by: Stephen Toub <stoub@microsoft.com>
Co-authored-by: Jan Kotas <jkotas@microsoft.com>
Co-authored-by: Adam Sitnik <adam.sitnik@gmail.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
  • Loading branch information
4 people committed Jun 23, 2021
1 parent fa86c81 commit c0ed319
Show file tree
Hide file tree
Showing 9 changed files with 52 additions and 51 deletions.
36 changes: 8 additions & 28 deletions src/libraries/Common/src/System/IO/PathInternal.CaseSensitivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,47 +12,27 @@ namespace System.IO
/// <summary>Contains internal path helpers that are shared between many projects.</summary>
internal static partial class PathInternal
{
private static readonly bool s_isCaseSensitive = GetIsCaseSensitive();

/// <summary>Returns a comparison that can be used to compare file and directory names for equality.</summary>
internal static StringComparison StringComparison
{
get
{
return s_isCaseSensitive ?
return IsCaseSensitive ?
StringComparison.Ordinal :
StringComparison.OrdinalIgnoreCase;
}
}

/// <summary>Gets whether the system is case-sensitive.</summary>
internal static bool IsCaseSensitive { get { return s_isCaseSensitive; } }

/// <summary>
/// Determines whether the file system is case sensitive.
/// </summary>
/// <remarks>
/// Ideally we'd use something like pathconf with _PC_CASE_SENSITIVE, but that is non-portable,
/// not supported on Windows or Linux, etc. For now, this function creates a tmp file with capital letters
/// and then tests for its existence with lower-case letters. This could return invalid results in corner
/// cases where, for example, different file systems are mounted with differing sensitivities.
/// </remarks>
private static bool GetIsCaseSensitive()
internal static bool IsCaseSensitive
{
try
{
string pathWithUpperCase = Path.Combine(Path.GetTempPath(), "CASESENSITIVETEST" + Guid.NewGuid().ToString("N"));
using (new FileStream(pathWithUpperCase, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, 0x1000, FileOptions.DeleteOnClose))
{
string lowerCased = pathWithUpperCase.ToLowerInvariant();
return !File.Exists(lowerCased);
}
}
catch
get
{
// In case something goes wrong (e.g. temp pointing to a privilieged directory), we don't
// want to fail just because of a casing test, so we assume case-insensitive-but-preserving.
return false;
#if MS_IO_REDIST
return false; // Windows is always case-insensitive
#else
return !(OperatingSystem.IsWindows() || OperatingSystem.IsMacOS() || OperatingSystem.IsMacCatalyst() || OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() || OperatingSystem.IsWatchOS());
#endif
}
}
}
Expand Down
19 changes: 19 additions & 0 deletions src/libraries/System.IO.FileSystem/tests/FileSystemTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,5 +127,24 @@ protected void ReadOnly_FileSystemHelper(Action<string> testAction, string subDi
Assert.Equal(0, AdminHelpers.RunAsSudo($"umount {readOnlyDirectory}"));
}
}

/// <summary>
/// Determines whether the file system is case sensitive by creating a file in the specified folder and observing the result.
/// </summary>
/// <remarks>
/// Ideally we'd use something like pathconf with _PC_CASE_SENSITIVE, but that is non-portable,
/// not supported on Windows or Linux, etc. For now, this function creates a tmp file with capital letters
/// and then tests for its existence with lower-case letters. This could return invalid results in corner
/// cases where, for example, different file systems are mounted with differing sensitivities.
/// </remarks>
protected static bool GetIsCaseSensitiveByProbing(string probingDirectory)
{
string pathWithUpperCase = Path.Combine(probingDirectory, "CASESENSITIVETEST" + Guid.NewGuid().ToString("N"));
using (new FileStream(pathWithUpperCase, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, 0x1000, FileOptions.DeleteOnClose))
{
string lowerCased = pathWithUpperCase.ToLowerInvariant();
return !File.Exists(lowerCased);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
<Compile Include="$(CommonTestPath)System\IO\TempFile.cs" Link="Common\System\IO\TempFile.cs" />
<Compile Include="$(CommonTestPath)System\IO\PathFeatures.cs" Link="Common\System\IO\PathFeatures.cs" />
<Content Include="..\DirectoryInfo\test-dir\dummy.txt" Link="test-dir\dummy.txt" />
<Compile Include="$(CommonPath)System\IO\PathInternal.CaseSensitivity.cs"
Link="Common\System\IO\PathInternal.CaseSensitivity.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(CommonTestPath)StreamConformanceTests\StreamConformanceTests.csproj" />
Expand Down
18 changes: 18 additions & 0 deletions src/libraries/System.IO.FileSystem/tests/PathInternalTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Xunit;

namespace System.IO.Tests
{
public class PathInternalTests : FileSystemTest
{
[Fact]
[OuterLoop]
public void PathInternalIsCaseSensitiveMatchesProbing()
{
string probingDirectory = TestDirectory;
Assert.Equal(GetIsCaseSensitiveByProbing(probingDirectory), PathInternal.IsCaseSensitive);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
<Compile Include="Enumeration\ExampleTests.cs" />
<Compile Include="Enumeration\RemovedDirectoryTests.cs" />
<Compile Include="Enumeration\SymbolicLinksTests.cs" />
<Compile Include="PathInternalTests.cs" />
<Compile Include="RandomAccess\Base.cs" />
<Compile Include="RandomAccess\GetLength.cs" />
<Compile Include="RandomAccess\Read.cs" />
Expand Down Expand Up @@ -191,6 +192,8 @@
<Compile Include="$(CommonTestPath)System\IO\TempFile.cs" Link="Common\System\IO\TempFile.cs" />
<Compile Include="$(CommonTestPath)System\IO\PathFeatures.cs" Link="Common\System\IO\PathFeatures.cs" />
<Content Include="DirectoryInfo\test-dir\dummy.txt" Link="test-dir\dummy.txt" />
<Compile Include="$(CommonPath)System\IO\PathInternal.CaseSensitivity.cs"
Link="Common\System\IO\PathInternal.CaseSensitivity.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(CommonTestPath)StreamConformanceTests\StreamConformanceTests.csproj" />
Expand Down
12 changes: 0 additions & 12 deletions src/libraries/System.Private.CoreLib/src/System/IO/Path.Unix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,17 +129,5 @@ public static ReadOnlySpan<char> GetPathRoot(ReadOnlySpan<char> path)
return IsPathRooted(path) ? PathInternal.DirectorySeparatorCharAsString.AsSpan() : ReadOnlySpan<char>.Empty;
}

/// <summary>Gets whether the system is case-sensitive.</summary>
internal static bool IsCaseSensitive
{
get
{
#if TARGET_OSX || TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS
return false;
#else
return true;
#endif
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,6 @@ public static ReadOnlySpan<char> GetPathRoot(ReadOnlySpan<char> path)
return pathRoot <= 0 ? ReadOnlySpan<char>.Empty : path.Slice(0, pathRoot);
}

/// <summary>Gets whether the system is case-sensitive.</summary>
internal static bool IsCaseSensitive => false;

/// <summary>
/// Returns the volume name for dos, UNC and device paths.
/// </summary>
Expand Down
8 changes: 1 addition & 7 deletions src/libraries/System.Private.CoreLib/src/System/IO/Path.cs
Original file line number Diff line number Diff line change
Expand Up @@ -860,7 +860,7 @@ private static unsafe void Populate83FileNameFromRandomBytes(byte* bytes, int by
/// <exception cref="ArgumentNullException">Thrown if <paramref name="relativeTo"/> or <paramref name="path"/> is <c>null</c> or an empty string.</exception>
public static string GetRelativePath(string relativeTo, string path)
{
return GetRelativePath(relativeTo, path, StringComparison);
return GetRelativePath(relativeTo, path, PathInternal.StringComparison);
}

private static string GetRelativePath(string relativeTo, string path, StringComparison comparisonType)
Expand Down Expand Up @@ -957,12 +957,6 @@ private static string GetRelativePath(string relativeTo, string path, StringComp
return sb.ToString();
}

/// <summary>Returns a comparison that can be used to compare file and directory names for equality.</summary>
internal static StringComparison StringComparison =>
IsCaseSensitive ?
StringComparison.Ordinal :
StringComparison.OrdinalIgnoreCase;

/// <summary>
/// Trims one trailing directory separator beyond the root of the path.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -781,7 +781,7 @@ private static void OnAssemblyLoad(RuntimeAssembly assembly)
string assemblyPath = Path.Combine(parentDirectory, assemblyName.CultureName!, $"{assemblyName.Name}.dll");

bool exists = System.IO.FileSystem.FileExists(assemblyPath);
if (!exists && Path.IsCaseSensitive)
if (!exists && PathInternal.IsCaseSensitive)
{
#if CORECLR
if (AssemblyLoadContext.IsTracingEnabled())
Expand Down

0 comments on commit c0ed319

Please sign in to comment.