File: BackEnd\TaskParameter.cs
Web Access
Project: ..\..\..\src\MSBuildTaskHost\MSBuildTaskHost.csproj (MSBuildTaskHost)
// 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.Globalization;
using System.Security;
using Microsoft.Build.Framework;
using Microsoft.Build.TaskHost.Resources;
using Microsoft.Build.TaskHost.Utilities;
 
namespace Microsoft.Build.TaskHost.BackEnd;
 
/// <summary>
/// Type of parameter, used to figure out how to serialize it.
/// </summary>
internal enum TaskParameterType
{
    /// <summary>
    /// Parameter is null.
    /// </summary>
    Null,
 
    /// <summary>
    /// Parameter is of a type described by a <see cref="TypeCode"/>.
    /// </summary>
    PrimitiveType,
 
    /// <summary>
    /// Parameter is an array of a type described by a <see cref="TypeCode"/>.
    /// </summary>
    PrimitiveTypeArray,
 
    /// <summary>
    /// Parameter is a value type.  Note:  Must be <see cref="IConvertible"/>.
    /// </summary>
    ValueType,
 
    /// <summary>
    /// Parameter is an array of value types.  Note:  Must be <see cref="IConvertible"/>.
    /// </summary>
    ValueTypeArray,
 
    /// <summary>
    /// Parameter is an ITaskItem.
    /// </summary>
    ITaskItem,
 
    /// <summary>
    /// Parameter is an array of ITaskItems.
    /// </summary>
    ITaskItemArray,
 
    /// <summary>
    /// An invalid parameter -- the value of this parameter contains the exception
    /// that is thrown when trying to access it.
    /// </summary>
    Invalid,
}
 
/// <summary>
/// Wrapper for task parameters, to allow proper serialization even
/// in cases where the parameter is not .NET serializable.
/// </summary>
internal class TaskParameter : MarshalByRefObject, ITranslatable
{
    /// <summary>
    /// The TaskParameterType of the wrapped parameter.
    /// </summary>
    private TaskParameterType _parameterType;
 
    /// <summary>
    /// The <see cref="TypeCode"/> of the wrapped parameter if it's a primitive type.
    /// </summary>
    private TypeCode _parameterTypeCode;
 
    /// <summary>
    /// The actual task parameter that we're wrapping
    /// </summary>
    private object? _wrappedParameter;
 
    public TaskParameter(object? wrappedParameter)
    {
        if (wrappedParameter == null)
        {
            _parameterType = TaskParameterType.Null;
            _wrappedParameter = null;
            return;
        }
 
        Type wrappedParameterType = wrappedParameter.GetType();
 
        if (wrappedParameter is Exception)
        {
            _parameterType = TaskParameterType.Invalid;
            _wrappedParameter = wrappedParameter;
            return;
        }
 
        // It's not null or invalid, so it should be a valid parameter type.
        ErrorUtilities.VerifyThrow(
            TaskParameterTypeVerifier.IsValidInputParameter(wrappedParameterType) || TaskParameterTypeVerifier.IsValidOutputParameter(wrappedParameterType),
            "How did we manage to get a task parameter of type {0} that isn't a valid parameter type?",
            wrappedParameterType);
 
        if (wrappedParameterType.IsArray)
        {
            TypeCode typeCode = Type.GetTypeCode(wrappedParameterType.GetElementType());
            if (typeCode != TypeCode.Object && typeCode != TypeCode.DBNull)
            {
                _parameterType = TaskParameterType.PrimitiveTypeArray;
                _parameterTypeCode = typeCode;
                _wrappedParameter = wrappedParameter;
            }
            else if (typeof(ITaskItem[]).IsAssignableFrom(wrappedParameterType))
            {
                _parameterType = TaskParameterType.ITaskItemArray;
                ITaskItem[] inputAsITaskItemArray = (ITaskItem[])wrappedParameter;
                ITaskItem[] taskItemArrayParameter = new ITaskItem[inputAsITaskItemArray.Length];
 
                for (int i = 0; i < inputAsITaskItemArray.Length; i++)
                {
                    if (inputAsITaskItemArray[i] != null)
                    {
                        taskItemArrayParameter[i] = new TaskParameterTaskItem(inputAsITaskItemArray[i]);
                    }
                }
 
                _wrappedParameter = taskItemArrayParameter;
            }
            else if (wrappedParameterType.GetElementType().IsValueType)
            {
                _parameterType = TaskParameterType.ValueTypeArray;
                _wrappedParameter = wrappedParameter;
            }
            else
            {
                ErrorUtilities.ThrowInternalErrorUnreachable();
            }
        }
        else
        {
            // scalar parameter
            // Preserve enums as strings: the enum type itself may not
            // be loaded on the other side of the serialization, but
            // we would convert to string anyway after pulling the
            // task output into a property or item.
            if (wrappedParameterType.IsEnum)
            {
                wrappedParameter = (string)Convert.ChangeType(wrappedParameter, typeof(string), CultureInfo.InvariantCulture);
                wrappedParameterType = typeof(string);
            }
 
            TypeCode typeCode = Type.GetTypeCode(wrappedParameterType);
            if (typeCode != TypeCode.Object && typeCode != TypeCode.DBNull)
            {
                _parameterType = TaskParameterType.PrimitiveType;
                _parameterTypeCode = typeCode;
                _wrappedParameter = wrappedParameter;
            }
            else if (typeof(ITaskItem).IsAssignableFrom(wrappedParameterType))
            {
                _parameterType = TaskParameterType.ITaskItem;
                _wrappedParameter = new TaskParameterTaskItem((ITaskItem)wrappedParameter);
            }
            else if (wrappedParameterType.IsValueType)
            {
                _parameterType = TaskParameterType.ValueType;
                _wrappedParameter = wrappedParameter;
            }
            else
            {
                ErrorUtilities.ThrowInternalErrorUnreachable();
            }
        }
    }
 
