Skip to content
Closed
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
17 changes: 17 additions & 0 deletions src/Shared/FileUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,23 @@ internal static string FixFilePath(string path)
return string.IsNullOrEmpty(path) || Path.DirectorySeparatorChar == '\\' ? path : path.Replace('\\', '/'); // .Replace("//", "/");
}

internal static string FixFilePath(string path, string targetOs)
{
char targetSeparator = targetOs switch
{
"windows" => '\\',
"unix" => '/',
"current" => Path.DirectorySeparatorChar,
_ => Path.DirectorySeparatorChar,
};

return path switch
{
{ } when string.IsNullOrEmpty(path) => path,
_ => path.Replace('\\', targetSeparator).Replace('/', targetSeparator),
};
}

#if !CLR2COMPATIBILITY
/// <summary>
/// If on Unix, convert backslashes to slashes for strings that resemble paths.
Expand Down
233 changes: 233 additions & 0 deletions src/Tasks.UnitTests/CreateItem_TaskItem_Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
// 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.IO;
using System.Linq;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
using Microsoft.Build.Tasks;
using Microsoft.Build.Utilities;
using Shouldly;
using Xunit;

#nullable disable

namespace Microsoft.Build.UnitTests
{
public sealed class CreateItem_TaskItem_Tests
{
private const string ProjectUsingTaskItemGroup =
"""
<Project ToolsVersion=`msbuilddefaulttoolsversion`>
<Target Name=`TargetUsingItemGroup`>
<ItemGroup>
<Items Include=`{0}` />
</ItemGroup>
</Target>
</Project>
""";

private const string ProjectUsingTaskItemGroupWithFixFilePathFalse =
"""
<Project ToolsVersion=`msbuilddefaulttoolsversion`>
<Target Name=`TargetUsingItemGroup`>
<ItemGroup>
<Items Include=`{0}`>
<FixFilePath>false</FixFilePath>
</Items>
</ItemGroup>
</Target>
</Project>
""";

private const string ProjectUsingTaskItemGroupWithTargetOs =
"""
<Project ToolsVersion=`msbuilddefaulttoolsversion`>
<Target Name=`TargetUsingItemGroup`>
<ItemGroup>
<Items Include=`{0}`>
<TargetOs>{1}</TargetOs>
</Items>
</ItemGroup>
</Target>
</Project>
""";

/// <summary>
/// CreateItem automatically fixes the directory separator in the Include items by default
/// (this is the current behaviour, and we cannot change that without a large impact)
/// </summary>
[Theory]
[MemberData(nameof(PathsWithVariousSlashes))]
public void FixesDirectorySeparatorCharByDefault(string original, string expected)
{
CreateItem t = new() { BuildEngine = new MockEngine(), Include = [new TaskItem(original)], };

bool success = t.Execute();
success.ShouldBeTrue();

t.Include[0].ItemSpec.ShouldBe(expected);
}

/// <summary>
/// CreateItem automatically fixes the directory separator in the Include items by default
/// (this is the current behaviour, and we cannot change that without a large impact)
/// </summary>
[Theory]
[MemberData(nameof(PathsWithVariousSlashes))]
public void FixesDirectorySeparatorCharByDefaultInAnActualProject(string original, string expected)
{
string projectFile = RandomProjectFile();
string projectContent = string.Format(ProjectUsingTaskItemGroup, original);

Project project = ObjectModelHelpers.LoadProjectFileInTempProjectDirectory(
ObjectModelHelpers.CreateFileInTempProjectDirectory(projectFile,
projectContent));

var instance = project.CreateProjectInstance(ProjectInstanceSettings.None);
Assert.True(instance.Build(["TargetUsingItemGroup"], []));

var items = instance.GetItems("Items");
items.ShouldHaveSingleItem();
items.First().EvaluatedInclude.ShouldBe(expected);
}

/// <summary>
/// CreateItem does not automatically fix the directory separator in the Include item if the
/// special metadata item FixFilePath is set to false
/// </summary>
[Theory]
[MemberData(nameof(PathsWithVariousSlashes))]
public void DoesNotFixDirectorySeparatorCharIfSpecialMetaDataIsSet(string original, string _)
{
var metadata = new Dictionary<string, string> { { "FixFilePath", "false" }, };

CreateItem t = new() { BuildEngine = new MockEngine(), Include = [new TaskItem(original, metadata)], };

bool success = t.Execute();
success.ShouldBeTrue();

t.Include[0].ItemSpec.ShouldBe(original);
}

/// <summary>
/// CreateItem does not automatically fix the directory separator in the Include item if the
/// special metadata item FixFilePath is set to false
/// </summary>
[Theory]
[MemberData(nameof(PathsWithVariousSlashes))]
public void DoesNotFixDirectorySeparatorCharIfSpecialMetaDataIsSetInAnActualProject(string original, string _)
{
string projectFile = RandomProjectFile();
string projectContent = string.Format(ProjectUsingTaskItemGroupWithFixFilePathFalse, original);

Project project = ObjectModelHelpers.LoadProjectFileInTempProjectDirectory(
ObjectModelHelpers.CreateFileInTempProjectDirectory(projectFile,
projectContent));

var instance = project.CreateProjectInstance(ProjectInstanceSettings.None);
Assert.True(instance.Build(["TargetUsingItemGroup"], []));

var items = instance.GetItems("Items");
items.ShouldHaveSingleItem();
items.First().EvaluatedInclude.ShouldBe(original);
}

/// <summary>
/// CreateItem uses the target platform when fixing the directory separator if the
/// special metadata item TargetPlatform is set
/// </summary>
[Theory]
[MemberData(nameof(PathsWithVariousSlashesAndTargetOs))]
public void FixesDirectorySeparatorCharToSuppliedTargetPlatform(string platform, string original, string expected)
{
var metadata = new Dictionary<string, string> { { "TargetOs", platform }, };

CreateItem t = new() { BuildEngine = new MockEngine(), Include = [new TaskItem(original, metadata)], };

bool success = t.Execute();
success.ShouldBeTrue();

t.Include[0].ItemSpec.ShouldBe(expected);
}

/// <summary>
/// CreateItem uses the target platform when fixing the directory separator if the
/// special metadata item TargetPlatform is set
/// </summary>
[Theory]
[MemberData(nameof(PathsWithVariousSlashesAndTargetOs))]
public void FixesDirectorySeparatorCharToSuppliedTargetPlatformInAnActualProject(string targetOs, string original, string expected)
{
string projectFile = RandomProjectFile();
string projectContent = string.Format(ProjectUsingTaskItemGroupWithTargetOs, original, targetOs);

Project project = ObjectModelHelpers.LoadProjectFileInTempProjectDirectory(
ObjectModelHelpers.CreateFileInTempProjectDirectory(projectFile,
projectContent));

var instance = project.CreateProjectInstance(ProjectInstanceSettings.None);
Assert.True(instance.Build(["TargetUsingItemGroup"], []));

var items = instance.GetItems("Items");
items.ShouldHaveSingleItem();
items.First().EvaluatedInclude.ShouldBe(expected);
}


public static TheoryData<string, string> PathsWithVariousSlashes
{
get
{
char s = Path.DirectorySeparatorChar;
return new TheoryData<string, string>
{
{ @"C:\windows\path\anyfile.txt", $"C:{s}windows{s}path{s}anyfile.txt" },
{ @"unrooted\windows\path\anyfile.txt", $"unrooted{s}windows{s}path{s}anyfile.txt" },
{ @"C:/windows/path/with/unix/slashes/anyfile.txt", $"C:{s}windows{s}path{s}with{s}unix{s}slashes{s}anyfile.txt" },
{ @"/unixpath/anyfile.txt", $"{s}unixpath{s}anyfile.txt" },
{ @"/mixed\paths/anyfile.txt", $"{s}mixed{s}paths{s}anyfile.txt" },
};
}
}

public static TheoryData<string, string, string> PathsWithVariousSlashesAndTargetOs
{
get
{
char s = Path.DirectorySeparatorChar;
char w = '\\';
char u = '/';
return new TheoryData<string, string, string>
{
{ "windows", @"C:\windows\path\anyfile.txt", $"C:{w}windows{w}path{w}anyfile.txt" },
{ "windows", @"unrooted\windows\path\anyfile.txt", $"unrooted{w}windows{w}path{w}anyfile.txt" },
{ "windows", @"C:/windows/path/with/unix/slashes/anyfile.txt", $"C:{w}windows{w}path{w}with{w}unix{w}slashes{w}anyfile.txt" },
{ "windows", @"/unixpath/anyfile.txt", $"{w}unixpath{w}anyfile.txt" },
{ "windows", @"/mixed\paths/anyfile.txt", $"{w}mixed{w}paths{w}anyfile.txt" },
{ "unix", @"C:\windows\path\anyfile.txt", $"C:{u}windows{u}path{u}anyfile.txt" },
{ "unix", @"unrooted\windows\path\anyfile.txt", $"unrooted{u}windows{u}path{u}anyfile.txt" },
{ "unix", @"C:/windows/path/with/unix/slashes/anyfile.txt", $"C:{u}windows{u}path{u}with{u}unix{u}slashes{u}anyfile.txt" },
{ "unix", @"/unixpath/anyfile.txt", $"{u}unixpath{u}anyfile.txt" },
{ "unix", @"/mixed\paths/anyfile.txt", $"{u}mixed{u}paths{u}anyfile.txt" },
{ "current", @"C:\windows\path\anyfile.txt", $"C:{s}windows{s}path{s}anyfile.txt" },
{ "current", @"unrooted\windows\path\anyfile.txt", $"unrooted{s}windows{s}path{s}anyfile.txt" },
{ "current", @"C:/windows/path/with/current/slashes/anyfile.txt", $"C:{s}windows{s}path{s}with{s}current{s}slashes{s}anyfile.txt" },
{ "current", @"/currentpath/anyfile.txt", $"{s}currentpath{s}anyfile.txt" },
{ "current", @"/mixed\paths/anyfile.txt", $"{s}mixed{s}paths{s}anyfile.txt" },
{ null, @"C:\windows\path\anyfile.txt", $"C:{s}windows{s}path{s}anyfile.txt" },
{ "_anything", @"unrooted\windows\path\anyfile.txt", $"unrooted{s}windows{s}path{s}anyfile.txt" },
{ "_not_valid", @"C:/windows/path/with/current/slashes/anyfile.txt", $"C:{s}windows{s}path{s}with{s}current{s}slashes{s}anyfile.txt" },
{ "_invalid", @"/currentpath/anyfile.txt", $"{s}currentpath{s}anyfile.txt" },
{ "_run_with_default", @"/mixed\paths/anyfile.txt", $"{s}mixed{s}paths{s}anyfile.txt" },
};
}
}

private static string RandomProjectFile() =>
Random.Shared.GetItems("abcdefghijklmnopqrstuvwxyz0123456789".ToCharArray(), 8)
.ToString();
}
}
53 changes: 52 additions & 1 deletion src/Utilities/TaskItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ public sealed class TaskItem :
// This is the final evaluated item specification. Stored in escaped form.
private string _itemSpec;

// The original, un-touched item-spec, we need this to be able to fix the paths correctly
private string _originalItemSpec;

// These are the user-defined metadata on the item, specified in the
// project file via XML child elements of the item element. These have
// no meaning to MSBuild, but tasks may use them.
Expand All @@ -64,6 +67,7 @@ public sealed class TaskItem :
/// </summary>
public TaskItem()
{
_originalItemSpec = string.Empty;
_itemSpec = string.Empty;
}

Expand All @@ -77,9 +81,46 @@ public TaskItem(
{
ErrorUtilities.VerifyThrowArgumentNull(itemSpec, nameof(itemSpec));

_originalItemSpec = itemSpec;
_itemSpec = FileUtilities.FixFilePath(itemSpec);

FixPathsAccordingToMetadata();
}

private const string FixFilePath = nameof(FixFilePath);
private const string TargetOs = nameof(TargetOs);

private void FixPathsAccordingToMetadata()
{
bool fixFilePath = true;
string targetOs = null;

foreach (string key in _metadata.Keys)
{
// Check if the special metadata FixFilePath is set to false - if so, don't fix the file path
if (FixFilePath.Equals(key, StringComparison.OrdinalIgnoreCase) && bool.TryParse(_metadata[key], out bool fixFilePathValue))
{
fixFilePath = fixFilePathValue;
}

// Check if the special metadata TargetPlatform is set - if it is, use that when fixing the paths
if (TargetOs.Equals(key, StringComparison.OrdinalIgnoreCase))
{
targetOs = _metadata[key];
}
}

if (!fixFilePath)
{
_itemSpec = _originalItemSpec;
}
else if (targetOs != null)
{
_itemSpec = FileUtilities.FixFilePath(_originalItemSpec, targetOs);
}
}


/// <summary>
/// This constructor creates a new TaskItem, using the given item spec and metadata.
/// </summary>
Expand All @@ -106,10 +147,13 @@ public TaskItem(
string key = (string)singleMetadata.Key;
if (!FileUtilities.ItemSpecModifiers.IsDerivableItemSpecModifier(key))
{
_metadata[key] = (string)singleMetadata.Value ?? string.Empty;
string value = (string)singleMetadata.Value;
_metadata[key] = value ?? string.Empty;
}
}
}

