Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
86 changes: 86 additions & 0 deletions src/Build.UnitTests/XmlReaderExtension_Tests.cs
Original file line number Diff line number Diff line change
@@ -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<string, string>(),
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() => "<Project>\r\n" +
" <ItemGroup>\r\n" +
" <I Include=\"I\">\r\n" +
" <M>multiple\r\n" +
"lines\r\n" +
"in\r\n" +
"this\r\n" +
"metadatum</M>\r\n" +
" </I>\r\n" +
" </ItemGroup>\r\n" +
"</Project>";
}
}
9 changes: 6 additions & 3 deletions src/Build/Xml/XmlReaderExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading