Skip to content

Fix DirectoryInfo.CreateSubdirectory failing on root directories#125708

Closed
Copilot wants to merge 2 commits intomainfrom
copilot/fix-create-subdirectory-root-dir
Closed

Fix DirectoryInfo.CreateSubdirectory failing on root directories#125708
Copilot wants to merge 2 commits intomainfrom
copilot/fix-create-subdirectory-root-dir

Conversation

Copy link
Contributor

Copilot AI commented Mar 18, 2026

Description

DirectoryInfo.CreateSubdirectory("foo") threw ArgumentException when DirectoryInfo referred to a root directory (C:\, /).

Root cause: Path.TrimEndingDirectorySeparator intentionally preserves root path separators (since \ in C:\ and / in / are part of the path identity, not just trailing decoration). This left trimmedCurrentPath = "C:\", so the boundary check IsDirectorySeparator(newPath[3]) evaluated 'f' instead of '\', causing the false rejection.

Fix: Use FullPath.TrimEnd(Path.DirectorySeparatorChar) instead, which strips the trailing separator unconditionally:

  • C:\C:newPath[2] = '\'
  • /""newPath[0] = '/' ✓ (all absolute Unix paths start with /, so vacuous StartsWith("") is semantically correct here)

Test: Extended the existing Unix root test from [PlatformSpecific(TestPlatforms.Linux)] to [PlatformSpecific(TestPlatforms.AnyUnix)] for macOS coverage, consistent with other Unix-specific tests in the file. The Windows root test (CreateSubdirectory_RootDriveSubfolder_Windows) was already in place.

// Previously threw ArgumentException; now works correctly
DirectoryInfo directory = new DirectoryInfo("T:\\");
directory.CreateSubdirectory("foo"); // Creates T:\foo

// On Linux, correctly reaches the filesystem (throws UnauthorizedAccessException as non-root)
new DirectoryInfo("/").CreateSubdirectory("foo");

Changes

  • src/libraries/System.Private.CoreLib/src/System/IO/DirectoryInfo.cs — replace Path.TrimEndingDirectorySeparator(FullPath.AsSpan()) with FullPath.TrimEnd(Path.DirectorySeparatorChar) for trimmedCurrentPath
  • src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/DirectoryInfo/CreateSubdirectory.cs — broaden Unix root test from Linux to AnyUnix

Testing

All 77 CreateSubdirectory tests pass, including the updated Unix root test.

Original prompt

This section details on the original issue you should resolve

<issue_title>System.IO.DirectoryInfo CreateSubdirectory method fails when DirectoryInfo refers to a root directory</issue_title>
<issue_description>### Description
The below code fails to create a subdirectory and throws an exception.

using System;
using System.IO;

namespace CreateSubdirectoryTest
{
    class Program
    {
        static void Main(string[] args)
        {
            DirectoryInfo directory = new DirectoryInfo("T:\\"); // Or any other root directory, including / on Linux.
            directory.CreateSubdirectory("foo");
        }
    }
}

Expected:
T:\foo is created (or /foo, if doing the equivalent on Linux).

Actual:

System.ArgumentException: The directory specified, 'foo', is not a subdirectory of 'T:'. (Parameter 'path')

Configuration

.NET Version: 5.0.3
OS: Windows 10 or Linux

Other information

The exception message is a very odd one for the given circumstance, which led me to explore
runtime/src/libraries/System.IO.FileSystem/src/System/IO/DirectoryInfo.cs,
where I found the following definition of CreateSubdirectory():

