diff --git a/Schema.Test/SchemaDataSourceTests.cs b/Schema.Test/SchemaDataSourceTests.cs
index 62421a3..770098d 100644
--- a/Schema.Test/SchemaDataSourceTests.cs
+++ b/Schema.Test/SchemaDataSourceTests.cs
@@ -107,6 +107,71 @@ public void TestCodeGeneratorOutputPathResolves()
Assert.AreEqual(Path.GetFullPath(Path.Combine(workingDirectory, "generated")), resolved.ToString());
}
+ ///
+ /// A resolved path is normalised, not merely concatenated. A schema beside the data it
+ /// describes is the easy case; one that reaches a sibling directory through .. is the
+ /// case that tells a resolver from a string join, and the resolved value is compared,
+ /// displayed and used as a key, so it has to come back as the directory it names rather than
+ /// as a route to it.
+ ///
+ [TestMethod]
+ public void TestFilePathsResolveThroughTraversalSegments()
+ {
+ Schema schema = CreateAnchoredSchema("../shared/./items.json");
+
+ Assert.IsTrue(schema.GetDataSource("Items".As())!.TryResolveFile(out AbsoluteFilePath resolved));
+ Assert.AreEqual(
+ Path.GetFullPath(Path.Combine(workingDirectory, "../shared/./items.json")),
+ resolved.ToString());
+ Assert.IsFalse(resolved.ToString().Contains("..", StringComparison.Ordinal), resolved.ToString());
+ }
+
+ [TestMethod]
+ public void TestDirectoryPathsResolveThroughTraversalSegments()
+ {
+ Schema schema = CreateAnchoredSchema();
+ SchemaCodeGenerator generator = schema.AddCodeGenerator("CSharp".As())!;
+ generator.OutputPath = "../build/./generated".As();
+
+ Assert.IsTrue(generator.TryResolveOutputPath(out AbsoluteDirectoryPath resolved));
+ Assert.AreEqual(
+ Path.GetFullPath(Path.Combine(workingDirectory, "../build/./generated")),
+ resolved.ToString());
+ Assert.IsFalse(resolved.ToString().Contains("..", StringComparison.Ordinal), resolved.ToString());
+ }
+
+ ///
+ /// The anchor is the directory holding the schema file, and nothing else about the file.
+ ///
+ [TestMethod]
+ public void TestTheAnchorIsTheSchemaFilesDirectory()
+ {
+ Schema schema = new();
+ schema.SetSourceFile(SchemaPath);
+
+ Assert.AreEqual(workingDirectory, schema.SourceDirectory.ToString());
+ Assert.AreEqual("test.schema.json", schema.SourceFileName);
+ }
+
+ ///
+ /// Anchoring a schema must not disturb the path it was handed. Callers pass an instance they
+ /// keep using - the editor records the same one as a recent file straight afterwards - and
+ /// reading AbsoluteFilePath.AbsoluteDirectoryPath to find the anchor silently breaks
+ /// equality and the hash code of the instance it is read from (ktsu.Semantics.Paths 5.4.2),
+ /// while leaving its text alone. That is why the anchor is still taken from the string.
+ ///
+ [TestMethod]
+ public void TestSettingTheSourceFileLeavesTheCallersPathEqualToItself()
+ {
+ AbsoluteFilePath handedIn = SchemaPath;
+ AbsoluteFilePath untouched = SchemaPath;
+
+ new Schema().SetSourceFile(handedIn);
+
+ Assert.AreEqual(untouched, handedIn, "Anchoring the schema changed the path it was given.");
+ Assert.AreEqual(untouched.GetHashCode(), handedIn.GetHashCode(), "Anchoring the schema changed the hash code of the path it was given.");
+ }
+
[TestMethod]
public void TestLoadWithASourcePathAnchorsTheSchema()
{
diff --git a/Schema/Models/Schema.Paths.cs b/Schema/Models/Schema.Paths.cs
index 646dff8..eecf914 100644
--- a/Schema/Models/Schema.Paths.cs
+++ b/Schema/Models/Schema.Paths.cs
@@ -16,6 +16,19 @@ namespace ktsu.Schema.Models;
/// The anchor is supplied by whoever read the file, so the serializer itself stays free of the
/// filesystem. A schema that was never read from a file has no anchor and cannot resolve
/// anything, which every resolution API reports rather than guessing at the working directory.
+///
+/// Resolution goes through AsAbsolute rather than the / combine operator: a schema
+/// may reach a sibling directory through .., and only the former normalises those segments
+/// away. The operator joins, which hands back a route to the file rather than the file.
+///
+/// The anchor is still taken with rather than
+/// AbsoluteFilePath.AbsoluteDirectoryPath, which would read better. Reading that property
+/// mutates the instance it is read from: in ktsu.Semantics.Paths 5.4.2 an
+/// AbsoluteFilePath stops comparing equal to an identical one, and its hash code changes,
+/// once the property has been touched, while its text stays the same. Callers hand the same
+/// instance on afterwards - the editor records it as a recent file - so reading it here corrupted
+/// equality for a value this code does not own. AsAbsolute carries no such hazard, which is
+/// why only the resolution moved.
///
public partial class Schema
{
@@ -40,7 +53,7 @@ public bool TryResolvePath(RelativeFilePath relativePath, out AbsoluteFilePath r
return false;
}
- resolved = Path.GetFullPath(Path.Combine(SourceDirectory, relativePath)).As();
+ resolved = relativePath.AsAbsolute(SourceDirectory);
return true;
}
@@ -59,7 +72,7 @@ public bool TryResolvePath(RelativeDirectoryPath relativePath, out AbsoluteDirec
return false;
}
- resolved = Path.GetFullPath(Path.Combine(SourceDirectory, relativePath)).As();
+ resolved = relativePath.AsAbsolute(SourceDirectory);
return true;
}