From c04ef83e8645b169d428983c6f68acf5bfa1b59c Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova Date: Tue, 25 Aug 2026 10:46:15 +0200 Subject: [PATCH] Harden typed task parameter infrastructure Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../BuildEventArgsSerialization_Tests.cs | 93 +++++++++++++++++++ .../IntrinsicTasks/ItemGroupLoggingHelper.cs | 8 ++ .../BinaryLogger/BuildEventArgsWriter.cs | 5 + src/Framework/TaskItem_T.cs | 11 ++- src/Framework/TaskParameterEventArgs.cs | 5 + src/Utilities.UnitTests/TaskItem_Tests.cs | 8 ++ 6 files changed, 128 insertions(+), 2 deletions(-) diff --git a/src/Build.UnitTests/BuildEventArgsSerialization_Tests.cs b/src/Build.UnitTests/BuildEventArgsSerialization_Tests.cs index 7df03d42322..6f963797536 100644 --- a/src/Build.UnitTests/BuildEventArgsSerialization_Tests.cs +++ b/src/Build.UnitTests/BuildEventArgsSerialization_Tests.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Runtime.Serialization; using System.Text; using FluentAssertions; using Microsoft.Build.BackEnd; @@ -875,6 +876,98 @@ public void RoundtripTaskParameterEventArgs() e => TranslationHelpers.GetItemsString(e.Items)); } + [Fact] + public void AbsolutePathTaskParameterTextUsesOriginalValue() + { + var basePath = new AbsolutePath(Path.GetFullPath(".")); + var path = new AbsolutePath("input.txt", basePath); + + ItemGroupLoggingHelper.GetStringFromParameterValue(path).ShouldBe("input.txt"); + } + + [Fact] + public void TaskParameterEventForwardingPreservesAbsolutePathOriginalValue() + { + TaskParameterEventArgs args = CreateAbsolutePathTaskParameterEventArgs(); + var memoryStream = new MemoryStream(); + using (var binaryWriter = new BinaryWriter(memoryStream, Encoding.UTF8, leaveOpen: true)) + { + args.WriteToStream(binaryWriter); + } + + memoryStream.Position = 0; +#pragma warning disable SYSLIB0050 // Required to exercise the legacy event forwarding deserializer. + var forwardedArgs = (TaskParameterEventArgs)FormatterServices.GetUninitializedObject(typeof(TaskParameterEventArgs)); +#pragma warning restore SYSLIB0050 + using (var binaryReader = new BinaryReader(memoryStream, Encoding.UTF8, leaveOpen: true)) + { + forwardedArgs.CreateFromStream(binaryReader, version: 0); + } + + forwardedArgs.Items.Count.ShouldBe(1); + ((ITaskItem)forwardedArgs.Items[0]).ItemSpec.ShouldBe("input.txt"); + } + + [Fact] + public void BinaryLogSerializationPreservesAbsolutePathOriginalValue() + { + TaskParameterEventArgs args = CreateAbsolutePathTaskParameterEventArgs(); + var memoryStream = new MemoryStream(); + using (var binaryWriter = new BinaryWriter(memoryStream, Encoding.UTF8, leaveOpen: true)) + { + new BuildEventArgsWriter(binaryWriter).Write(args); + } + + memoryStream.Position = 0; + using var reader = new BinaryReader(memoryStream, Encoding.UTF8, leaveOpen: true); + using var eventArgsReader = new BuildEventArgsReader(reader, BinaryLogger.FileFormatVersion); + var replayedArgs = (TaskParameterEventArgs)eventArgsReader.Read(); + + replayedArgs.Items.Count.ShouldBe(1); + ((ITaskItem)replayedArgs.Items[0]).ItemSpec.ShouldBe("input.txt"); + replayedArgs.Message.ShouldContain("input.txt"); + replayedArgs.Message.ShouldNotContain(Path.GetFullPath("input.txt")); + } + + [Fact] + public void BinaryLogSerializationWritesEmptyItemSpecForDefaultAbsolutePath() + { + var args = new TaskParameterEventArgs( + TaskParameterMessageKind.TaskInput, + "File", + propertyName: null, + "File", + new object[] { default(AbsolutePath) }, + logItemMetadata: false, + DateTime.MinValue); + var memoryStream = new MemoryStream(); + using (var binaryWriter = new BinaryWriter(memoryStream, Encoding.UTF8, leaveOpen: true)) + { + new BuildEventArgsWriter(binaryWriter).Write(args); + } + + memoryStream.Position = 0; + using var reader = new BinaryReader(memoryStream, Encoding.UTF8, leaveOpen: true); + using var eventArgsReader = new BuildEventArgsReader(reader, BinaryLogger.FileFormatVersion); + var replayedArgs = (TaskParameterEventArgs)eventArgsReader.Read(); + + replayedArgs.Items.Count.ShouldBe(1); + ((ITaskItem)replayedArgs.Items[0]).ItemSpec.ShouldBe(string.Empty); + } + + private static TaskParameterEventArgs CreateAbsolutePathTaskParameterEventArgs() + { + var basePath = new AbsolutePath(Path.GetFullPath(".")); + return new TaskParameterEventArgs( + TaskParameterMessageKind.TaskInput, + "File", + propertyName: null, + "File", + new object[] { new AbsolutePath("input.txt", basePath) }, + logItemMetadata: false, + DateTime.MinValue); + } + [Fact] public void RoundtripProjectEvaluationStartedEventArgs() { diff --git a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupLoggingHelper.cs b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupLoggingHelper.cs index 2fd03538738..9038bb76d72 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupLoggingHelper.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupLoggingHelper.cs @@ -243,6 +243,14 @@ private static void AppendStringFromParameterValue(ReuseableStringBuilder sb, ob keyValuePairList.Clear(); } } + else if (parameterValue is AbsolutePath absolutePath) + { + sb.Append(absolutePath.OriginalValue); + } + else if (TaskItemTypeDetector.IsSupportedPathType(parameterValue.GetType())) + { + sb.Append(ValueTypeParser.ToString(parameterValue)); + } else if (parameterValue.GetType().IsValueType) { sb.Append((string)Convert.ChangeType(parameterValue, typeof(string), CultureInfo.CurrentCulture)); diff --git a/src/Build/Logging/BinaryLogger/BuildEventArgsWriter.cs b/src/Build/Logging/BinaryLogger/BuildEventArgsWriter.cs index cba8208d74d..06b0d64d739 100644 --- a/src/Build/Logging/BinaryLogger/BuildEventArgsWriter.cs +++ b/src/Build/Logging/BinaryLogger/BuildEventArgsWriter.cs @@ -987,6 +987,11 @@ private void WriteTaskItemList(IEnumerable items, bool writeMetadata = true) Write((byte)0); } } + else if (item is AbsolutePath absolutePath) + { + WriteDeduplicatedString(absolutePath.OriginalValue ?? string.Empty); + Write(0); + } else { WriteDeduplicatedString(item?.ToString() ?? ""); // itemspec diff --git a/src/Framework/TaskItem_T.cs b/src/Framework/TaskItem_T.cs index b54706b1d21..4c418004ec1 100644 --- a/src/Framework/TaskItem_T.cs +++ b/src/Framework/TaskItem_T.cs @@ -70,8 +70,15 @@ public TaskItem(ITaskItem item) /// Returns the FullPath metadata value if non-empty, otherwise falls back to itemSpec. private static string GetFullPathOrItemSpec(ITaskItem item, string itemSpec) { - string fullPath = item.GetMetadata("FullPath"); - return !string.IsNullOrEmpty(fullPath) ? fullPath : itemSpec; + try + { + string fullPath = item.GetMetadata("FullPath"); + return !string.IsNullOrEmpty(fullPath) ? fullPath : itemSpec; + } + catch (InvalidOperationException ex) + { + throw new ArgumentException($"Cannot create TaskItem<{typeof(T).Name}> from the provided item.", nameof(item), ex); + } } /// diff --git a/src/Framework/TaskParameterEventArgs.cs b/src/Framework/TaskParameterEventArgs.cs index 52f623017af..fdb54492bb9 100644 --- a/src/Framework/TaskParameterEventArgs.cs +++ b/src/Framework/TaskParameterEventArgs.cs @@ -220,6 +220,11 @@ private void WriteItem(BinaryWriter writer, object item) writer.Write7BitEncodedInt(0); } } + else if (item is AbsolutePath absolutePath) + { + writer.Write(absolutePath.OriginalValue ?? string.Empty); + writer.Write7BitEncodedInt(0); + } else // string or ValueType { writer.Write(item?.ToString() ?? ""); diff --git a/src/Utilities.UnitTests/TaskItem_Tests.cs b/src/Utilities.UnitTests/TaskItem_Tests.cs index 1b42c8d3169..6b92ce90bc2 100644 --- a/src/Utilities.UnitTests/TaskItem_Tests.cs +++ b/src/Utilities.UnitTests/TaskItem_Tests.cs @@ -486,6 +486,14 @@ public void FromITaskItem_PathLikeType_UsesFullPathMetadata() item.Value.FullName.ShouldBe(expectedAbsolutePath); } + [Fact] + public void FromITaskItem_InvalidPath_ThrowsArgumentException() + { + var backingItem = new TaskItem("bad\0path"); + + Should.Throw(() => new TaskItem(backingItem)); + } + [Fact] public void FromITaskItem_NonPathType_UsesItemSpec() {