-
Notifications
You must be signed in to change notification settings - Fork 1
feat(#7): XSD validation from game directory #50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.IO; | ||
| using Xunit; | ||
| using BannerlordModEditor.Common.Services; | ||
|
|
||
| namespace BannerlordModEditor.Common.Tests.Services | ||
| { | ||
| public class XsdSchemaProviderTests | ||
| { | ||
| private readonly XsdSchemaProvider _provider; | ||
|
|
||
| public XsdSchemaProviderTests() | ||
| { | ||
| _provider = new XsdSchemaProvider(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void GetXsdPathForXml_ReturnsNullForNonExistentGameDirectory() | ||
| { | ||
| var result = _provider.GetXsdPathForXml("test.xml", "/nonexistent/path"); | ||
| Assert.Null(result); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void GetXsdPathForXml_ReturnsNullForEmptyPaths() | ||
| { | ||
| Assert.Null(_provider.GetXsdPathForXml("", "")); | ||
| Assert.Null(_provider.GetXsdPathForXml(null!, null!)); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void GetXsdPathForXml_FindsXsdInXmlSchemasDirectory() | ||
| { | ||
| var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); | ||
| var xsdDir = Path.Combine(tempDir, "XmlSchemas"); | ||
| Directory.CreateDirectory(xsdDir); | ||
|
|
||
| try | ||
| { | ||
| var xsdPath = Path.Combine(xsdDir, "attributes.xsd"); | ||
| File.WriteAllText(xsdPath, "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\"></xs:schema>"); | ||
|
|
||
| var result = _provider.GetXsdPathForXml("attributes.xml", tempDir); | ||
| Assert.Equal(xsdPath, result); | ||
| } | ||
| finally | ||
| { | ||
| Directory.Delete(tempDir, true); | ||
| } | ||
| } | ||
|
|
||
| [Fact] | ||
| public void GetAllXsdFiles_ReturnsEmptyForNonExistentDirectory() | ||
| { | ||
| var result = _provider.GetAllXsdFiles("/nonexistent/path"); | ||
| Assert.Empty(result); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void GetAllXsdFiles_FindsAllXsdFiles() | ||
| { | ||
| var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); | ||
| var xsdDir = Path.Combine(tempDir, "XmlSchemas"); | ||
| Directory.CreateDirectory(xsdDir); | ||
|
|
||
| try | ||
| { | ||
| File.WriteAllText(Path.Combine(xsdDir, "file1.xsd"), ""); | ||
| File.WriteAllText(Path.Combine(xsdDir, "file2.xsd"), ""); | ||
|
|
||
| var result = _provider.GetAllXsdFiles(tempDir); | ||
| Assert.Equal(2, result.Count); | ||
| } | ||
| finally | ||
| { | ||
| Directory.Delete(tempDir, true); | ||
| } | ||
| } | ||
|
|
||
| [Fact] | ||
| public void LoadSchemaSet_ReturnsEmptyForNonExistentFile() | ||
| { | ||
| var result = _provider.LoadSchemaSet("/nonexistent/schema.xsd"); | ||
| Assert.NotNull(result); | ||
| Assert.Equal(0, result.Count); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void LoadSchemaSet_LoadsValidXsd() | ||
| { | ||
| var tempFile = Path.GetTempFileName(); | ||
| var xsd = @"<?xml version=""1.0"" encoding=""utf-8""?> | ||
| <xs:schema xmlns:xs=""http://www.w3.org/2001/XMLSchema""> | ||
| <xs:element name=""Test""> | ||
| <xs:complexType> | ||
| <xs:sequence> | ||
| <xs:element name=""Name"" type=""xs:string""/> | ||
| </xs:sequence> | ||
| </xs:complexType> | ||
| </xs:element> | ||
| </xs:schema>"; | ||
|
|
||
| try | ||
| { | ||
| File.WriteAllText(tempFile, xsd); | ||
|
|
||
| var result = _provider.LoadSchemaSet(tempFile); | ||
| Assert.NotNull(result); | ||
| Assert.Equal(1, result.Count); | ||
| } | ||
| finally | ||
| { | ||
| File.Delete(tempFile); | ||
| } | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ValidateXmlAgainstXsd_ReturnsFalseForNonExistentXml() | ||
| { | ||
| var result = _provider.ValidateXmlAgainstXsd("/nonexistent.xml", "/schema.xsd", out var errors); | ||
| Assert.False(result); | ||
| Assert.Contains("XML file not found", errors[0]); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ValidateXmlAgainstXsd_ReturnsFalseForNonExistentXsd() | ||
| { | ||
| var tempXml = Path.GetTempFileName(); | ||
| File.WriteAllText(tempXml, "<Test></Test>"); | ||
|
|
||
| try | ||
| { | ||
| var result = _provider.ValidateXmlAgainstXsd(tempXml, "/nonexistent.xsd", out var errors); | ||
| Assert.False(result); | ||
| Assert.Contains("XSD schema not found", errors[0]); | ||
| } | ||
| finally | ||
| { | ||
| File.Delete(tempXml); | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,148 @@ | ||||||
| using System; | ||||||
| using System.Collections.Generic; | ||||||
| using System.IO; | ||||||
| using System.Linq; | ||||||
| using System.Xml; | ||||||
| using System.Xml.Schema; | ||||||
|
|
||||||
| namespace BannerlordModEditor.Common.Services | ||||||
| { | ||||||
| public interface IXsdSchemaProvider | ||||||
| { | ||||||
| string? GetXsdPathForXml(string xmlFilePath, string gameDirectory); | ||||||
| List<string> GetAllXsdFiles(string gameDirectory); | ||||||
| XmlSchemaSet LoadSchemaSet(string xsdPath); | ||||||
| bool ValidateXmlAgainstXsd(string xmlFilePath, string xsdPath, out List<string> errors); | ||||||
| } | ||||||
|
|
||||||
| public class XsdSchemaProvider : IXsdSchemaProvider | ||||||
| { | ||||||
| private const string XsdDirectoryRelativePath = "XmlSchemas"; | ||||||
| private const string ModuleDataRelativePath = "Modules"; | ||||||
| private const string NativeModuleRelativePath = "Modules\\Native\\ModuleData"; | ||||||
|
||||||
| private const string NativeModuleRelativePath = "Modules\\Native\\ModuleData"; | |
| private static readonly string NativeModuleRelativePath = Path.Combine("Modules", "Native", "ModuleData"); |
Copilot
AI
Mar 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NativeModuleRelativePath uses Windows backslashes. When combined on non-Windows platforms, the embedded \ becomes part of the directory name, causing schema discovery/scanning to fail. Define the path using Path.Combine("Modules", "Native", "ModuleData") (or store segments and combine) so it’s platform-correct.
Copilot
AI
Mar 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NativeModuleRelativePath uses Windows backslashes. When combined on non-Windows platforms, the embedded \ becomes part of the directory name, causing schema discovery/scanning to fail. Define the path using Path.Combine("Modules", "Native", "ModuleData") (or store segments and combine) so it’s platform-correct.
Copilot
AI
Mar 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Swallowing all exceptions here can silently hide permission issues, path-too-long errors, or IO failures and make schema discovery non-deterministic in production. Catch specific exceptions (e.g., UnauthorizedAccessException, IOException) and either (a) surface them via an error list/logging, or (b) at least document why ignoring is safe and expected.
Copilot
AI
Mar 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
XmlSchemaSet can resolve <xs:include>/<xs:import> via an XmlResolver (default may allow external fetches / local file access depending on runtime), which is risky if schemas are not fully trusted. Consider setting schemaSet.XmlResolver = null (or a constrained resolver) and also ensure the XmlReaderSettings.XmlResolver is null during validation to prevent external entity/resource resolution.
Copilot
AI
Mar 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests currently cover missing XML/XSD paths, but don’t verify the core behavior of validating a real XML against a real XSD (both a passing case returning true and a failing case that returns false with populated validation errors). Adding those tests would cover the main feature this method introduces.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ModuleDataRelativePathis declared but not used anywhere in this file. Removing it avoids dead code and reduces confusion about intended search paths.