-
Notifications
You must be signed in to change notification settings - Fork 5.4k
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:
newPathwill beT:\foo(Linux:/foo)trimmedNewPathwill beT:\foo(/foo)trimmedCurrentPathwill beT:\(/) <-- the outcome of this step is the key thing I think the above code fails to anticipate.- The
trimmedNewPath.StartsWith()condition is fulfilled, but: trimmedCurrentPath.Length== 3, andnewPath[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 thePathInternal.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.