From 9bc51c3f45f9e03d37a2b995c5dc96f66a276f44 Mon Sep 17 00:00:00 2001 From: Matt Edmondson Date: Thu, 30 Jul 2026 17:38:27 +1000 Subject: [PATCH] [patch] Fix directory detection in SemanticRelativePath.Make SemanticRelativePath.Make appends a trailing separator to its endpoints when they denote directories, so that Uri.MakeRelativeUri anchors inside the directory rather than treating its last segment as a file name. It decided this by reflecting for IsDirectoryPathAttribute. Commit ec51717 removed that attribute from every path record, so the check has silently returned false ever since and Make produced relative paths one level off: from C:\base\folder (dir) to C:\base\other (dir) -> "other" from C:\base\folder (dir) to C:\base\folder\file.txt -> "folder\file.txt" Detect directories via the IDirectoryPath interface instead. The interfaces are what the type system already models, so the check can no longer drift out of sync with the validation attributes. Expected results after the fix: from C:\base\folder (dir) to C:\base\other (dir) -> "..\other" from C:\base\folder (dir) to C:\base\folder\file.txt -> "file.txt" Claude-Session: https://claude.ai/code/session_01FUmVUSjEjKY9fSA62tttJm --- Semantics.Paths/SemanticRelativePath.cs | 11 +++----- Semantics.Test/SemanticPathTests.cs | 36 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/Semantics.Paths/SemanticRelativePath.cs b/Semantics.Paths/SemanticRelativePath.cs index 514bee75..bc3ed82f 100644 --- a/Semantics.Paths/SemanticRelativePath.cs +++ b/Semantics.Paths/SemanticRelativePath.cs @@ -71,15 +71,10 @@ public static TRelativePath Make(TFromPath fr } /// - /// Determines whether the specified path type represents a directory path based on its validation attributes. + /// Determines whether the specified path represents a directory path. /// /// The type of semantic path to check. /// The path instance to check. - /// if the path type has the ; otherwise, . - private static bool IsDirectoryPath(T path) where T : SemanticPath - { - // Check if it's a directory-specific type based on validation attributes - Type type = path.GetType(); - return type.GetCustomAttributes(typeof(IsDirectoryPathAttribute), true).Length > 0; - } + /// if the path implements ; otherwise, . + private static bool IsDirectoryPath(T path) where T : SemanticPath => path is IDirectoryPath; } diff --git a/Semantics.Test/SemanticPathTests.cs b/Semantics.Test/SemanticPathTests.cs index 64b756bb..4cbe4769 100644 --- a/Semantics.Test/SemanticPathTests.cs +++ b/Semantics.Test/SemanticPathTests.cs @@ -265,6 +265,42 @@ public void SemanticRelativePath_Make_ShouldCreateCorrectRelativePath() Assert.IsTrue(relativePath.IsValid(), "Created relative path should be valid"); } + [TestMethod] + public void SemanticRelativePath_Make_WithDirectoryEndpoints_ShouldTreatThemAsDirectories() + { + // Arrange + string fromValue = OperatingSystem.IsWindows() ? "C:\\base\\folder" : "/base/folder"; + string toValue = OperatingSystem.IsWindows() ? "C:\\base\\other" : "/base/other"; + AbsoluteDirectoryPath from = AbsoluteDirectoryPath.Create(fromValue); + AbsoluteDirectoryPath to = AbsoluteDirectoryPath.Create(toValue); + + // Act + RelativePath relativePath = + RelativePath.Make(from, to); + + // Assert + // "folder" is a directory, so the relative path must step out of it before entering "other". + Assert.AreEqual(Path.Combine("..", "other"), relativePath.WeakString); + } + + [TestMethod] + public void SemanticRelativePath_Make_FromDirectoryToContainedFile_ShouldNotStepOutOfTheDirectory() + { + // Arrange + string fromValue = OperatingSystem.IsWindows() ? "C:\\base\\folder" : "/base/folder"; + string toValue = OperatingSystem.IsWindows() ? "C:\\base\\folder\\file.txt" : "/base/folder/file.txt"; + AbsoluteDirectoryPath from = AbsoluteDirectoryPath.Create(fromValue); + AbsoluteFilePath to = AbsoluteFilePath.Create(toValue); + + // Act + RelativePath relativePath = + RelativePath.Make(from, to); + + // Assert + // The file lives inside "folder", so the relative path is just the file name. + Assert.AreEqual("file.txt", relativePath.WeakString); + } + // Additional comprehensive tests for edge cases and missing coverage [TestMethod] public void SemanticPath_MakeCanonical_WithRootPath_ShouldPreserveTrailingSeparator()