    /// <summary>
    /// Constructor for deserialization.
    /// </summary>
    private TaskParameter()
    {
    }
 
    /// <summary>
    /// Gets the <see cref="TaskParameterType"/> of the wrapped parameter.
    /// </summary>
    public TaskParameterType ParameterType => _parameterType;
 
    /// <summary>
    /// Gets the <see cref="TypeCode"/> of the wrapper parameter if it's a primitive or array of primitives.
    /// </summary>
    public TypeCode ParameterTypeCode => _parameterTypeCode;
 
    /// <summary>
    /// Gets the actual task parameter that we're wrapping.
    /// </summary>
    public object? WrappedParameter => _wrappedParameter;
 
    /// <summary>
    /// TaskParameter's ToString should just pass through to whatever it's wrapping.
    /// </summary>
    public override string ToString()
        => _wrappedParameter is null ? string.Empty : _wrappedParameter.ToString();
 
    /// <summary>
    /// Serialize / deserialize this item.
    /// </summary>
    public void Translate(ITranslator translator)
    {
        translator.TranslateEnum(ref _parameterType, (int)_parameterType);
 
        switch (_parameterType)
        {
            case TaskParameterType.Null:
                _wrappedParameter = null;
                break;
            case TaskParameterType.PrimitiveType:
                TranslatePrimitiveType(translator);
                break;
            case TaskParameterType.PrimitiveTypeArray:
                TranslatePrimitiveTypeArray(translator);
                break;
            case TaskParameterType.ValueType:
                TranslateValueType(translator);
                break;
            case TaskParameterType.ValueTypeArray:
                TranslateValueTypeArray(translator);
                break;
            case TaskParameterType.ITaskItem:
                TranslateITaskItem(translator);
                break;
            case TaskParameterType.ITaskItemArray:
                TranslateITaskItemArray(translator);
                break;
            case TaskParameterType.Invalid:
                var exceptionParam = (Exception?)_wrappedParameter;
                translator.TranslateException(ref exceptionParam);
                _wrappedParameter = exceptionParam;
                break;
            default:
                ErrorUtilities.ThrowInternalErrorUnreachable();
                break;
        }
    }
 
    /// <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;
 
    /// <summary>
    /// Factory for deserialization.
    /// </summary>
    internal static TaskParameter FactoryForDeserialization(ITranslator translator)
    {
        TaskParameter taskParameter = new();
        taskParameter.Translate(translator);
        return taskParameter;
    }
 
    /// <summary>
    /// Serialize / deserialize this item.
    /// </summary>
    private void TranslateITaskItemArray(ITranslator translator)
    {
        var wrappedItems = (ITaskItem[]?)_wrappedParameter;
        int length = wrappedItems?.Length ?? 0;
        translator.Translate(ref length);
        wrappedItems ??= new ITaskItem[length];
 
        for (int i = 0; i < wrappedItems.Length; i++)
        {
            TaskParameterTaskItem taskItem = (TaskParameterTaskItem)wrappedItems[i];
            translator.Translate(ref taskItem, TaskParameterTaskItem.FactoryForDeserialization);
            wrappedItems[i] = taskItem;
        }
 
        _wrappedParameter = wrappedItems;
    }
 
    /// <summary>
    /// Serialize / deserialize this item.
    /// </summary>
    private void TranslateITaskItem(ITranslator translator)
    {
        TaskParameterTaskItem taskItem = (TaskParameterTaskItem)_wrappedParameter!;
        translator.Translate(ref taskItem, TaskParameterTaskItem.FactoryForDeserialization);
        _wrappedParameter = taskItem;
    }
 
    /// <summary>
    /// Serializes or deserializes a primitive type value wrapped by this <see cref="TaskParameter"/>.
    /// </summary>
    private void TranslatePrimitiveType(ITranslator translator)
    {
        translator.TranslateEnum(ref _parameterTypeCode, (int)_parameterTypeCode);
 
        switch (_parameterTypeCode)
        {
            case TypeCode.Boolean:
                bool boolParam = _wrappedParameter is bool wrappedBool ? wrappedBool : default;
                translator.Translate(ref boolParam);
                _wrappedParameter = boolParam;
                break;
 
            case TypeCode.Byte:
                byte byteParam = _wrappedParameter is byte wrappedByte ? wrappedByte : default;
                translator.Translate(ref byteParam);
                _wrappedParameter = byteParam;
                break;
 
            case TypeCode.Int16:
                short shortParam = _wrappedParameter is short wrappedShort ? wrappedShort : default;
                translator.Translate(ref shortParam);
                _wrappedParameter = shortParam;
                break;
 
            case TypeCode.UInt16:
                ushort ushortParam = _wrappedParameter is ushort wrappedUShort ? wrappedUShort : default;
                translator.Translate(ref ushortParam);
                _wrappedParameter = ushortParam;
                break;
 
            case TypeCode.Int64:
                long longParam = _wrappedParameter is long wrappedLong ? wrappedLong : default;
                translator.Translate(ref longParam);
                _wrappedParameter = longParam;
                break;
 
            case TypeCode.Double:
                double doubleParam = _wrappedParameter is double wrappedDouble ? wrappedDouble : default;
                translator.Translate(ref doubleParam);
                _wrappedParameter = doubleParam;
                break;
 
            case TypeCode.String:
                string? stringParam = (string?)_wrappedParameter;
                translator.Translate(ref stringParam);
                _wrappedParameter = stringParam;
                break;
 
            case TypeCode.DateTime:
                DateTime dateTimeParam = _wrappedParameter is DateTime wrappedDateTime ? wrappedDateTime : default;
                translator.Translate(ref dateTimeParam);
                _wrappedParameter = dateTimeParam;
                break;
 
            default:
                // Fall back to converting to/from string for types that don't have ITranslator support.
                string? stringValue = null;
 
                if (translator.Mode == TranslationDirection.WriteToStream)
                {
                    stringValue = (string)Convert.ChangeType(_wrappedParameter, typeof(string), CultureInfo.InvariantCulture);
                }
 
                translator.Translate(ref stringValue);
 
                if (translator.Mode == TranslationDirection.ReadFromStream)
                {
                    _wrappedParameter = Convert.ChangeType(stringValue, _parameterTypeCode, CultureInfo.InvariantCulture);
                }
 
                break;
        }
    }
 
    /// <summary>
    /// Serializes or deserializes an array of primitive type values wrapped by this <see cref="TaskParameter"/>.
    /// </summary>
    private void TranslatePrimitiveTypeArray(ITranslator translator)
    {
        translator.TranslateEnum(ref _parameterTypeCode, (int)_parameterTypeCode);
 
        switch (_parameterTypeCode)
        {
            case TypeCode.Boolean:
                bool[]? boolArrayParam = (bool[]?)_wrappedParameter;
                translator.Translate(ref boolArrayParam);
                _wrappedParameter = boolArrayParam;
                break;
 
            case TypeCode.Int32:
                int[]? intArrayParam = (int[]?)_wrappedParameter;
                translator.Translate(ref intArrayParam);
                _wrappedParameter = intArrayParam;
                break;
 
            case TypeCode.String:
                string[]? stringArrayParam = (string[]?)_wrappedParameter;
                translator.Translate(ref stringArrayParam);
                _wrappedParameter = stringArrayParam;
                break;
 
            default:
                // Fall back to converting to/from string for types that don't have ITranslator support.
                if (translator.Mode == TranslationDirection.WriteToStream)
                {
                    Array array = (Array)_wrappedParameter!;
                    int length = array.Length;
 
                    translator.Translate(ref length);
 
                    for (int i = 0; i < length; i++)
                    {
                        string? valueString = Convert.ToString(array.GetValue(i), CultureInfo.InvariantCulture);
                        translator.Translate(ref valueString);
                    }
                }
                else
                {
                    Type elementType = _parameterTypeCode switch
                    {
                        TypeCode.Char => typeof(char),
                        TypeCode.SByte => typeof(sbyte),
                        TypeCode.Byte => typeof(byte),
                        TypeCode.Int16 => typeof(short),
                        TypeCode.UInt16 => typeof(ushort),
                        TypeCode.UInt32 => typeof(uint),
                        TypeCode.Int64 => typeof(long),
                        TypeCode.UInt64 => typeof(ulong),
                        TypeCode.Single => typeof(float),
                        TypeCode.Double => typeof(double),
                        TypeCode.Decimal => typeof(decimal),
                        TypeCode.DateTime => typeof(DateTime),
                        _ => throw new NotImplementedException(),
                    };
 
                    int length = 0;
                    translator.Translate(ref length);
 
                    Array array = Array.CreateInstance(elementType, length);
                    for (int i = 0; i < length; i++)
                    {
                        string? valueString = null;
                        translator.Translate(ref valueString);
                        array.SetValue(Convert.ChangeType(valueString, _parameterTypeCode, CultureInfo.InvariantCulture), i);
                    }
 
                    _wrappedParameter = array;
                }
 
                break;
        }
    }
 
    /// <summary>
    /// Serializes or deserializes the value type instance wrapped by this <see cref="TaskParameter"/>.
    /// </summary>
    /// <remarks>
    /// The value type is converted to/from string using the <see cref="Convert"/> class. Note that we require
    /// task parameter types to be <see cref="IConvertible"/> so this conversion is guaranteed to work for parameters
    /// that have made it this far.
    /// </remarks>
    private void TranslateValueType(ITranslator translator)
    {
        string? valueString = null;
 
        if (translator.Mode == TranslationDirection.WriteToStream)
        {
            valueString = (string)Convert.ChangeType(_wrappedParameter, typeof(string), CultureInfo.InvariantCulture);
        }
 
        translator.Translate(ref valueString);
 
        if (translator.Mode == TranslationDirection.ReadFromStream)
        {
            // We don't know how to convert the string back to the original value type. This is fine because output
            // task parameters are anyway converted to strings by the engine (see TaskExecutionHost.GetValueOutputs)
            // and input task parameters of custom value types are not supported.
            _wrappedParameter = valueString;
        }
    }
 
    /// <summary>
    /// Serializes or deserializes the value type array instance wrapped by this <see cref="TaskParameter"/>.
    /// </summary>
    /// <remarks>
    /// The array is assumed to be non-null.
    /// </remarks>
    private void TranslateValueTypeArray(ITranslator translator)
    {
        if (translator.Mode == TranslationDirection.WriteToStream)
        {
            var array = (Array)_wrappedParameter!;
            int length = array.Length;
 
            translator.Translate(ref length);
 
            for (int i = 0; i < length; i++)
            {
                string? valueString = Convert.ToString(array.GetValue(i), CultureInfo.InvariantCulture);
                translator.Translate(ref valueString);
            }
        }
        else
        {
            int length = 0;
            translator.Translate(ref length);
 
            string?[] stringArray = new string[length];
            for (int i = 0; i < length; i++)
            {
                translator.Translate(ref stringArray[i]);
            }
 
            // We don't know how to convert the string array back to the original value type array.
            // This is fine because the engine would eventually convert it to strings anyway.
            _wrappedParameter = stringArray;
        }
    }
 
    /// <summary>
    /// Super simple ITaskItem derivative that we can use as a container for read items.
    /// </summary>
    private class TaskParameterTaskItem : MarshalByRefObject, ITaskItem, ITranslatable
    {
        private string? _escapedItemSpec;
        private string? _escapedDefiningProject;
        private Dictionary<string, string?>? _customEscapedMetadata;
        private string? _fullPath;
 
        internal TaskParameterTaskItem(ITaskItem copyFrom)
        {
            if (copyFrom is TaskParameterTaskItem taskParameterTaskItem)
            {
                _escapedItemSpec = taskParameterTaskItem._escapedItemSpec;
                _escapedDefiningProject = taskParameterTaskItem.GetMetadataValueEscaped(FileUtilities.ItemSpecModifiers.DefiningProjectFullPath);
                _customEscapedMetadata = new(taskParameterTaskItem._customEscapedMetadata);
            }
            else
            {
                // If we don't have ITaskItem2 to fall back on, we have to make do with the fact that
                // CloneCustomMetadata, GetMetadata, & ItemSpec returns unescaped values, and
                // TaskParameterTaskItem's constructor expects escaped values, so escaping them all
                // is the closest approximation to correct we can get.
                _escapedItemSpec = EscapingUtilities.Escape(copyFrom.ItemSpec);
                _escapedDefiningProject = EscapingUtilities.EscapeWithCaching(copyFrom.GetMetadata(FileUtilities.ItemSpecModifiers.DefiningProjectFullPath));
 
                IDictionary customMetadata = copyFrom.CloneCustomMetadata();
                _customEscapedMetadata = new(StringComparer.OrdinalIgnoreCase);
 
                if (customMetadata?.Count > 0)
                {
                    foreach (DictionaryEntry entry in customMetadata)
                    {
                        _customEscapedMetadata[(string)entry.Key] = EscapingUtilities.Escape((string)entry.Value) ?? string.Empty;
                    }
                }
            }
 
            ErrorUtilities.VerifyThrowInternalNull(_escapedItemSpec);
        }
 
        private TaskParameterTaskItem()
        {
        }
 
        /// <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;
 
        /// <summary>
        /// Gets or sets the item "specification" e.g. for disk-based items this would be the file path.
        /// </summary>
        /// <remarks>
        /// This should be named "EvaluatedInclude" but that would be a breaking change to this interface.
        /// </remarks>
        /// <value>The item-spec string.</value>
        public string ItemSpec
        {
            get => _escapedItemSpec is null ? string.Empty : EscapingUtilities.UnescapeAll(_escapedItemSpec);
            set => _escapedItemSpec = value;
        }
 
        /// <summary>
        /// Gets the names of all the metadata on the item.
        /// Includes the built-in metadata like "FullPath".
        /// </summary>
        /// <value>The list of metadata names.</value>
        public ICollection MetadataNames
        {
            get
            {
                List<string> result = _customEscapedMetadata is not null
                    ? [.. _customEscapedMetadata.Keys]
                    : [];
 
                result.AddRange(FileUtilities.ItemSpecModifiers.All);
 
                return result;
            }
        }
 
        /// <summary>
        /// Gets the number of pieces of metadata on the item. Includes
        /// both custom and built-in metadata.  Used only for unit testing.
        /// </summary>
        /// <value>Count of pieces of metadata.</value>
        public int MetadataCount
        {
            get
            {
                int count = _customEscapedMetadata?.Count ?? 0;
                return count + FileUtilities.ItemSpecModifiers.All.Length;
            }
        }
 
