|
// 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;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
#if FEATURE_SECURITY_PERMISSIONS
using System.Security;
#endif
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.Collections;
#nullable disable
namespace Microsoft.Build.Utilities
{
/// <summary>
/// This class represents a single item of the project, as it is passed into a task. TaskItems do not exactly correspond to
/// item elements in project files, because then tasks would have access to data that wasn't explicitly passed into the task
/// via the project file. It's not a security issue, but more just an issue with project file clarity and transparency.
///
/// Note: This class has to be sealed. It has to be sealed because the engine instantiates it's own copy of this type and
/// thus if someone were to extend it, they would not get the desired behavior from the engine.
/// </summary>
/// <comment>
/// Surprisingly few of these Utilities TaskItems are created: typically several orders of magnitude fewer than the number of engine TaskItems.
/// </comment>
public sealed class TaskItem :
#if FEATURE_APPDOMAIN
MarshalByRefObject,
#endif
ITaskItem2,
IMetadataContainer // expose direct underlying metadata for fast access in binary logger
{
#region Member Data
// This is the final evaluated item specification. Stored in escaped form.
private string _itemSpec;
// 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.
// Values are stored in escaped form.
private CopyOnWriteDictionary<string> _metadata;
// cache of the fullpath value
private string _fullPath;
/// <summary>
/// May be defined if we're copying this item from a pre-existing one. Otherwise,
/// we simply don't know enough to set it properly, so it will stay null.
/// </summary>
private readonly string _definingProject;
#endregion
#region Constructors
/// <summary>
/// Default constructor -- do not use.
/// </summary>
/// <remarks>
/// This constructor exists only so that the type is COM-creatable. Prefer <see cref="TaskItem(string)"/>.
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Never)]
public TaskItem()
{
_itemSpec = string.Empty;
}
/// <summary>
/// This constructor creates a new task item, given the item spec.
/// </summary>
/// <comments>Assumes the itemspec passed in is escaped.</comments>
/// <param name="itemSpec">The item-spec string.</param>
public TaskItem(
string itemSpec)
{
ErrorUtilities.VerifyThrowArgumentNull(itemSpec);
_itemSpec = FileUtilities.FixFilePath(itemSpec);
}
/// <summary>
/// This constructor creates a new TaskItem, using the given item spec and metadata.
/// </summary>
/// <comments>
/// Assumes the itemspec passed in is escaped, and also that any escapable metadata values
/// are passed in escaped form.
/// </comments>
/// <param name="itemSpec">The item-spec string.</param>
/// <param name="itemMetadata">Custom metadata on the item.</param>
public TaskItem(
string itemSpec,
IDictionary itemMetadata) :
this(itemSpec)
{
ErrorUtilities.VerifyThrowArgumentNull(itemMetadata);
if (itemMetadata.Count > 0)
{
_metadata = new CopyOnWriteDictionary<string>(MSBuildNameIgnoreCaseComparer.Default);
foreach (DictionaryEntry singleMetadata in itemMetadata)
{
// don't import metadata whose names clash with the names of reserved metadata
string key = (string)singleMetadata.Key;
if (!FileUtilities.ItemSpecModifiers.IsDerivableItemSpecModifier(key))
{
_metadata[key] = (string)singleMetadata.Value ?? string.Empty;
}
}
}
}
/// <summary>
/// This constructor creates a new TaskItem, using the given ITaskItem.
/// </summary>
/// <param name="sourceItem">The item to copy.</param>
public TaskItem(
ITaskItem sourceItem)
{
ErrorUtilities.VerifyThrowArgumentNull(sourceItem);
// Attempt to preserve escaped state
if (!(sourceItem is ITaskItem2 sourceItemAsITaskItem2))
{
_itemSpec = EscapingUtilities.Escape(sourceItem.ItemSpec);
_definingProject = EscapingUtilities.EscapeWithCaching(sourceItem.GetMetadata(FileUtilities.ItemSpecModifiers.DefiningProjectFullPath));
}
else
{
_itemSpec = sourceItemAsITaskItem2.EvaluatedIncludeEscaped;
_definingProject = sourceItemAsITaskItem2.GetMetadataValueEscaped(FileUtilities.ItemSpecModifiers.DefiningProjectFullPath);
}
sourceItem.CopyMetadataTo(this);
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the item-spec.
/// </summary>
/// <comments>
/// This one is a bit tricky. Orcas assumed that the value being set was escaped, but
/// that the value being returned was unescaped. Maintain that behaviour here. To get
/// the escaped value, use ITaskItem2.EvaluatedIncludeEscaped.
/// </comments>
/// <value>The item-spec string.</value>
public string ItemSpec
{
get => _itemSpec == null ? string.Empty : EscapingUtilities.UnescapeAll(_itemSpec);
set
{
ErrorUtilities.VerifyThrowArgumentNull(value, nameof(ItemSpec));
_itemSpec = FileUtilities.FixFilePath(value);
_fullPath = null;
}
}
/// <summary>
/// Gets or sets the escaped include, or "name", for the item.
/// </summary>
/// <remarks>
/// Taking the opportunity to fix the property name, although this doesn't
/// make it obvious it's an improvement on ItemSpec.
/// </remarks>
string ITaskItem2.EvaluatedIncludeEscaped
{
// It's already escaped
get => _itemSpec;
set
{
_itemSpec = FileUtilities.FixFilePath(value);
_fullPath = null;
}
}
/// <summary>
/// Gets the names of all the item's metadata.
/// </summary>
/// <value>List of metadata names.</value>
public ICollection MetadataNames
{
get
{
int count = (_metadata?.Count ?? 0) + FileUtilities.ItemSpecModifiers.All.Length;
var metadataNames = new List<string>(capacity: count);
if (_metadata is not null)
{
metadataNames.AddRange(_metadata.Keys);
}
metadataNames.AddRange(FileUtilities.ItemSpecModifiers.All);
return metadataNames;
}
}
/// <summary>
/// Gets the number of metadata set on the item.
/// </summary>
/// <value>Count of metadata.</value>
public int MetadataCount => (_metadata?.Count ?? 0) + FileUtilities.ItemSpecModifiers.All.Length;
/// <summary>
/// Gets the metadata dictionary
/// Property is required so that we can access the metadata dictionary in an item from
/// another appdomain, as the CLR has implemented remoting policies that disallow accessing
/// private fields in remoted items.
/// </summary>
private CopyOnWriteDictionary<string> Metadata
{
get
{
return _metadata;
}
set
{
_metadata = value;
}
}
#endregion
#region Methods
/// <summary>
/// Removes one of the arbitrary metadata on the item.
/// </summary>
/// <param name="metadataName">Name of metadata to remove.</param>
public void RemoveMetadata(string metadataName)
{
ErrorUtilities.VerifyThrowArgumentNull(metadataName);
ErrorUtilities.VerifyThrowArgument(!FileUtilities.ItemSpecModifiers.IsItemSpecModifier(metadataName),
"Shared.CannotChangeItemSpecModifiers", metadataName);
_metadata?.Remove(metadataName);
}
/// <summary>
/// Sets one of the arbitrary metadata on the item.
/// </summary>
/// <comments>
/// Assumes that the value being passed in is in its escaped form.
/// </comments>
/// <param name="metadataName">Name of metadata to set or change.</param>
/// <param name="metadataValue">Value of metadata.</param>
public void SetMetadata(
string metadataName,
string metadataValue)
{
ErrorUtilities.VerifyThrowArgumentLength(metadataName);
// Non-derivable metadata can only be set at construction time.
// That's why this is IsItemSpecModifier and not IsDerivableItemSpecModifier.
ErrorUtilities.VerifyThrowArgument(!FileUtilities.ItemSpecModifiers.IsDerivableItemSpecModifier(metadataName),
"Shared.CannotChangeItemSpecModifiers", metadataName);
_metadata ??= new CopyOnWriteDictionary<string>(MSBuildNameIgnoreCaseComparer.Default);
_metadata[metadataName] = metadataValue ?? string.Empty;
}
/// <summary>
/// Retrieves one of the arbitrary metadata on the item.
/// If not found, returns empty string.
/// </summary>
/// <comments>
/// Returns the unescaped value of the metadata requested.
/// </comments>
/// <param name="metadataName">The name of the metadata to retrieve.</param>
/// <returns>The metadata value.</returns>
public string GetMetadata(string metadataName)
{
string metadataValue = (this as ITaskItem2).GetMetadataValueEscaped(metadataName);
return EscapingUtilities.UnescapeAll(metadataValue);
}
/// <summary>
/// Copy the metadata (but not the ItemSpec) to destinationItem. If a particular metadata already exists on the
/// destination item, then it is not overwritten -- the original value wins.
/// </summary>
/// <param name="destinationItem">The item to copy metadata to.</param>
public void CopyMetadataTo(ITaskItem destinationItem)
{
ErrorUtilities.VerifyThrowArgumentNull(destinationItem);
// also copy the original item-spec under a "magic" metadata -- this is useful for tasks that forward metadata
// between items, and need to know the source item where the metadata came from
string originalItemSpec = destinationItem.GetMetadata("OriginalItemSpec");
ITaskItem2 destinationAsITaskItem2 = destinationItem as ITaskItem2;
if (_metadata != null)
{
if (destinationItem is TaskItem destinationAsTaskItem)
{
CopyOnWriteDictionary<string> copiedMetadata;
// Avoid a copy if we can, and if not, minimize the number of items we have to set.
if (destinationAsTaskItem.Metadata == null)
{
copiedMetadata = _metadata.Clone(); // Copy on write!
}
else if (destinationAsTaskItem.Metadata.Count < _metadata.Count)
{
copiedMetadata = _metadata.Clone(); // Copy on write!
copiedMetadata.SetItems(destinationAsTaskItem.Metadata.Where(entry => !String.IsNullOrEmpty(entry.Value)));
}
else
{
copiedMetadata = destinationAsTaskItem.Metadata.Clone();
copiedMetadata.SetItems(_metadata.Where(entry => !destinationAsTaskItem.Metadata.TryGetValue(entry.Key, out string val) || String.IsNullOrEmpty(val)));
}
destinationAsTaskItem.Metadata = copiedMetadata;
}
else
{
foreach (KeyValuePair<string, string> entry in _metadata)
{
string value;
if (destinationAsITaskItem2 != null)
{
value = destinationAsITaskItem2.GetMetadataValueEscaped(entry.Key);
if (string.IsNullOrEmpty(value))
{
destinationAsITaskItem2.SetMetadata(entry.Key, entry.Value);
}
}
else
{
value = destinationItem.GetMetadata(entry.Key);
if (string.IsNullOrEmpty(value))
{
destinationItem.SetMetadata(entry.Key, EscapingUtilities.Escape(entry.Value));
}
}
}
}
}
if (string.IsNullOrEmpty(originalItemSpec))
{
if (destinationAsITaskItem2 != null)
{
destinationAsITaskItem2.SetMetadata("OriginalItemSpec", ((ITaskItem2)this).EvaluatedIncludeEscaped);
}
else
{
destinationItem.SetMetadata("OriginalItemSpec", EscapingUtilities.Escape(ItemSpec));
}
}
}
/// <summary>
/// Get the collection of custom metadata. This does not include built-in metadata.
/// </summary>
/// <remarks>
/// RECOMMENDED GUIDELINES FOR METHOD IMPLEMENTATIONS:
/// 1) this method should return a clone of the metadata
/// 2) writing to this dictionary should not be reflected in the underlying item.
/// </remarks>
/// <comments>
/// Returns an UNESCAPED version of the custom metadata. For the escaped version (which
/// is how it is stored internally), call ITaskItem2.CloneCustomMetadataEscaped.
/// </comments>
public IDictionary CloneCustomMetadata()
{
var dictionary = new CopyOnWriteDictionary<string>(MSBuildNameIgnoreCaseComparer.Default);
if (_metadata != null)
{
foreach (KeyValuePair<string, string> entry in _metadata)
{
dictionary.Add(entry.Key, EscapingUtilities.UnescapeAll(entry.Value));
}
}
return dictionary;
}
/// <summary>
/// Gets the item-spec.
/// </summary>
/// <returns>The item-spec string.</returns>
public override string ToString() => _itemSpec;
#if FEATURE_APPDOMAIN
/// <summary>
/// Overridden to give this class infinite lease time. Otherwise we end up with a limited
/// lease (5 minutes I think) and instances can expire if they take long time processing.
/// </summary>
[SecurityCritical]
public override object InitializeLifetimeService() => null; // null means infinite lease time
#endif
#endregion
#region Operators
/// <summary>
/// This allows an explicit typecast from a "TaskItem" to a "string", returning the escaped ItemSpec for this item.
/// </summary>
/// <param name="taskItemToCast">The item to operate on.</param>
/// <returns>The item-spec of the item.</returns>
public static explicit operator string(TaskItem taskItemToCast)
{
ErrorUtilities.VerifyThrowArgumentNull(taskItemToCast);
return taskItemToCast.ItemSpec;
}
#endregion
#region ITaskItem2 implementation
/// <summary>
/// Returns the escaped value of the metadata with the specified key.
/// </summary>
string ITaskItem2.GetMetadataValueEscaped(string metadataName)
{
ErrorUtilities.VerifyThrowArgumentNull(metadataName);
string metadataValue = null;
if (FileUtilities.ItemSpecModifiers.IsDerivableItemSpecModifier(metadataName))
{
// FileUtilities.GetItemSpecModifier is expecting escaped data, which we assume we already are.
// Passing in a null for currentDirectory indicates we are already in the correct current directory
metadataValue = FileUtilities.ItemSpecModifiers.GetItemSpecModifier(null, _itemSpec, _definingProject, metadataName, ref _fullPath);
}
else
{
_metadata?.TryGetValue(metadataName, out metadataValue);
}
return metadataValue ?? string.Empty;
}
/// <summary>
/// Sets the escaped value of the metadata with the specified name.
/// </summary>
/// <comments>
/// Assumes the value is passed in unescaped.
/// </comments>
void ITaskItem2.SetMetadataValueLiteral(string metadataName, string metadataValue) => SetMetadata(metadataName, EscapingUtilities.Escape(metadataValue));
/// <summary>
/// ITaskItem2 implementation which returns a clone of the metadata on this object.
/// Values returned are in their original escaped form.
/// </summary>
/// <returns>The cloned metadata.</returns>
IDictionary ITaskItem2.CloneCustomMetadataEscaped() => _metadata == null
? new CopyOnWriteDictionary<string>(MSBuildNameIgnoreCaseComparer.Default)
: _metadata.Clone();
#endregion
IEnumerable<KeyValuePair<string, string>> IMetadataContainer.EnumerateMetadata()
{
#if FEATURE_APPDOMAIN
// Can't send a yield-return iterator across AppDomain boundaries
// so have to allocate
if (!AppDomain.CurrentDomain.IsDefaultAppDomain())
{
return EnumerateMetadataEager();
}
#endif
// In general case we want to return an iterator without allocating a collection
// to hold the result, so we can stream the items directly to the consumer.
return EnumerateMetadataLazy();
}
void IMetadataContainer.ImportMetadata(IEnumerable<KeyValuePair<string, string>> metadata)
{
_metadata ??= new CopyOnWriteDictionary<string>(MSBuildNameIgnoreCaseComparer.Default);
_metadata.SetItems(metadata.Select(kvp => new KeyValuePair<string, string>(kvp.Key, kvp.Value ?? string.Empty)));
}
private IEnumerable<KeyValuePair<string, string>> EnumerateMetadataEager()
{
if (_metadata == null)
{
return [];
}
int count = _metadata.Count;
int index = 0;
var result = new KeyValuePair<string, string>[count];
foreach (var kvp in _metadata)
{
var unescaped = new KeyValuePair<string, string>(kvp.Key, EscapingUtilities.UnescapeAll(kvp.Value));
result[index++] = unescaped;
}
return result;
}
private IEnumerable<KeyValuePair<string, string>> EnumerateMetadataLazy()
{
if (_metadata == null)
{
yield break;
}
foreach (var kvp in _metadata)
{
var unescaped = new KeyValuePair<string, string>(kvp.Key, EscapingUtilities.UnescapeAll(kvp.Value));
yield return unescaped;
}
}
}
}
|