Question

Does .net have a way to determine whether the local filesystem is case-sensitive?

Was it helpful?

Solution

You can create a file in the temp folder (using lowercase filename), then check if the file exists (using uppercase filename), e.g:

string file = Path.GetTempPath() + Guid.NewGuid().ToString().ToLower();
File.CreateText(file).Close();
bool isCaseInsensitive = File.Exists(file.ToUpper());
File.Delete(file);

OTHER TIPS

There is no such a function in the .NET Class Library.

You can, however, roll out your own: Try creating a file with a lowercase name and then try to open it with the upparcase version of its name. Probably it is possible to improve this method, but you get the idea.

EDIT: You could actually just take the first file in the root directory and then check if both filename.ToLower() and filename.ToUpper() exist. Unfortunately it is quite possible that both uppercase and lowercase variants of the same file exist, so you should compare the FileInfo.Name properties of both the lowercase and uppercase variants to see if they are indeed the same or not. This will not require writing to the disk.

Obviously, this will fail if there are no files at all on the volume. In this case, just fall back to the first option (see Martin's answer for the implementation).

Keep in mind that you might have multiple file systems with different casing rules. For example, the root filesystem could be case-sensitive, but you can have a case-insensitive filesystem (e.g. an USB stick with a FAT filesystem on it) mounted somewhere. So if you do such checks, make sure that you make them in the directory that you are going to access.

Also, what if the user copies the data from say a case-sensitive to a case-insensitive file system? If you have files that differ only by case, one of them will overwrite the other, causing data loss. When copying in the other direction, you might also run into problems, for example, if file A contains a reference to file "b", but the file is actually named "B". This works on the original case-insensitive file system, but not on the case-sensitive system.

Thus I would suggest that you avoid depending on whether the file system is case-sensitive or not if you can. Do not generate file names that differ only by case, use the standard file picker dialogs, be prepared that the case might change, etc.

Try creating a temporary file in all lowercase, and then check if it exists using uppercase.

It's not a .NET function, but the GetVolumeInformation and GetVolumeInformationByHandleW functions from the Windows API will do what you want (see yje lpFileSystemFlags parameter.

There are actually two ways to interpret the original question.

  1. How to determine whether a specific file system is able to preserve case-sensitivity in file names?
  2. How to determine whether the current operating system interprets file names case-sensitively when working with a specific file system.

This answer is based on the second interpretation, because I think that is what the OP wanted to know and also what matters to most people.

The following code is loosely based on M4N's and Nicolas Raoul's answer and attempts to create a really robust implementation that is able to determine whether the operating system handles file names case-sensitive inside of a specified directory (excluding sub-directories, since these could be mounted from another file system).

It works by creating two new files in succession, one with lower-case, the other one with upper-case characters. The files are locked exclusively and are deleted automatically when closed. This should avert any negative side effects caused by creating files. Of course, this implementation only works if the specified directory exists and the current user is able to create files inside of it.

The code is written for .NET Framework 4.0 and C# 7.2 (or later).

using System;
using System.IO;
using System.Reflection;

/// <summary>
/// Check whether the operating system handles file names case-sensitive in the specified directory.
/// </summary>
/// <param name="directoryPath">The path to the directory to check.</param>
/// <returns>A value indicating whether the operating system handles file names case-sensitive in the specified directory.</returns>
/// <exception cref="ArgumentNullException"><paramref name="directoryPath"/> is null.</exception>
/// <exception cref="ArgumentException"><paramref name="directoryPath"/> contains one or more invalid characters.</exception>
/// <exception cref="DirectoryNotFoundException">The specified directory does not exist.</exception>
/// <exception cref="UnauthorizedAccessException">The current user has no write permission to the specified directory.</exception>
private static bool IsFileSystemCaseSensitive(string directoryPath)
{
    if (directoryPath == null)
    {
        throw new ArgumentNullException(nameof(directoryPath));
    }

    while (true)
    {
        string fileNameLower = ".cstest." + Guid.NewGuid().ToString();
        string fileNameUpper = fileNameLower.ToUpperInvariant();

        string filePathLower = Path.Combine(directoryPath, fileNameLower);
        string filePathUpper = Path.Combine(directoryPath, fileNameUpper);

        FileStream fileStreamLower = null;
        FileStream fileStreamUpper = null;
        try
        {
            try
            {
                // Try to create filePathUpper to ensure a unique non-existing file.
                fileStreamUpper = new FileStream(filePathUpper, FileMode.CreateNew, FileAccess.Write, FileShare.None, bufferSize: 4096, FileOptions.DeleteOnClose);

                // After ensuring that it didn't exist before, filePathUpper must be closed/deleted again to ensure correct opening of filePathLower, regardless of the case-sensitivity of the file system.
                // On case-sensitive file systems there is a tiny chance for a race condition, where another process could create filePathUpper between closing/deleting it here and newly creating it after filePathLower.
                // This method would then incorrectly indicate a case-insensitive file system.
                fileStreamUpper.Dispose();
            }
            catch (IOException ioException) when (IsErrorFileExists(ioException))
            {
                // filePathUpper already exists, try another file name
                continue;
            }

            try
            {
                fileStreamLower = new FileStream(filePathLower, FileMode.CreateNew, FileAccess.Write, FileShare.None, bufferSize: 4096, FileOptions.DeleteOnClose);
            }
            catch (IOException ioException) when (IsErrorFileExists(ioException))
            {
                // filePathLower already exists, try another file name
                continue;
            }

            try
            {
                fileStreamUpper = new FileStream(filePathUpper, FileMode.CreateNew, FileAccess.Write, FileShare.None, bufferSize: 4096, FileOptions.DeleteOnClose);

                // filePathUpper does not exist, this indicates case-sensitivity
                return true;
            }
            catch (IOException ioException) when (IsErrorFileExists(ioException))
            {
                // fileNameUpper already exists, this indicates case-insensitivity
                return false;
            }
        }
        finally
        {
            fileStreamLower?.Dispose();
            fileStreamUpper?.Dispose();
        }
    }
}

/// <summary>
/// Determines whether the specified <see cref="IOException"/> indicates a "file exists" error.
/// </summary>
/// <param name="ioException">The <see cref="IOException"/> to check.</param>
/// <returns>A value indicating whether the specified <see cref="IOException"/> indicates a "file exists" error.</returns>
private static bool IsErrorFileExists(IOException ioException)
{
    // https://referencesource.microsoft.com/mscorlib/microsoft/win32/win32native.cs.html#dd35d7f626262141
    const int ERROR_FILE_EXISTS = 0x50;

    // The Exception.HResult property's get accessor is protected before .NET 4.5, need to get its value via reflection.
    int hresult = (int)typeof(Exception)
        .GetProperty("HResult", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
        .GetValue(ioException, index: null);

    // https://referencesource.microsoft.com/mscorlib/microsoft/win32/win32native.cs.html#9f6ca3226ff8f9ba
    return hresult == unchecked((int)0x80070000 | ERROR_FILE_EXISTS);
}

As you can see there is a tiny possibility for a race condition which can cause a false-negative. If this race condition is something you really worry about I suggest you do the check a second time when the result is false, either inside the IsFileSystemCaseSensitive method or outside of it. However, in my opinion, the probability of encountering this race condition once, let alone two times in a row, is astronomically small.

I invoke The Cheat:

Path.DirectorySeparatorChar == '\\' ? "I'm insensitive" : "I'm probably sensitive"
/// <summary>
/// Check whether the operating system is case-sensitive.
/// For instance on Linux you can have two files/folders called
//// "test" and "TEST", but on Windows the two can not coexist.
/// This method does not extend to mounted filesystems, which might have different properties.
/// </summary>
/// <returns>true if the operating system is case-sensitive</returns>
public static bool IsFileSystemCaseSensitive()
{
    // Actually try.
    string file = Path.GetTempPath() + Guid.NewGuid().ToString().ToLower() + "test";
    File.CreateText(file).Close();
    bool result = ! File.Exists(file.ToUpper());
    File.Delete(file);

    return result;
}

Based on M4N's answer, with the following changes:

  • Static names so that we are sure it contains a letter and not only numbers.
  • Maybe more readable?
  • Wrapped in a method.
  • Documentation.

A better strategy would be to take a path as an argument, and create the file on the same filesystem, but writing there might have unexpected consequences.

How about this heuristic?

public static bool IsCaseSensitiveFileSystem() {
   var tmp = Path.GetTempPath();
   return !Directory.Exists(tmp.ToUpper()) || !Directory.Exists(tmp.ToLower());
}

Here is the approach that does not use temporary files:

using System;
using System.Runtime.InteropServices;

static bool IsCaseSensitive()
{
    if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ||
        RuntimeInformation.IsOSPlatform(OSPlatform.OSX))  // HFS+ (the Mac file-system) is usually configured to be case insensitive.
    {
        return false;
    }
    else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
    {
        return true;
    }
    else if (Environment.OSVersion.Platform == PlatformID.Unix)
    {
        return true;
    }
    else
    {
       // A default.
       return false;
    }
}

Instead, it contains an ingrained knowledge about operating environments.

Readily available as NuGet package, works on everything .NET 4.0+ and updated on a regular basis: https://github.com/gapotchenko/Gapotchenko.FX/tree/master/Source/Gapotchenko.FX.IO#iscasesensitive

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top