        /// <summary>
        /// Allows the values of metadata on the item to be queried.
        /// </summary>
        /// <param name="metadataName">The name of the metadata to retrieve.</param>
        /// <returns>The value of the specified metadata.</returns>
        public string GetMetadata(string metadataName)
        {
            string metadataValue = GetMetadataValueEscaped(metadataName);
            return EscapingUtilities.UnescapeAll(metadataValue);
        }
 
        /// <summary>
        /// Allows a piece of custom metadata to be set on the item.
        /// </summary>
        /// <param name="metadataName">The name of the metadata to set.</param>
        /// <param name="metadataValue">The metadata value.</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),
                SR.Shared_CannotChangeItemSpecModifiers,
                metadataName);
 
            _customEscapedMetadata ??= new(StringComparer.OrdinalIgnoreCase);
            _customEscapedMetadata[metadataName] = metadataValue ?? string.Empty;
        }
 
        /// <summary>
        /// Allows the removal of custom metadata set on the item.
        /// </summary>
        /// <param name="metadataName">The name of the metadata to remove.</param>
        public void RemoveMetadata(string metadataName)
        {
            ErrorUtilities.VerifyThrowArgumentNull(metadataName);
            ErrorUtilities.VerifyThrowArgument(
                !FileUtilities.ItemSpecModifiers.IsItemSpecModifier(metadataName),
                SR.Shared_CannotChangeItemSpecModifiers,
                metadataName);
 
            if (_customEscapedMetadata == null)
            {
                return;
            }
 
            _customEscapedMetadata.Remove(metadataName);
        }
 
        /// <summary>
        /// Allows custom metadata on the item to be copied to another item.
        /// </summary>
        /// <remarks>
        /// RECOMMENDED GUIDELINES FOR METHOD IMPLEMENTATIONS:
        /// 1) this method should NOT copy over the item-spec.
        /// 2) if a particular piece of metadata already exists on the destination item, it should NOT be overwritten.
        /// 3) if there are pieces of metadata on the item that make no semantic sense on the destination item, they should NOT be copied.
        /// </remarks>
        /// <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");
 
            if (_customEscapedMetadata != null)
            {
                foreach (KeyValuePair<string, string?> entry in _customEscapedMetadata)
                {
                    string value = destinationItem.GetMetadata(entry.Key);
 
                    if (string.IsNullOrEmpty(value))
                    {
                        destinationItem.SetMetadata(entry.Key, entry.Value);
                    }
                }
            }
 
            if (string.IsNullOrEmpty(originalItemSpec))
            {
                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>
        /// <returns>Dictionary of cloned metadata.</returns>
        public IDictionary CloneCustomMetadata()
        {
            Dictionary<string, string> clonedMetadata = new(StringComparer.OrdinalIgnoreCase);
 
            if (_customEscapedMetadata != null)
            {
                foreach (KeyValuePair<string, string?> metadatum in _customEscapedMetadata)
                {
                    clonedMetadata.Add(metadatum.Key, EscapingUtilities.UnescapeAll(metadatum.Value!));
                }
            }
 
            return clonedMetadata;
        }
 
        private string 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(
                    currentDirectory: null, _escapedItemSpec!, _escapedDefiningProject, metadataName, ref _fullPath);
            }
            else if (_customEscapedMetadata != null)
            {
                _customEscapedMetadata.TryGetValue(metadataName, out metadataValue);
            }
 
            return metadataValue ?? string.Empty;
        }
 
        public void Translate(ITranslator translator)
        {
            translator.Translate(ref _escapedItemSpec);
            translator.Translate(ref _escapedDefiningProject);
            translator.TranslateDictionary(ref _customEscapedMetadata, StringComparer.OrdinalIgnoreCase);
 
            ErrorUtilities.VerifyThrowInternalNull(_escapedItemSpec);
            ErrorUtilities.VerifyThrowInternalNull(_customEscapedMetadata);
        }
 
        internal static TaskParameterTaskItem FactoryForDeserialization(ITranslator translator)
        {
            var taskItem = new TaskParameterTaskItem();
            taskItem.Translate(translator);
            return taskItem;
        }
    }
}