FixPathsAccordingToMetadata();
}

/// <summary>
Expand All @@ -134,6 +178,7 @@ public TaskItem(
}

sourceItem.CopyMetadataTo(this);
FixPathsAccordingToMetadata();
}

#endregion
Expand All @@ -157,7 +202,9 @@ public string ItemSpec
{
ErrorUtilities.VerifyThrowArgumentNull(value, nameof(ItemSpec));

_originalItemSpec = value;
_itemSpec = FileUtilities.FixFilePath(value);
FixPathsAccordingToMetadata();
_fullPath = null;
}
}
Expand All @@ -177,6 +224,7 @@ string ITaskItem2.EvaluatedIncludeEscaped
set
{
_itemSpec = FileUtilities.FixFilePath(value);
FixPathsAccordingToMetadata();
_fullPath = null;
}
}
Expand Down Expand Up @@ -225,6 +273,7 @@ private CopyOnWriteDictionary<string> Metadata
set
{
_metadata = value;
FixPathsAccordingToMetadata();
}
}

Expand All @@ -243,6 +292,7 @@ public void RemoveMetadata(string metadataName)
"Shared.CannotChangeItemSpecModifiers", metadataName);

_metadata?.Remove(metadataName);
FixPathsAccordingToMetadata();
}

/// <summary>
Expand All @@ -267,6 +317,7 @@ public void SetMetadata(
_metadata ??= new CopyOnWriteDictionary<string>(MSBuildNameIgnoreCaseComparer.Default);

_metadata[metadataName] = metadataValue ?? string.Empty;
FixPathsAccordingToMetadata();
}

/// <summary>
Expand Down