public DirectoryInfo CreateSubdirectory(string path)
{
    if (path == null)
        throw new ArgumentNullException(nameof(path));
    if (PathInternal.IsEffectivelyEmpty(path.AsSpan()))
        throw new ArgumentException(SR.Argument_PathEmpty, nameof(path));
    if (Path.IsPathRooted(path))
        throw new ArgumentException(SR.Arg_Path2IsRooted, nameof(path));

    string newPath = Path.GetFullPath(Path.Combine(FullPath, path));

    ReadOnlySpan<char> trimmedNewPath = Path.TrimEndingDirectorySeparator(newPath.AsSpan());
    ReadOnlySpan<char> trimmedCurrentPath = Path.TrimEndingDirectorySeparator(FullPath.AsSpan());

    // We want to make sure the requested directory is actually under the subdirectory.
    if (trimmedNewPath.StartsWith(trimmedCurrentPath, PathInternal.StringComparison)
        // Allow the exact same path, but prevent allowing "..\FooBar" through when the directory is "Foo"
        && ((trimmedNewPath.Length == trimmedCurrentPath.Length) || PathInternal.IsDirectorySeparator(newPath[trimmedCurrentPath.Length])))
    {
        FileSystem.CreateDirectory(newPath);
        return new DirectoryInfo(newPath);
    }

    // We weren't nested
    throw new ArgumentException(SR.Format(SR.Argument_InvalidSubPath, path, FullPath), nameof(path));
}

I believe the above is guaranteed to fail for any path (e.g., foo) if FullPath is something like T:\ or / because:

  1. newPath will be T:\foo (Linux: /foo)
  2. trimmedNewPath will be T:\foo (/foo)
  3. trimmedCurrentPath will be T:\ (/) <-- the outcome of this step is the key thing I think the above code fails to anticipate.
  4. The trimmedNewPath.StartsWith() condition is fulfilled, but:
  5. trimmedCurrentPath.Length == 3, and newPath[3] == 'f' (or, for the Linux root path, the length is 1, which results in the same: the tested character of newPath being 'f'), so the PathInternal.IsDirectorySeparator() test against that character fails, allowing execution to pass to throwing an exception.

The issue is that for a directory such as T:\ or /, Path.TrimEndingDirectorySeparator() does not remove the trailing separator, because it is not just a separator; it is actually the path.

Suggested Solution

It looks to me like what the above code is trying to accomplish is to verify that the full starting path is either identical to or a substring of the finished new full path and make sure that the new part is tacked on on the other side of a directory separator from the old part.

Rather than trimming the directory separator from the original full path, then testing for the new one to start with the original followed by a directory separator, perhaps it would be more accurate to add a directory separator to the original if missing, and then simply test that the new full path starts with [original full path including separator]. That would also have the benefit of making the purpose of the if condition easier to understand by making its expression much simpler.</issue_description>

Comments on the Issue (you are @copilot in this section)

@danmoseley @SuperJMN would you like to pick up the draft PR and complete it? @danmoseley @SuperJMN, I see there is a PR #49414 that @trajanmcgill was not able to complete. Not sure of how far they got.

📍 Connect Copilot coding agent with Jira, Azure Boards or Linear to delegate work to Copilot in one click without leaving your project management tool.

Co-authored-by: danmoseley <6385855+danmoseley@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix CreateSubdirectory method for root directory Fix DirectoryInfo.CreateSubdirectory failing on root directories Mar 18, 2026
Copilot AI requested a review from danmoseley March 18, 2026 04:19
@jozkee
Copy link
Member

jozkee commented Mar 18, 2026

Duplicate of #121906.

@jozkee jozkee closed this Mar 18, 2026
@jozkee jozkee deleted the copilot/fix-create-subdirectory-root-dir branch March 18, 2026 04:27
@danmoseley
Copy link
Member

@jozkee thanks so why is issue still open?

@danmoseley
Copy link
Member

Oh you closed. Thanks

@jozkee
Copy link
Member

jozkee commented Mar 18, 2026

Yeah, the issue was a lingering duplicate of #116087. It would've been nice if Copilot have discovered that it was a dupe and have closed/reported back instead of sending the PR.

@danmoseley
Copy link
Member

Is its attribute edit in the pr correct?

@jozkee
Copy link
Member

jozkee commented Mar 18, 2026

It is though, I closed it because I thought it wasn't worth the pipeline run, but it has value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

System.IO.DirectoryInfo CreateSubdirectory method fails when DirectoryInfo refers to a root directory

4 participants