|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#if FEATURE_XML_SCHEMA_VALIDATION
using System;
using System.IO;
using System.Xml;
using System.Xml.Schema;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;
#nullable disable
namespace Microsoft.Build.CommandLine
{
/// <summary>
/// This class is used for validating projects against a designated schema.
/// </summary>
internal sealed class ProjectSchemaValidationHandler
{
// Set to true if there was a syntax error in the project file.
private bool _syntaxError;
#pragma warning disable format // region formatting is different in net7.0 and net472, and cannot be fixed for both
#region Methods
/// <summary>
/// Validates a project file against the given schema. If no schema is given, validates
/// against the default schema
/// </summary>
/// <param name="projectFile">Path of the file to validate.</param>
/// <param name="schemaFile">Can be null.</param>
/// <param name="binPath">Path to the framework directory where the default schema for
/// this ToolsVersion can be found.</param>
/// <returns>True if the project was successfully validated against the given schema, false otherwise</returns>
internal static void VerifyProjectSchema(
string projectFile,
string schemaFile,
string binPath)
{
ErrorUtilities.VerifyThrowArgumentNull(projectFile);
ErrorUtilities.VerifyThrowArgumentNull(binPath);
if (string.IsNullOrEmpty(schemaFile))
{
schemaFile = Path.Combine(binPath, "Microsoft.Build.xsd");
}
if (FileSystems.Default.FileExists(schemaFile))
{
// Print the schema file we're using, particularly since it can vary
// according to the toolset being used
Console.WriteLine(AssemblyResources.GetString("SchemaFileLocation"), schemaFile);
}
else
{
// If we've gotten to this point, there is no schema to validate against -- just exit.
InitializationException.Throw(
ResourceUtilities.FormatResourceStringStripCodeAndKeyword("SchemaNotFoundErrorWithFile", schemaFile),
null); /* No associated command line switch */
}
ProjectSchemaValidationHandler validationHandler = new ProjectSchemaValidationHandler();
validationHandler.VerifyProjectSchema(projectFile, schemaFile);
}
/// <summary>
/// Validates a project against the given schema. A schema file must be provided.
/// </summary>
private void VerifyProjectSchema(
string projectFile,
string schemaFile)
{
ErrorUtilities.VerifyThrowArgumentNull(schemaFile);
ErrorUtilities.VerifyThrowArgumentNull(projectFile);
// Options for XmlReader object can be set only in constructor. After the object is created, they
// become read-only. Because of that we need to create
// XmlSettings structure, fill it in with correct parameters and pass into XmlReader constructor.
XmlReaderSettings validatorSettings = new XmlReaderSettings();
validatorSettings.ValidationType = ValidationType.Schema;
validatorSettings.XmlResolver = null;
validatorSettings.ValidationEventHandler += this.OnSchemaValidationError;
XmlTextReader schemaReader = new XmlTextReader(schemaFile);
schemaReader.DtdProcessing = DtdProcessing.Ignore;
using (schemaReader)
{
try
{
validatorSettings.Schemas.Add(XMakeAttributes.defaultXmlNamespace, schemaReader);
// We need full path to the project file to be able handle it as URI in ValidationEventHandler.
// Uri class cannot instantiate with relative paths.
projectFile = Path.GetFullPath(projectFile);
using (StreamReader contentReader = new StreamReader(projectFile))
{
using (XmlReader validator = XmlReader.Create(contentReader, validatorSettings, projectFile)) // May also throw XmlSchemaException
{
_syntaxError = false;
bool couldRead = true;
while (couldRead)
{
try
{
couldRead = validator.Read();
}
catch (XmlException)
{
// We swallow exception here because XmlValidator fires the validation event to report the error
// And we handle the event. Also XmlValidator can continue parsing Xml text after throwing an exception.
// Thus we don't need any special recover here.
}
}
VerifyThrowInitializationExceptionWithResource(
!_syntaxError,
projectFile,
0 /* line */,
0 /* end line */,
0 /* column */,
0 /* end column */,
"ProjectSchemaErrorHalt");
}
}
}
// handle errors in the schema itself
catch (XmlException e)
{
ThrowInitializationExceptionWithResource(
(e.SourceUri.Length == 0) ? String.Empty : new Uri(e.SourceUri).LocalPath,
e.LineNumber,
0 /* end line */,
e.LinePosition,
0 /* end column */,
"InvalidSchemaFile",
schemaFile,
e.Message);
}
// handle errors in the schema itself
catch (XmlSchemaException e)
{
ThrowInitializationExceptionWithResource(
(e.SourceUri.Length == 0) ? String.Empty : new Uri(e.SourceUri).LocalPath,
e.LineNumber,
0 /* end line */,
e.LinePosition,
0 /* end column */,
"InvalidSchemaFile",
schemaFile,
e.Message);
}
}
}
/// <summary>
/// Given the parameters passed in, if the condition is false, builds an
/// error message and throws an InitializationException with that message.
/// </summary>
private static void VerifyThrowInitializationExceptionWithResource(
bool condition,
string projectFile,
int fileLine,
int fileEndLine,
int fileColumn,
int fileEndColumn,
string resourceName,
params object[] args)
{
if (!condition)
{
ThrowInitializationExceptionWithResource(
projectFile,
fileLine,
fileEndLine,
fileColumn,
fileEndColumn,
resourceName,
args);
}
}
/// <summary>
/// Given the parameters passed in, builds an error message and throws an
/// InitializationException with that message.
/// </summary>
private static void ThrowInitializationExceptionWithResource(
string projectFile,
int fileLine,
int fileEndLine,
int fileColumn,
int fileEndColumn,
string resourceName,
params object[] args)
{
InitializationException.Throw(
BuildStringFromResource(
projectFile,
fileLine,
fileEndLine,
fileColumn,
fileEndColumn,
resourceName,
args),
null); /* No associated command line switch */
}
/// <summary>
/// Given a resource string and information about a file, builds up a string
/// containing the message.
/// </summary>
private static string BuildStringFromResource(
string projectFile,
int fileLine,
int fileEndLine,
int fileColumn,
int fileEndColumn,
string resourceName,
params object[] args)
{
string errorCode;
string helpKeyword;
string message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(out errorCode, out helpKeyword, resourceName, args);
return EventArgsFormatting.FormatEventMessage(
"error",
AssemblyResources.GetString("SubCategoryForSchemaValidationErrors"),
message,
errorCode,
projectFile,
fileLine,
fileEndLine,
fileColumn,
fileEndColumn,
0); /* thread id */
}
#endregion // Methods
#region Event Handlers
/// <summary>
/// Receives any errors that occur while validating the project's schema.
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
private void OnSchemaValidationError(object sender, ValidationEventArgs args)
{
_syntaxError = true;
// We should handle empty URI specially, because Uri class does not allow to instantiate with empty string.
string filePath = String.Empty;
if (args.Exception.SourceUri.Length != 0)
{
filePath = (new Uri(args.Exception.SourceUri)).LocalPath;
}
Console.WriteLine(
BuildStringFromResource(
filePath,
args.Exception.LineNumber,
0 /* end line */,
args.Exception.LinePosition,
0 /* end column */,
"SchemaValidationError",
args.Exception.Message));
}
#endregion // Event Handlers
#pragma warning restore format
}
}
#endif
|