diff --git a/src/Build.UnitTests/Construction/XmlReaderWithoutLocation_Tests.cs b/src/Build.UnitTests/Construction/XmlReaderWithoutLocation_Tests.cs index 050bead637a..ebb03b8ed13 100644 --- a/src/Build.UnitTests/Construction/XmlReaderWithoutLocation_Tests.cs +++ b/src/Build.UnitTests/Construction/XmlReaderWithoutLocation_Tests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.IO; using System.Xml; using Microsoft.Build.Evaluation; diff --git a/src/Build.UnitTests/XmlReaderExtension_Tests.cs b/src/Build.UnitTests/XmlReaderExtension_Tests.cs new file mode 100644 index 00000000000..6fe9efff1cb --- /dev/null +++ b/src/Build.UnitTests/XmlReaderExtension_Tests.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; + +using Microsoft.Build.Evaluation; +using Shouldly; +using Xunit; + +#nullable disable + +namespace Microsoft.Build.UnitTests +{ + public class XmlReaderExtension_Tests + { + private readonly ITestOutputHelper _output; + + public XmlReaderExtension_Tests(ITestOutputHelper output) + { + _output = output; + } + + [WindowsOnlyFact] + public void ItemMetadataPreservesCrLfWhenLoadedFromDisk_Regression() + { + using TestEnvironment env = TestEnvironment.Create(_output); + + TransientTestFile projectFile = env.CreateFile("metadata-newlines.proj", GetMultilineMetadataProjectContents()); + var pc = env.CreateProjectCollection(); + Project project = pc.Collection.LoadProject(projectFile.Path); + string metadataValue = project.GetItems("I").Single().GetMetadataValue("M"); + + metadataValue.ShouldBe(string.Join(Environment.NewLine, + [ + "multiple", + "lines", + "in", + "this", + "metadatum", + ])); + } + + [WindowsOnlyFact] + public void ItemMetadataPreservesCrLfWhenLoadedReadOnly_Regression() + { + using TestEnvironment env = TestEnvironment.Create(_output); + + TransientTestFile projectFile = env.CreateFile("metadata-newlines-readonly.proj", GetMultilineMetadataProjectContents()); + + using var projectCollection = new ProjectCollection( + new Dictionary(), + loggers: null, + remoteLoggers: null, + ToolsetDefinitionLocations.Default, + maxNodeCount: 1, + onlyLogCriticalEvents: false, + loadProjectsReadOnly: true); + + Project project = projectCollection.LoadProject(projectFile.Path); + string metadataValue = project.GetItems("I").Single().GetMetadataValue("M"); + + metadataValue.ShouldBe(string.Join(Environment.NewLine, + [ + "multiple", + "lines", + "in", + "this", + "metadatum", + ])); + } + + private static string GetMultilineMetadataProjectContents() => "\r\n" + + " \r\n" + + " \r\n" + + " multiple\r\n" + + "lines\r\n" + + "in\r\n" + + "this\r\n" + + "metadatum\r\n" + + " \r\n" + + " \r\n" + + ""; + } +} diff --git a/src/Build/Xml/XmlReaderExtension.cs b/src/Build/Xml/XmlReaderExtension.cs index bd14487bf55..7c91ae9adee 100644 --- a/src/Build/Xml/XmlReaderExtension.cs +++ b/src/Build/Xml/XmlReaderExtension.cs @@ -87,9 +87,12 @@ private static XmlReader GetXmlReader(string file, StreamReader input, bool load string uri = new UriBuilder(Uri.UriSchemeFile, string.Empty) { Path = file }.ToString(); - // Ignore loadAsReadOnly for now; using XmlReader.Create results in whitespace changes - // of attribute text, specifically newline removal. - // https://github.com/dotnet/msbuild/issues/4210 + // loadAsReadOnly is currently ignored. + // Compatibility note: XmlReader.Create normalizes whitespace/newlines in ways that changed + // observed project values (for example, multiline Exec commands and metadata), breaking + // existing builds. We intentionally keep XmlTextReader behavior here to preserve those + // established semantics until a non-reflection, compatibility-safe replacement exists. + // Related history: #4210, #4213, #4083, #6232, #6669. XmlReader reader = new XmlTextReader(uri, input) { DtdProcessing = DtdProcessing.Ignore }; reader.Read();