Skip to content

Fix InMemoryDirectoryInfo path resolution for '..' and relative paths#126320

Draft
svick wants to merge 2 commits intodotnet:mainfrom
svick:inmemory-go-up
Draft

Fix InMemoryDirectoryInfo path resolution for '..' and relative paths#126320
svick wants to merge 2 commits intodotnet:mainfrom
svick:inmemory-go-up

Conversation

@svick
Copy link
Copy Markdown
Member

@svick svick commented Mar 30, 2026

Fix #99691InMemoryDirectoryInfo does not work with the .. pattern.

Problem

InMemoryDirectoryInfo.GetDirectory had two bugs:

  • For .. it combined the path with FullName but passed it as already-normalized, leaving FullName as e.g. /Folder1/.. instead of resolving to the parent directory.
  • For other relative paths it resolved against the process CWD, ignoring FullName entirely.

GetFile had the same issue as the second case — it resolved relative paths against CWD instead of FullName.

Fix

  • GetDirectory("..") now uses Path.GetDirectoryName(FullName) to resolve to the parent. An isParentPath flag preserves Name as ".." (needed by the matcher's pattern traversal), matching DirectoryInfoWrapper's approach.
  • GetDirectory and GetFile for other paths now combine the input with FullName via Path.Combine.

Tests

  • Converted FileAbstractionsTests from [Fact] to [Theory] with bool useInMemory, so each test exercises both DirectoryInfoWrapper and InMemoryDirectoryInfo.
  • Added VerifyInMemoryDirectoryInfo_ParentPatternMatches in FunctionalTests — an end-to-end test that uses Matcher with a ../Folder2/** pattern against InMemoryDirectoryInfo.
  • Enhanced DisposableFileSystem with useInMemory support: when true, no files/directories are created on disk and GetDirectoryInfoBase() returns an InMemoryDirectoryInfo populated from the recorded CreateFile/CreateFiles calls.

Copilot AI review requested due to automatic review settings March 30, 2026 15:50
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-extensions-filesystem
See info in area-owners.md if you want to be subscribed.

GetDirectory had two bugs:
- For '..' it combined with FullName but skipped normalization, leaving
  FullName as e.g. '/Folder1/..' instead of resolving to the parent.
- For other relative paths it resolved against the process CWD,
  ignoring FullName entirely.

GetFile had the same issue as the second case above.

The fix uses Path.GetDirectoryName for '..' and Path.Combine with
FullName for other paths. An isParentPath flag preserves Name as '..'
(needed by the matcher's pattern traversal), matching
DirectoryInfoWrapper's approach.

Also adds test coverage via theories that exercise both DirectoryInfoWrapper
and InMemoryDirectoryInfo through the same test logic, and enhances
DisposableFileSystem to support in-memory mode.

Fixes dotnet#99691

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes path resolution issues in Microsoft.Extensions.FileSystemGlobbing’s InMemoryDirectoryInfo (notably .. traversal and relative path resolution) and extends Microsoft.Extensions.FileProviders.Physical watching behavior to better handle missing roots / mismatched watcher paths, with expanded test coverage across both areas.

Changes:

  • Correct InMemoryDirectoryInfo.GetDirectory/GetFile to resolve relative paths against the directory’s FullName, and add coverage for ../ pattern traversal.
  • Enhance physical file watching to handle missing root directories and broader watcher path configurations, plus update polling tokens to observe directory changes.
  • Expand and refactor tests/utilities to exercise both disk-backed and in-memory abstractions.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/libraries/Microsoft.Extensions.FileSystemGlobbing/src/InMemoryDirectoryInfo.cs Updates path resolution for .. and relative paths in the in-memory directory abstraction.
src/libraries/Microsoft.Extensions.FileSystemGlobbing/src/Abstractions/DirectoryInfoWrapper.cs Refreshes wrapped DirectoryInfo before enumeration to avoid stale Exists state.
src/libraries/Microsoft.Extensions.FileSystemGlobbing/tests/TestUtility/DisposableFileSystem.cs Adds useInMemory mode and a helper to return DirectoryInfoBase for both implementations.
src/libraries/Microsoft.Extensions.FileSystemGlobbing/tests/FileAbstractionsTests.cs Converts key tests to run against both DirectoryInfoWrapper and InMemoryDirectoryInfo.
src/libraries/Microsoft.Extensions.FileSystemGlobbing/tests/FunctionalTests.cs Adds an end-to-end matcher test for ../Folder2/** against InMemoryDirectoryInfo.
src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs Adds missing-root handling via a pending creation watcher and improves token invalidation / root filtering.
src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PollingFileChangeToken.cs Extends polling to consider directory changes when the file path doesn’t exist.
src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFileProvider.cs Allows roots that don’t exist and adjusts watcher creation to support deferred enabling.
src/libraries/Microsoft.Extensions.FileProviders.Physical/src/Internal/PathUtils.cs Centralizes path separator handling and improves trailing-slash behavior.
src/libraries/Microsoft.Extensions.FileProviders.Physical/src/Resources/Strings.resx Adds new resource strings for watcher/root validation errors.
src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFilesWatcherTests.cs Adds new watcher-mode matrix tests and missing-directory scenarios.
src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFileProviderTests.cs Refactors a token test and keeps provider/watch behavior covered.
src/libraries/Microsoft.Extensions.Configuration.FileExtensions/src/FileConfigurationSource.cs Changes file provider resolution to use the file’s directory even if missing.
src/libraries/Microsoft.Extensions.Configuration.FileExtensions/tests/FileConfigurationProviderTest.cs Adds regression coverage for watching a config file whose parent directory is missing.

Comment on lines +147 to +149
string? combinedPath = isParentPath ? Path.GetDirectoryName(FullName) : Path.Combine(FullName, path);
string? normPath = combinedPath?.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
return normPath == null ? null : new InMemoryDirectoryInfo(normPath, _files, true, _comparisonType, isParentPath);
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InMemoryDirectoryInfo.GetDirectory currently uses Path.GetDirectoryName(FullName) for "..". For filesystem roots (e.g. "/" or "C:\"), GetDirectoryName returns null, so GetDirectory("..") returns null (and differs from DirectoryInfoWrapper, where ".." from root resolves back to the root). Consider falling back to FullName (or using Path.GetFullPath(Path.Combine(FullName, ".."))) so traversing above root doesn’t unexpectedly stop.

Suggested change
string? combinedPath = isParentPath ? Path.GetDirectoryName(FullName) : Path.Combine(FullName, path);
string? normPath = combinedPath?.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
return normPath == null ? null : new InMemoryDirectoryInfo(normPath, _files, true, _comparisonType, isParentPath);
string? combinedPath = isParentPath
? Path.GetDirectoryName(FullName) ?? FullName
: Path.Combine(FullName, path);
string? normPath = combinedPath?.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
return normPath is null ? null : new InMemoryDirectoryInfo(normPath, _files, true, _comparisonType, isParentPath);

Copilot uses AI. Check for mistakes.
Comment on lines +146 to +149
bool isParentPath = string.Equals(path, "..", StringComparison.Ordinal);
string? combinedPath = isParentPath ? Path.GetDirectoryName(FullName) : Path.Combine(FullName, path);
string? normPath = combinedPath?.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
return normPath == null ? null : new InMemoryDirectoryInfo(normPath, _files, true, _comparisonType, isParentPath);
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetDirectory combines FullName with the provided path but doesn’t normalize the result (e.g., via Path.GetFullPath). If the input contains "."/".." segments beyond the special-case "..", the resulting FullName can be non-canonical and won’t match the normalized paths stored in _files, breaking enumeration/matching. Consider normalizing the combined path before constructing the child InMemoryDirectoryInfo.

Copilot uses AI. Check for mistakes.
{
string normPath = Path.GetFullPath(path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar));
string combinedPath = Path.Combine(FullName, path);
string normPath = combinedPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetFile builds a combined path via Path.Combine + separator replace but doesn’t normalize it (e.g., Path.GetFullPath). Since _files are stored as fully normalized absolute paths, inputs containing "."/".." segments (or other non-canonical forms) won’t be found. Consider normalizing the combined path before comparing to entries in _files.

Suggested change
string normPath = combinedPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
string normPath = Path.GetFullPath(combinedPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar));

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings March 30, 2026 16:48
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comment on lines 23 to 24
Assert.Equal(Path.GetFileName(scenario.RootPath), scenario.DirectoryInfo.Name);
Assert.Equal(scenario.RootPath, scenario.DirectoryInfo.FullName);
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With #nullable enable in this file and DisposableFileSystem.DirectoryInfo now being nullable, scenario.DirectoryInfo.Name / .FullName can trigger nullable warnings (possible null dereference) and break the build under warnings-as-errors. Use scenario.DirectoryInfo! consistently here (or assert non-null / refactor to avoid using the nullable property).

Suggested change
Assert.Equal(Path.GetFileName(scenario.RootPath), scenario.DirectoryInfo.Name);
Assert.Equal(scenario.RootPath, scenario.DirectoryInfo.FullName);
Assert.Equal(Path.GetFileName(scenario.RootPath), scenario.DirectoryInfo!.Name);
Assert.Equal(scenario.RootPath, scenario.DirectoryInfo!.FullName);

Copilot uses AI. Check for mistakes.
Comment on lines 45 to 52
[Fact]
public void FoldersAreEnumerated()
{
using (var scenario = new DisposableFileSystem()
.CreateFolder("beta"))
{
var contents1 = new DirectoryInfoWrapper(scenario.DirectoryInfo).EnumerateFileSystemInfos();
var contents1 = scenario.GetDirectoryInfoBase().EnumerateFileSystemInfos();
var beta = contents1.OfType<DirectoryInfoBase>().Single();
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description says FileAbstractionsTests were converted so each test exercises both DirectoryInfoWrapper and InMemoryDirectoryInfo, but FoldersAreEnumerated is still a single [Fact] using the default (disk-backed) DisposableFileSystem(). Either convert this test to a [Theory] like the others (and make DisposableFileSystem record empty directories for in-memory), or adjust the PR description accordingly.

Copilot uses AI. Check for mistakes.
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.

Microsoft.Extensions.FileSystemGlobbing.InMemoryDirectoryInfo does not work with the ".." pattern.

2 participants