1 // Copyright (c) Microsoft. All rights reserved.
2 // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3 
4 using System;
5 using System.Xml;
6 using System.Text;
7 using System.Collections;
8 using System.Collections.Generic;
9 using System.Text.RegularExpressions;
10 
11 using Microsoft.Build.BuildEngine.Shared;
12 using Microsoft.Win32;
13 using System.IO;
14 using System.Security;
15 using System.Globalization;
16 using System.Diagnostics;
17 using System.Reflection;
18 
19 namespace Microsoft.Build.BuildEngine
20 {
21     internal class Expander
22     {
23         /// <summary>
24         /// Debugging aid and emergency exit for customers.
25         /// Allows any functions to be used not just the safe list.
26         /// </summary>
27         private static bool enableAllPropertyFunctions = (Environment.GetEnvironmentVariable("MSBUILDENABLEALLPROPERTYFUNCTIONS") == "1");
28 
29         // Items and properties to refer to
30         private ReadOnlyLookup lookup;
31 
32         // If we're only initialized with properties, store them directly
33         // instead of using the overhead of a lookup
34         private BuildPropertyGroup properties;
35 
36         // Table of metadata values.
37         // May have some qualified keys (type.name) or all unqualified.
38         // If all unqualified, the implicitMetadataItemType field indicates the type.
39         private Dictionary<string, string> itemMetadata;
40         private string implicitMetadataItemType;
41 
42         // An optional item definition library to refer to when expanding metadata in expressions
43         private SpecificItemDefinitionLibrary specificItemDefinitionLibrary;
44 
45         private ExpanderOptions options;
46 
47         /// <summary>
48         /// Accessor for the item metadata used for metadata expansion (not counting metadata
49         /// referenced inside a transform).
50         /// </summary>
51         internal Dictionary<string, string> ItemMetadata
52         {
53             get { return itemMetadata; }
54         }
55 
56         #region Constructors
57 
58         /// <summary>
59         /// Special cased constructor. Where we are only going to expand properties,
60         /// it's a waste of memory to use a lookup. Just use the property group.
61         /// PERF: This improves the EvaluateAllPropertyGroups codepath.
62         /// </summary>
Expander(BuildPropertyGroup properties)63         internal Expander(BuildPropertyGroup properties)
64         {
65             this.options = ExpanderOptions.ExpandProperties;
66             this.properties = properties;
67         }
68 
69         /// <summary>
70         /// Special cased constructor. Where we are only going to expand properties and metadata,
71         /// it's a waste of memory to use a lookup. Just use the property group.
72         /// PERF: This improves the EvaluateAllItemDefinitions codepath.
73         /// </summary>
Expander(BuildPropertyGroup properties, string implicitMetadataItemType, Dictionary<string, string> unqualifiedItemMetadata)74         internal Expander(BuildPropertyGroup properties, string implicitMetadataItemType, Dictionary<string, string> unqualifiedItemMetadata)
75         {
76             this.options = ExpanderOptions.ExpandPropertiesAndMetadata;
77             this.properties = properties;
78             this.itemMetadata = unqualifiedItemMetadata;
79             this.implicitMetadataItemType = implicitMetadataItemType;
80         }
81 
82         // Used in many places
Expander(BuildPropertyGroup properties, Hashtable items)83         internal Expander(BuildPropertyGroup properties, Hashtable items)
84             : this(new ReadOnlyLookup(items, properties), null, ExpanderOptions.ExpandPropertiesAndItems)
85         {
86         }
87 
88         // Used by BuildItemGroup.Evaluate
Expander(BuildPropertyGroup properties, Hashtable items, ExpanderOptions options)89         internal Expander(BuildPropertyGroup properties, Hashtable items, ExpanderOptions options)
90             : this(new ReadOnlyLookup(items, properties), null, options)
91         {
92         }
93 
94         // Used by ItemBucket
Expander(ReadOnlyLookup lookup, Dictionary<string, string> itemMetadata)95         internal Expander(ReadOnlyLookup lookup, Dictionary<string, string> itemMetadata)
96             : this(lookup, itemMetadata, ExpanderOptions.ExpandAll)
97         {
98         }
99 
100         // Used by IntrinsicTask
Expander(ReadOnlyLookup lookup)101         internal Expander(ReadOnlyLookup lookup)
102             : this(lookup, null, ExpanderOptions.ExpandPropertiesAndItems)
103         {
104         }
105 
106         // Used by unit tests
Expander(ReadOnlyLookup lookup, Dictionary<string, string> itemMetadata, ExpanderOptions options)107         internal Expander(ReadOnlyLookup lookup, Dictionary<string, string> itemMetadata, ExpanderOptions options)
108         {
109             ErrorUtilities.VerifyThrow(options != ExpanderOptions.Invalid, "Must specify options");
110 
111             this.lookup = lookup;
112             this.itemMetadata = itemMetadata;
113             this.options = options;
114         }
115 
116         /// <summary>
117         /// Create an expander from another expander, but with different
118         /// options
119         /// </summary>
Expander(Expander expander, ExpanderOptions options)120         internal Expander(Expander expander, ExpanderOptions options)
121             : this(expander.lookup, expander.itemMetadata, options)
122         {
123         }
124 
Expander(Expander expander, SpecificItemDefinitionLibrary itemDefinitionLibrary)125         internal Expander(Expander expander, SpecificItemDefinitionLibrary itemDefinitionLibrary)
126             : this(expander.lookup, null , expander.options)
127         {
128             if (implicitMetadataItemType == null)
129             {
130                 this.itemMetadata = expander.itemMetadata;
131             }
132             this.specificItemDefinitionLibrary = itemDefinitionLibrary;
133         }
134 
135 #endregion
136 
137         /// <summary>
138         /// Adds metadata to the table being used by this expander.
139         /// This is useful when expanding metadata definitions that may refer to other values defined
140         /// immediately above: as each value is expanded, it is added to the table in the expander.
141         /// </summary>
SetMetadataInMetadataTable(string itemType, string name, string value)142         internal void SetMetadataInMetadataTable(string itemType, string name, string value)
143         {
144             ErrorUtilities.VerifyThrow((options & ExpanderOptions.ExpandMetadata) != 0, "Must be expanding metadata");
145             ErrorUtilities.VerifyThrow(implicitMetadataItemType == null || String.Equals(implicitMetadataItemType, itemType, StringComparison.OrdinalIgnoreCase), "Unexpected metadata type");
146 
147             if (itemMetadata == null)
148             {
149                 itemMetadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
150                 implicitMetadataItemType = itemType;
151             }
152 
153             if (String.Equals(implicitMetadataItemType, itemType, StringComparison.OrdinalIgnoreCase))
154             {
155                 itemMetadata[name] = value;
156             }
157             else
158             {
159                 itemMetadata[itemType + "." + name] = value;
160             }
161         }
162 
163         /// <summary>
164         /// Expands item metadata, properties, and items (in that order), and produces a list of TaskItems.
165         /// </summary>
166         /// <param name="expression"></param>
167         /// <param name="expressionAttribute"></param>
168         /// <returns></returns>
ExpandAllIntoBuildItems( string expression, XmlAttribute expressionAttribute )169         internal List<BuildItem> ExpandAllIntoBuildItems
170             (
171             string expression,
172             XmlAttribute expressionAttribute
173             )
174         {
175             // We don't know how many items we're going to end up with, but we'll
176             // keep adding them to this arraylist as we find them.
177             List<BuildItem> buildItems = new List<BuildItem>();
178 
179             string evaluatedParameterValue = this.ExpandPropertiesLeaveEscaped(this.ExpandMetadataLeaveEscaped(expression), expressionAttribute);
180 
181             if (evaluatedParameterValue.Length > 0)
182             {
183                 // Take the string that is being passed into the task parameter in the
184                 // project XML file, and split it up by semicolons.  Loop through each
185                 // piece individually.
186                 List<string> userSpecifiedItemExpressions = ExpressionShredder.SplitSemiColonSeparatedList(evaluatedParameterValue);
187                 foreach (string userSpecifiedItemExpression in userSpecifiedItemExpressions)
188                 {
189                     BuildItemGroup itemsToAdd = this.ExpandSingleItemListExpressionIntoItemsLeaveEscaped(userSpecifiedItemExpression, expressionAttribute);
190                     if (itemsToAdd != null)
191                     {
192                         foreach (BuildItem itemToAdd in itemsToAdd)
193                         {
194                             buildItems.Add(itemToAdd);
195                         }
196                     }
197                     else
198                     {
199                         // The expression is not of the form @(itemName).  Therefore, just
200                         // treat it as a string, and create a new TaskItem from that string.
201                         buildItems.Add(new BuildItem(null, userSpecifiedItemExpression));
202                     }
203                 }
204             }
205 
206             return buildItems;
207         }
208 
209         /// <summary>
210         /// Expands item metadata, properties, and items (in that order), and produces a list of TaskItems.
211         ///
212         /// All data accessed through the TaskItem (ItemSpec and metadata) is going to be unescaped, so it's nice
213         /// and ready for a task to consume.
214         /// </summary>
215         /// <param name="expression"></param>
216         /// <param name="expressionAttribute"></param>
217         /// <returns></returns>
218         /// <owner>RGoel</owner>
ExpandAllIntoTaskItems( string expression, XmlAttribute expressionAttribute )219         internal List<TaskItem> ExpandAllIntoTaskItems
220             (
221             string expression,
222             XmlAttribute expressionAttribute
223             )
224         {
225             List<BuildItem> buildItems = ExpandAllIntoBuildItems(expression, expressionAttribute);
226 
227             List<TaskItem> taskItems = new List<TaskItem>(buildItems.Count);
228             for (int i = 0; i < buildItems.Count; i++)
229             {
230                 if (!buildItems[i].IsUninitializedItem)
231                 {
232                     taskItems.Add(new TaskItem(buildItems[i]));
233                 }
234                 else
235                 {
236                     taskItems.Add(new TaskItem(buildItems[i].FinalItemSpecEscaped));
237                 }
238             }
239 
240             return taskItems;
241         }
242 
243         /// <summary>
244         /// An overload of ExpandAllIntoString that conveniently only takes in an XmlAttribute whose
245         /// value we should expand.
246         /// </summary>
247         /// <param name="expressionAttribute"></param>
248         /// <returns></returns>
249         /// <owner>RGoel</owner>
ExpandAllIntoString( XmlAttribute expressionAttribute )250         internal string ExpandAllIntoString
251             (
252             XmlAttribute expressionAttribute
253             )
254         {
255             return this.ExpandAllIntoString(expressionAttribute.Value, expressionAttribute);
256         }
257 
258         /// <summary>
259         /// Expands embedded item metadata, properties, and embedded item lists (in that order)
260         /// within an expression.
261         /// </summary>
262         /// <param name="expression"></param>
263         /// <param name="expressionNode">The XML attribute containing the string we're trying to expand here.  Solely
264         /// for the purposes of providing line/column number information when there's an error.</param>
265         /// <returns>fully expanded string</returns>
266         /// <owner>RGoel</owner>
ExpandAllIntoString( string expression, XmlNode expressionNode )267         internal string ExpandAllIntoString
268             (
269             string expression,
270             XmlNode expressionNode
271             )
272         {
273             return EscapingUtilities.UnescapeAll(this.ExpandAllIntoStringLeaveEscaped(expression, expressionNode));
274         }
275 
276         /// <summary>
277         /// An overload of ExpandAllIntoString that conveniently only takes in an XmlAttribute whose
278         /// value we should expand.
279         /// </summary>
280         /// <param name="expressionAttribute"></param>
281         /// <returns></returns>
282         /// <owner>RGoel</owner>
ExpandAllIntoStringLeaveEscaped( XmlAttribute expressionAttribute )283         internal string ExpandAllIntoStringLeaveEscaped
284             (
285             XmlAttribute expressionAttribute
286             )
287         {
288             return this.ExpandAllIntoStringLeaveEscaped(expressionAttribute.Value, expressionAttribute);
289         }
290 
291         /// <summary>
292         /// Expands embedded item metadata, properties, and embedded item lists (in that order)
293         /// within an expression.
294         /// </summary>
295         /// <param name="expression"></param>
296         /// <param name="expressionNode">The XML attribute containing the string we're trying to expand here.  Solely
297         /// for the purposes of providing line/column number information when there's an error.</param>
298         /// <returns>fully expanded string</returns>
299         /// <owner>RGoel</owner>
ExpandAllIntoStringLeaveEscaped( string expression, XmlNode expressionNode )300         internal string ExpandAllIntoStringLeaveEscaped
301             (
302             string expression,
303             XmlNode expressionNode
304             )
305         {
306             ErrorUtilities.VerifyThrow(expression != null, "Must pass in non-null expression.");
307             if (expression.Length == 0)
308             {
309                 return expression;
310             }
311 
312             return this.ExpandItemsIntoStringLeaveEscaped(this.ExpandPropertiesLeaveEscaped(this.ExpandMetadataLeaveEscaped(expression), expressionNode), expressionNode);
313         }
314 
315         /// <summary>
316         /// Expands metadata, properties, and items (in that order) into a list of strings.
317         /// </summary>
318         /// <param name="expression"></param>
319         /// <param name="expressionNode">The XML attribute containing the string we're trying to expand here.  Solely
320         /// for the purposes of providing line/column number information when there's an error.</param>
321         /// <returns></returns>
322         /// <owner>RGoel</owner>
ExpandAllIntoStringList( string expression, XmlNode expressionNode )323         internal List<string> ExpandAllIntoStringList
324             (
325             string expression,
326             XmlNode expressionNode
327             )
328         {
329             List<string> stringList = ExpressionShredder.SplitSemiColonSeparatedList(ExpandAllIntoStringLeaveEscaped(expression, expressionNode));
330 
331             for (int i = 0; i < stringList.Count; i++)
332             {
333                 stringList[i] = EscapingUtilities.UnescapeAll(stringList[i]);
334             }
335 
336             return stringList;
337         }
338 
339         /// <summary>
340         /// Expands metadata, properties, and items (in that order) into a list of strings.
341         /// </summary>
342         /// <param name="expressionAttribute"></param>
343         /// <returns></returns>
344         /// <owner>RGoel</owner>
ExpandAllIntoStringList( XmlAttribute expressionAttribute )345         internal List<string> ExpandAllIntoStringList
346             (
347             XmlAttribute expressionAttribute
348             )
349         {
350             return this.ExpandAllIntoStringList(expressionAttribute.Value, expressionAttribute);
351         }
352 
353         /// <summary>
354         /// Expands metadata, properties, and items (in that order) into a list of strings.
355         /// </summary>
356         /// <param name="expression"></param>
357         /// <param name="expressionNode">The XML attribute containing the string we're trying to expand here.  Solely
358         /// for the purposes of providing line/column number information when there's an error.</param>
359         /// <returns></returns>
360         /// <owner>RGoel</owner>
ExpandAllIntoStringListLeaveEscaped( string expression, XmlNode expressionNode )361         internal List<string> ExpandAllIntoStringListLeaveEscaped
362             (
363             string expression,
364             XmlNode expressionNode
365             )
366         {
367             return ExpressionShredder.SplitSemiColonSeparatedList(ExpandAllIntoStringLeaveEscaped(expression, expressionNode));
368         }
369 
370         /// <summary>
371         /// Expands metadata, properties, and items (in that order) into a list of strings.
372         /// </summary>
373         /// <param name="expressionAttribute"></param>
374         /// <returns></returns>
375         /// <owner>RGoel</owner>
ExpandAllIntoStringListLeaveEscaped( XmlAttribute expressionAttribute )376         internal List<string> ExpandAllIntoStringListLeaveEscaped
377             (
378             XmlAttribute expressionAttribute
379             )
380         {
381             return ExpandAllIntoStringListLeaveEscaped(expressionAttribute.Value, expressionAttribute);
382         }
383 
384         /// <summary>
385         /// This method takes a string which may contain any number of
386         /// "$(propertyname)" tags in it.  It replaces all those tags with
387         /// the actual property values, and returns a new string.  For example,
388         ///
389         ///     string processedString =
390         ///         propertyBag.ExpandProperties("Value of NoLogo is $(NoLogo).");
391         ///
392         /// This code might produce:
393         ///
394         ///     processedString = "Value of NoLogo is true."
395         ///
396         /// If the sourceString contains an embedded property which doesn't
397         /// have a value, then we replace that tag with an empty string.
398         ///
399         /// This method leaves the expression escaped.  Callers may need to unescape on their own as appropriate.
400         /// This method leaves the result escaped.  Callers may need to unescape on their own as appropriate.
401         /// </summary>
ExpandPropertiesLeaveEscaped( string sourceString, XmlNode sourceNode )402         internal string ExpandPropertiesLeaveEscaped
403         (
404             string sourceString,
405             XmlNode sourceNode
406         )
407         {
408             return ConvertToString(ExpandPropertiesLeaveTypedAndEscaped(sourceString, sourceNode));
409         }
410 
411         /// <summary>
412         /// This method takes a string which may contain any number of
413         /// "$(propertyname)" tags in it.  It replaces all those tags with
414         /// the actual property values, and returns a new string.  For example,
415         ///
416         ///     string processedString =
417         ///         propertyBag.ExpandProperties("Value of NoLogo is $(NoLogo).");
418         ///
419         /// This code might produce:
420         ///
421         ///     processedString = "Value of NoLogo is true."
422         ///
423         /// If the sourceString contains an embedded property which doesn't
424         /// have a value, then we replace that tag with an empty string.
425         ///
426         /// This method leaves the expression escaped.  Callers may need to unescape on their own as appropriate.
427         /// </summary>
428         /// <param name="expression"></param>
429         /// <returns></returns>
430         /// <owner>RGoel, JomoF</owner>
ExpandPropertiesLeaveTypedAndEscaped( string expression, XmlNode expressionNode )431         private object ExpandPropertiesLeaveTypedAndEscaped
432         (
433             string expression,
434             XmlNode expressionNode
435         )
436         {
437             if (((options & ExpanderOptions.ExpandProperties) != ExpanderOptions.ExpandProperties) || String.IsNullOrEmpty(expression))
438             {
439                 return expression;
440             }
441 
442             // These are also zero-based indices into the sourceString, but
443             // these tell us where the current property tag begins and ends.
444             int propertyStartIndex, propertyEndIndex;
445 
446             // If there are no substitutions, then just return the string.
447             propertyStartIndex = expression.IndexOf("$(", StringComparison.Ordinal);
448             if (propertyStartIndex == -1)
449             {
450                 return expression;
451             }
452 
453             // We will build our set of results as object components
454             // so that we can either maintain the object's type in the event
455             // that we have a single component, or convert to a string
456             // if concatenation is required.
457             List<object> results = new List<object>();
458 
459             // The sourceIndex is the zero-based index into the sourceString,
460             // where we've essentially read up to and copied into the target string.
461             int sourceIndex = 0;
462 
463             // Search for "$(" in the sourceString.  Loop until we don't find it
464             // any more.
465             while (propertyStartIndex != -1)
466             {
467                 bool tryExtractPropertyFunction = false;
468                 bool tryExtractRegistryFunction = false;
469 
470                 // Append the targetString with the portion of the sourceString up to
471                 // (but not including) the "$(", and advance the sourceIndex pointer.
472                 if (propertyStartIndex - sourceIndex > 0)
473                 {
474                     results.Add(expression.Substring(sourceIndex, propertyStartIndex - sourceIndex));
475                 }
476                 sourceIndex = propertyStartIndex;
477 
478                 // Following the "$(" we need to locate the matching ')'
479                 // Scan for the matching closing bracket, skipping any nested ones
480                 // This is a very complete, fast validation of parenthesis matching including for nested
481                 // function calls.
482                 propertyEndIndex = ScanForClosingParenthesis(expression, propertyStartIndex + 2, out tryExtractPropertyFunction, out tryExtractRegistryFunction);
483 
484                 if (propertyEndIndex == -1)
485                 {
486                     // If we didn't find the closing parenthesis, that means this
487                     // isn't really a well-formed property tag.  Just literally
488                     // copy the remainder of the sourceString (starting with the "$("
489                     // that we found) into the targetString, and quit.
490                     results.Add(expression.Substring(propertyStartIndex, expression.Length - propertyStartIndex));
491                     sourceIndex = expression.Length;
492                 }
493                 else
494                 {
495                     // Aha, we found the closing parenthesis.  All the stuff in
496                     // between the "$(" and the ")" constitutes the property body.
497                     // Note: Current propertyStartIndex points to the "$", and
498                     // propertyEndIndex points to the ")".  That's why we have to
499                     // add 2 for the start of the substring, and subtract 2 for
500                     // the length.
501                     string propertyBody = expression.Substring(propertyStartIndex + 2, propertyEndIndex - propertyStartIndex - 2);
502 
503                     // A property value of null will indicate that we're calling a static function on a type
504                     object propertyValue = null;
505 
506                     // Compat: $() should return String.Empty
507                     if (propertyBody.Length == 0)
508                     {
509                         propertyValue = String.Empty;
510                     }
511                     else if (tryExtractRegistryFunction && propertyBody.StartsWith("Registry:", StringComparison.OrdinalIgnoreCase))
512                     {
513                         // This is a registry reference, like $(Registry:HKEY_LOCAL_MACHINE\Software\Vendor\Tools@TaskLocation)
514                         propertyValue = ExpandRegistryValue(propertyBody, null);
515                     }
516                     else if (tryExtractPropertyFunction)
517                     {
518                         // This is either a regular property or a function expression
519                         propertyValue = ExpandPropertyBody(propertyBody, propertyValue, properties, options);
520                     }
521                     else // This is a regular property
522                     {
523                         propertyValue = LookupProperty(properties, propertyBody, expressionNode);
524                     }
525 
526                     // If it's a property function result, it may return null, so check before we add it.
527                     if (propertyValue != null)
528                     {
529                         // Append the property value to our targetString, and advance
530                         // our sourceIndex pointer to the character just after the closing
531                         // parenthesis.
532                         results.Add(propertyValue);
533                     }
534                     sourceIndex = propertyEndIndex + 1;
535                 }
536 
537                 propertyStartIndex = expression.IndexOf("$(", sourceIndex, StringComparison.Ordinal);
538             }
539             // If we have only a single result, then just return it
540             if (results.Count == 1 && expression.Length == sourceIndex)
541             {
542                 return results[0];
543             }
544             else
545             {
546                 // We have more than one result collected, therefore we need to concatenate
547                 // into the final result string. This does mean that we will lose type information.
548                 // However since the user wanted contatenation, then they clearly wanted that to happen.
549 
550                 // Initialize our output string to empty string.
551                 // PERF: This method is called very often - of the order of 3,000 times per project.
552                 // StringBuilder by default is initialized with a 16 char string and doubles the length
553                 // whenever it's too short. We want to avoid reallocation but also avoid excessive allocation.
554                 // The length of the source string turns out to be a fair compromise. (The final result may
555                 // be longer or it may be shorter.)
556                 StringBuilder result = new StringBuilder(expression.Length);
557 
558                 // We couldn't find anymore property tags in the expression,
559                 // so just literally copy the remainder into the result
560                 // and return.
561                 if (expression.Length - sourceIndex > 0)
562                 {
563                     results.Add(expression.Substring(sourceIndex, expression.Length - sourceIndex));
564                 }
565 
566                 // Create a combined result string from the result components that we've gathered
567                 foreach (object component in results)
568                 {
569                     result.Append(component.ToString());
570                 }
571 
572                 return result.ToString();
573             }
574         }
575 
576         /// <summary>
577         /// Convert the object into an MSBuild friendly string
578         /// Arrays are supported.
579         /// </summary>
ConvertToString(object valueToConvert)580         private static string ConvertToString(object valueToConvert)
581         {
582             if (valueToConvert != null)
583             {
584                 Type valueType = valueToConvert.GetType();
585                 string convertedString;
586 
587                 // If the type is a string, then there is nothing to do
588                 if (valueType == typeof(string))
589                 {
590                     convertedString = (string)valueToConvert;
591                 }
592                 else if (valueToConvert is IDictionary)
593                 {
594                     // If the return type is an IDictionary, then we convert this to
595                     // a semi-colon delimited set of A=B pairs.
596                     // Key and Value are converted to string and escaped
597                     IDictionary dictionary = valueToConvert as IDictionary;
598                     StringBuilder builder = new StringBuilder();
599 
600                     foreach (DictionaryEntry entry in dictionary)
601                     {
602                         if (builder.Length > 0)
603                         {
604                             builder.Append(';');
605                         }
606 
607                         // convert and escape each key and value in the dictionary entry
608                         builder.Append(EscapingUtilities.Escape(ConvertToString(entry.Key)));
609                         builder.Append('=');
610                         builder.Append(EscapingUtilities.Escape(ConvertToString(entry.Value)));
611                     }
612 
613                     convertedString = builder.ToString();
614                 }
615                 else if (valueToConvert is IEnumerable)
616                 {
617                     // If the return is enumerable, then we'll convert to semi-colon delimted elements
618                     // each of which must be converted, so we'll recurse for each element
619                     StringBuilder builder = new StringBuilder();
620 
621                     IEnumerable enumerable = (IEnumerable)valueToConvert;
622 
623                     foreach (object element in enumerable)
624                     {
625                         if (builder.Length > 0)
626                         {
627                             builder.Append(';');
628                         }
629 
630                         // we need to convert and escape each element of the array
631                         builder.Append(EscapingUtilities.Escape(ConvertToString(element)));
632                     }
633 
634                     convertedString = builder.ToString();
635                 }
636                 else
637                 {
638                     // The fall back is always to just convert to a string directly.
639                     convertedString = valueToConvert.ToString();
640                 }
641 
642                 return convertedString;
643             }
644             else
645             {
646                 return String.Empty;
647             }
648         }
649 
650         /// <summary>
651         /// Scan for the closing bracket that matches the one we've already skipped;
652         /// essentially, pushes and pops on a stack of parentheses to do this.
653         /// Takes the expression and the index to start at.
654         /// Returns the index of the matching parenthesis, or -1 if it was not found.
655         /// </summary>
ScanForClosingParenthesis(string expression, int index)656         private static int ScanForClosingParenthesis(string expression, int index)
657         {
658             bool potentialPropertyFunction = false;
659             bool potentialRegistryFunction = false;
660             return ScanForClosingParenthesis(expression, index, out potentialPropertyFunction, out potentialRegistryFunction);
661         }
662 
663         /// <summary>
664         /// Scan for the closing bracket that matches the one we've already skipped;
665         /// essentially, pushes and pops on a stack of parentheses to do this.
666         /// Takes the expression and the index to start at.
667         /// Returns the index of the matching parenthesis, or -1 if it was not found.
668         /// Also returns flags to indicate if a propertyfunction or registry property is likely
669         /// to be found in the expression
670         /// </summary>
ScanForClosingParenthesis(string expression, int index, out bool potentialPropertyFunction, out bool potentialRegistryFunction)671         private static int ScanForClosingParenthesis(string expression, int index, out bool potentialPropertyFunction, out bool potentialRegistryFunction)
672         {
673             int nestLevel = 1;
674             int length = expression.Length;
675 
676             potentialPropertyFunction = false;
677             potentialRegistryFunction = false;
678 
679             // Scan for our closing ')'
680             while (index < length && nestLevel > 0)
681             {
682                 char character = expression[index];
683 
684                 if (character == '(')
685                 {
686                     nestLevel++;
687                 }
688                 else if (character == ')')
689                 {
690                     nestLevel--;
691                 }
692                 else if (character == '.' || character == '[' || character == '$')
693                 {
694                     potentialPropertyFunction = true;
695                 }
696                 else if (character == ':')
697                 {
698                     potentialRegistryFunction = true;
699                 }
700 
701                 index++;
702             }
703 
704             // We will have parsed past the ')', so step back one character
705             index--;
706 
707             return (nestLevel == 0) ? index : -1;
708         }
709 
710         /// <summary>
711         /// Expand the body of the property, including any functions that it may contain
712         /// </summary>
ExpandPropertyBody(string propertyBody, object propertyValue, BuildPropertyGroup properties, ExpanderOptions options)713         private object ExpandPropertyBody(string propertyBody, object propertyValue, BuildPropertyGroup properties, ExpanderOptions options)
714         {
715             Function function = null;
716             string propertyName = propertyBody;
717 
718             // Trim the body for comatibility reasons:
719             // Spaces are not valid property name chars, but $( Foo ) is allowed, and should always expand to BLANK.
720             // Do a very fast check for leading and trailing whitespace, and trim them from the property body if we have any.
721             // But we will do a property name lookup on the propertyName that we held onto.
722             if (Char.IsWhiteSpace(propertyBody[0]) || Char.IsWhiteSpace(propertyBody[propertyBody.Length - 1]))
723             {
724                 propertyBody = propertyBody.Trim();
725             }
726 
727             // If we don't have a clean propertybody then we'll do deeper checks to see
728             // if what we have is a function
729             if (!IsValidPropertyName(propertyBody))
730             {
731                 if (propertyBody.Contains(".") || propertyBody[0] == '[')
732                 {
733                     // This is a function
734                     function = Function.ExtractPropertyFunction(propertyBody, propertyValue);
735 
736                     // We may not have been able to parse out a function
737                     if (function != null)
738                     {
739                         // We will have either extracted the actual property name
740                         // or realised that there is none (static function), and have recorded a null
741                         propertyName = function.ExpressionRootName;
742                     }
743                     else
744                     {
745                         // In the event that we have been handed an unrecognized property body, throw
746                         // an invalid function property exception.
747                         ProjectErrorUtilities.ThrowInvalidProject(null, "InvalidFunctionPropertyExpression", propertyBody, String.Empty);
748                         return null;
749                     }
750                 }
751                 else
752                 {
753                     // In the event that we have been handed an unrecognized property body, throw
754                     // an invalid function property exception.
755                     ProjectErrorUtilities.ThrowInvalidProject(null, "InvalidFunctionPropertyExpression", propertyBody, String.Empty);
756                     return null;
757                 }
758             }
759 
760             // Find the property value in our property collection.  This
761             // will automatically return "" (empty string) if the property
762             // doesn't exist in the collection, and we're not executing a static function
763             if (!String.IsNullOrEmpty(propertyName))
764             {
765                 BuildProperty property;
766                 if (lookup != null)
767                 {
768                     // We're using a lookup
769                     property = lookup.GetProperty(propertyName);
770                 }
771                 else
772                 {
773                     // We're only using a property group
774                     property = properties[propertyName];
775                 }
776 
777                 if (property == null)
778                 {
779                     propertyValue = String.Empty;
780                 }
781                 else
782                 {
783                     propertyValue = property.FinalValueEscaped;
784                 }
785             }
786 
787             if (function != null)
788             {
789                 // Because of the rich expansion capabilities of MSBuild, we need to keep things
790                 // as strings, since property expansion & string embedding can happen anywhere
791                 // propertyValue can be null here, when we're invoking a static function
792                 propertyValue = function.Execute(this, propertyValue, properties, options);
793             }
794 
795             return propertyValue;
796         }
797 
798         /// <summary>
799         /// Look up a simple property reference by the name of the property, e.g. "Foo" when expanding $(Foo)
800         /// </summary>
LookupProperty(BuildPropertyGroup properties, string propertyName, XmlNode expressionNode)801         private object LookupProperty(BuildPropertyGroup properties, string propertyName, XmlNode expressionNode)
802         {
803             // Regular property
804             BuildProperty property;
805             object propertyValue;
806 
807             if (lookup != null)
808             {
809                 // We're using a lookup
810                 property = lookup.GetProperty(propertyName);
811             }
812             else
813             {
814                 // We're only using a property group
815                 property = properties[propertyName];
816             }
817 
818             if (property == null)
819             {
820                 propertyValue = String.Empty;
821 
822                 // Support at least $(MSBuildThisFile)
823                 if (expressionNode != null && String.Equals(propertyName, "MSBuildThisFile", StringComparison.OrdinalIgnoreCase))
824                 {
825                     string thisFile = XmlUtilities.GetXmlNodeFile(expressionNode, String.Empty /* default */);
826 
827                     if (!String.IsNullOrEmpty(thisFile))
828                     {
829                         propertyValue = Path.GetFileName(thisFile);
830                     }
831                 }
832             }
833             else
834             {
835                 propertyValue = property.FinalValueEscaped;
836             }
837 
838             return propertyValue;
839         }
840 
841         /// <summary>
842         /// Returns true if the supplied string contains a valid property name
843         /// </summary>
IsValidPropertyName(string propertyName)844         private static bool IsValidPropertyName(string propertyName)
845         {
846             if (propertyName.Length == 0 || !XmlUtilities.IsValidInitialElementNameCharacter(propertyName[0]))
847             {
848                 return false;
849             }
850 
851             for (int n = 1; n < propertyName.Length; n++)
852             {
853                 if (!XmlUtilities.IsValidSubsequentElementNameCharacter(propertyName[n]))
854                 {
855                     return false;
856                 }
857             }
858 
859             return true;
860         }
861 
862         /// <summary>
863         /// Given a string like "Registry:HKEY_LOCAL_MACHINE\Software\Vendor\Tools@TaskLocation", return the value at that location
864         /// in the registry. If the value isn't found, returns String.Empty.
865         /// Properties may refer to a registry location by using the syntax for example
866         /// "$(Registry:HKEY_LOCAL_MACHINE\Software\Vendor\Tools@TaskLocation)", where "HKEY_LOCAL_MACHINE\Software\Vendor\Tools" is the key and
867         /// "TaskLocation" is the name of the value.  The name of the value and the preceding "@" may be omitted if
868         /// the default value is desired.
869         /// </summary>
870         /// <param name="registryLocation">Expression to expand, eg "Registry:HKEY_LOCAL_MACHINE\Software\Vendor\Tools@TaskLocation"</param>
871         /// <param name="node">Location associated with the expression, for purposes of good error messages</param>
872         /// <returns></returns>
ExpandRegistryValue(string registryExpression, XmlNode node)873         private string ExpandRegistryValue(string registryExpression, XmlNode node)
874         {
875             string registryLocation = registryExpression.Substring(9);
876 
877             // Split off the value name -- the part after the "@" sign. If there's no "@" sign, then it's the default value name
878             // we want.
879             int firstAtSignOffset = registryLocation.IndexOf('@');
880             int lastAtSignOffset = registryLocation.LastIndexOf('@');
881 
882             ProjectErrorUtilities.VerifyThrowInvalidProject(firstAtSignOffset == lastAtSignOffset, node, "InvalidRegistryPropertyExpression", "$(" + registryExpression + ")", String.Empty);
883 
884             string valueName = lastAtSignOffset == -1 || lastAtSignOffset == registryLocation.Length - 1
885                 ? null : registryLocation.Substring(lastAtSignOffset + 1);
886 
887             // If there's no '@', or '@' is first, then we'll use null or String.Empty for the location; otherwise
888             // the location is the part before the '@'
889             string registryKeyName = lastAtSignOffset != -1 ? registryLocation.Substring(0, lastAtSignOffset) : registryLocation;
890 
891             string result = String.Empty;
892             if (registryKeyName != null)
893             {
894                 // We rely on the '@' character to delimit the key and its value, but the registry
895                 // allows this character to be used in the names of keys and the names of values.
896                 // Hence we use our standard escaping mechanism to allow users to access such keys
897                 // and values.
898                 registryKeyName = EscapingUtilities.UnescapeAll(registryKeyName);
899 
900                 if (valueName != null)
901                 {
902                     valueName = EscapingUtilities.UnescapeAll(valueName);
903                 }
904 
905                 try
906                 {
907                     object valueFromRegistry = Registry.GetValue(registryKeyName,
908                                                                  valueName,
909                                                                  null /* default if key or value name is not found */);
910                     if (null != valueFromRegistry)
911                     {
912                         // Convert the result to a string that is reasonable for MSBuild
913                         result = ConvertToString(valueFromRegistry);
914                     }
915                     else
916                     {
917                         // This means either the key or value was not found in the registry.  In this case,
918                         // we simply expand the property value to String.Empty to imitate the behavior of
919                         // normal properties.
920                         result = String.Empty;
921                     }
922                 }
923                 catch (ArgumentException ex)
924                 {
925                     ProjectErrorUtilities.VerifyThrowInvalidProject(false, node, "InvalidRegistryPropertyExpression", "$(" + registryExpression + ")", ex.Message);
926                 }
927                 catch (IOException ex)
928                 {
929                     ProjectErrorUtilities.VerifyThrowInvalidProject(false, node, "InvalidRegistryPropertyExpression", "$(" + registryExpression + ")", ex.Message);
930                 }
931                 catch (SecurityException ex)
932                 {
933                     ProjectErrorUtilities.VerifyThrowInvalidProject(false, node, "InvalidRegistryPropertyExpression", "$(" + registryExpression + ")", ex.Message);
934                 }
935             }
936 
937             return result;
938         }
939 
940         /// <summary>
941         /// This class represents the function as extracted from a property expression
942         /// It is also responsible for executing the function
943         /// </summary>
944         private class Function
945         {
946             /// <summary>
947             /// The type that this function will act on
948             /// </summary>
949             private Type objectType;
950 
951             /// <summary>
952             /// The name of the function
953             /// </summary>
954             private string name;
955 
956             /// <summary>
957             /// The arguments for the function
958             /// </summary>
959             private string[] arguments;
960 
961             /// <summary>
962             /// The expression that constitutes this function
963             /// </summary>
964             private string expression;
965 
966             /// <summary>
967             /// The property name that is the context for this function
968             /// </summary>
969             private string expressionRootName;
970 
971             /// <summary>
972             /// The binding flags that will be used during invocation of this function
973             /// </summary>
974             private BindingFlags bindingFlags;
975 
976             /// <summary>
977             /// The remainder of the body once the function and arguments have been extracted
978             /// </summary>
979             private string remainder;
980 
981             /// <summary>
982             /// Construct a function that will be executed during property evaluation
983             /// </summary>
Function(Type objectType, string expression, string expressionRootName, string name, string[] arguments, BindingFlags bindingFlags, string remainder)984             public Function(Type objectType, string expression, string expressionRootName, string name, string[] arguments, BindingFlags bindingFlags, string remainder)
985             {
986                 this.name = name;
987                 this.arguments = arguments;
988                 this.expressionRootName = expressionRootName;
989                 this.expression = expression;
990                 this.objectType = objectType;
991                 this.bindingFlags = bindingFlags;
992                 this.remainder = remainder;
993             }
994 
995             /// <summary>
996             /// Part of the extraction may result in the name of the property
997             /// This accessor is used by the Expander
998             /// Examples of expression root:
999             ///     [System.Diagnostics.Process]::Start
1000             ///     SomeMSBuildProperty
1001             /// </summary>
1002             public string ExpressionRootName
1003             {
1004                 get { return expressionRootName; }
1005             }
1006 
1007             /// <summary>
1008             /// The type of the instance on which this function acts
1009             /// </summary>
1010             public Type ObjectType
1011             {
1012                 get { return objectType; }
1013             }
1014 
1015             /// <summary>
1016             /// Extract the function details from the given property function expression
1017             /// </summary>
ExtractPropertyFunction(string expressionFunction, object propertyValue)1018             public static Function ExtractPropertyFunction(string expressionFunction, object propertyValue)
1019             {
1020                 // If this a expression function rather than a static, then we'll capture the name of the property referenced
1021                 string propertyName = null;
1022 
1023                 // The type of the object that this function is part
1024                 Type objectType = null;
1025 
1026                 // By default the expression root is the whole function expression
1027                 string expressionRoot = expressionFunction;
1028 
1029                 // The arguments for this function start at the first '('
1030                 // If there are no arguments, then we're a property getter
1031                 int argumentStartIndex = expressionFunction.IndexOf('(');
1032 
1033                 // If we have arguments, then we only want the content up to but not including the '('
1034                 if (argumentStartIndex > -1)
1035                 {
1036                     expressionRoot = expressionFunction.Substring(0, argumentStartIndex);
1037                 }
1038 
1039                 // We ended up with something we don't understand
1040                 ProjectErrorUtilities.VerifyThrowInvalidProject(!String.IsNullOrEmpty(expressionRoot), null, "InvalidFunctionPropertyExpression", expressionFunction, String.Empty);
1041 
1042                 // First we'll see if there is a static function being called
1043                 // A static method is the content that follows the last "::", the rest being
1044                 // the type
1045                 int methodStartIndex = -1;
1046 
1047                 // This is a static method call
1048                 if (expressionRoot[0] == '[')
1049                 {
1050                     int typeEndIndex = expressionRoot.IndexOf(']', 1);
1051 
1052                     if (typeEndIndex < 1)
1053                     {
1054                         // We ended up with something other than a function expression
1055                         ProjectErrorUtilities.ThrowInvalidProject(null, "InvalidFunctionStaticMethodSyntax", "$(" + expressionFunction + ")");
1056                     }
1057 
1058                     string typeName = expressionRoot.Substring(1, typeEndIndex - 1);
1059                     methodStartIndex = typeEndIndex + 1;
1060 
1061                     // Make an attempt to locate a type that matches the body of the expression.
1062                     // We won't throw on error here
1063                     objectType = GetTypeForStaticMethod(typeName);
1064 
1065                     if (objectType == null)
1066                     {
1067                         // We ended up with something other than a type
1068                         ProjectErrorUtilities.ThrowInvalidProject(null, "InvalidFunctionStaticMethodSyntax", "$(" + expressionFunction + ")");
1069                     }
1070 
1071                     if (expressionRoot.Length > methodStartIndex + 2 && expressionRoot[methodStartIndex] == ':' && expressionRoot[methodStartIndex + 1] == ':')
1072                     {
1073                         // skip over the "::"
1074                         methodStartIndex += 2;
1075                     }
1076                     else
1077                     {
1078                         // We ended up with something other than a static function expression
1079                         ProjectErrorUtilities.ThrowInvalidProject(null, "InvalidFunctionStaticMethodSyntax", "$(" + expressionFunction + ")");
1080                     }
1081                 }
1082                 else
1083                 {
1084                     // No static function call was found, look for an instance function call next, such as in SomeStuff.ToLower()
1085                     methodStartIndex = expressionRoot.IndexOf('.');
1086                     if (methodStartIndex == -1)
1087                     {
1088                         // We don't have a function invocation in the expression root, return null
1089                         return null;
1090                     }
1091                     else
1092                     {
1093                         // skip over the '.';
1094                         methodStartIndex++;
1095                     }
1096                 }
1097 
1098                 // No type matched, therefore the content must be a property reference, or a recursive call as functions
1099                 // are chained together
1100                 if (objectType == null)
1101                 {
1102                     int rootEndIndex = expressionRoot.IndexOf('.');
1103                     propertyName = expressionRoot.Substring(0, rootEndIndex);
1104 
1105                     // If propertyValue is null (we're not recursing), then we're expecting a valid property name
1106                     if (propertyValue == null && !IsValidPropertyName(propertyName))
1107                     {
1108                         // We extracted something that wasn't a valid property name, fail.
1109                         ProjectErrorUtilities.ThrowInvalidProject(null, "InvalidFunctionPropertyExpression", expressionFunction, String.Empty);
1110                     }
1111 
1112                     objectType = typeof(string);
1113                 }
1114 
1115                 // If we are recursively acting on a type that has been already produced
1116                 // then pass that type inwards
1117                 if (propertyValue != null)
1118                 {
1119                     objectType = propertyValue.GetType();
1120                 }
1121 
1122                 Function function = ConstructFunction(expressionFunction, propertyName, objectType, argumentStartIndex, methodStartIndex);
1123 
1124                 return function;
1125             }
1126 
1127             /// <summary>
1128             /// Execute the function on the given instance
1129             /// </summary>
Execute(Expander expander, object objectInstance, BuildPropertyGroup properties, ExpanderOptions options)1130             public object Execute(Expander expander, object objectInstance, BuildPropertyGroup properties, ExpanderOptions options)
1131             {
1132                 object functionResult = String.Empty;
1133 
1134                 object[] args = null;
1135 
1136                 try
1137                 {
1138                     // If there is no object instance, then the method invocation will be a static
1139                     if (objectInstance == null)
1140                     {
1141                         // Check that the function that we're going to call is valid to call
1142                         if (!IsStaticMethodAvailable(ObjectType, name))
1143                         {
1144                             ProjectErrorUtilities.ThrowInvalidProject(null, "InvalidFunctionMethodUnavailable", name, ObjectType.FullName);
1145                         }
1146 
1147                         bindingFlags |= BindingFlags.Static;
1148 
1149                         // For our intrinsic function we need to support calling of internal methods
1150                         // since we don't want them to be public
1151                         if (objectType == typeof(Microsoft.Build.BuildEngine.IntrinsicFunctions))
1152                         {
1153                             bindingFlags |= BindingFlags.NonPublic;
1154                         }
1155                     }
1156                     else
1157                     {
1158                         bindingFlags |= BindingFlags.Instance;
1159                     }
1160 
1161                     // We have a methodinfo match, need to plug in the arguments
1162 
1163                     args = new object[arguments.Length];
1164 
1165                     // Assemble our arguments ready for passing to our method
1166                     for (int n = 0; n < arguments.Length; n++)
1167                     {
1168                         object argument = expander.ExpandPropertiesLeaveTypedAndEscaped(this.arguments[n], null);
1169                         string argumentValue = argument as string;
1170 
1171                         if (argumentValue != null)
1172                         {
1173                             // remove our 'quotes' from the escaped string, leaving escaped quotes intact
1174                             args[n] = EscapingUtilities.UnescapeAll(argumentValue.Trim('`', '"', '\''));
1175                         }
1176                         else
1177                         {
1178                             args[n] = argument;
1179                         }
1180                     }
1181 
1182                     // Handle special cases where the object type needs to affect the choice of method
1183                     // The default binder and method invoke, often chooses the incorrect Equals and CompareTo and
1184                     // fails the comparison, because what we have on the right is generally a string.
1185                     // This special casing is to realize that its a comparison that is taking place and handle the
1186                     // argument type coercion accordingly; effectively pre-preparing the argument type so
1187                     // that it matches the left hand side ready for the default binder�s method invoke.
1188                     if (objectInstance != null && args.Length == 1 && (String.Equals("Equals", this.name, StringComparison.OrdinalIgnoreCase) || String.Equals("CompareTo", this.name, StringComparison.OrdinalIgnoreCase)))
1189                     {
1190                         // change the type of the final unescaped string into the destination
1191                         args[0] = Convert.ChangeType(args[0], objectInstance.GetType(), CultureInfo.InvariantCulture);
1192                     }
1193 
1194                     // If we've been asked for and instance to be constructed, then we
1195                     // need to locate an appropriate constructor and invoke it
1196                     if (String.Equals("new", this.name, StringComparison.OrdinalIgnoreCase))
1197                     {
1198                         functionResult = LateBindExecute(null /* no previous exception */, BindingFlags.Public | BindingFlags.Instance, null /* no instance for a constructor */, args, true /* is constructor */);
1199                     }
1200                     else
1201                     {
1202                         // Execute the function given converted arguments
1203                         // The only exception that we should catch to try a late bind here is missing method
1204                         // otherwise there is the potential of running a function twice!
1205                         try
1206                         {
1207                             // First use InvokeMember using the standard binder - this will match and coerce as needed
1208                             functionResult = objectType.InvokeMember(this.name, bindingFlags, Type.DefaultBinder, objectInstance, args, CultureInfo.InvariantCulture);
1209                         }
1210                         catch (MissingMethodException ex) // Don't catch and retry on any other exception
1211                         {
1212                             // If we're invoking a method, then there are deeper attempts that
1213                             // can be made to invoke the method
1214                             if ((bindingFlags & BindingFlags.InvokeMethod) == BindingFlags.InvokeMethod)
1215                             {
1216                                 // The standard binder failed, so do our best to coerce types into the arguments for the function
1217                                 // This may happen if the types need coercion, but it may also happen if the object represents a type that contains open type parameters, that is, ContainsGenericParameters returns true.
1218                                 functionResult = LateBindExecute(ex, bindingFlags, objectInstance, args, false /* is not constructor */);
1219                             }
1220                             else
1221                             {
1222                                 // We were asked to get a property or field, and we found that we cannot
1223                                 // locate it. Since there is no further argument coersion possible
1224                                 // we'll throw right now.
1225                                 throw;
1226                             }
1227                         }
1228                     }
1229 
1230                     // If the result of the function call is a string, then we need to escape the result
1231                     // so that we maintain the "engine contains escaped data" state.
1232                     // The exception is that the user is explicitly calling MSBuild::Unescape or MSBuild::Escape
1233                     if (functionResult is string && !String.Equals("Unescape", name, StringComparison.OrdinalIgnoreCase) && !String.Equals("Escape", name, StringComparison.OrdinalIgnoreCase))
1234                     {
1235                         functionResult = EscapingUtilities.Escape((string)functionResult);
1236                     }
1237 
1238                     // There's nothing left to deal within the function expression, return the result from the execution
1239                     if (String.IsNullOrEmpty(remainder))
1240                     {
1241                         return functionResult;
1242                     }
1243 
1244                     // Recursively expand the remaining property body after execution
1245                     return expander.ExpandPropertyBody(remainder, functionResult, properties, options);
1246                 }
1247                 // Exceptions coming from the actual function called are wrapped in a TargetInvocationException
1248                 catch (TargetInvocationException ex)
1249                 {
1250                     // We ended up with something other than a function expression
1251                     string partiallyEvaluated = GenerateStringOfMethodExecuted(expression, objectInstance, name, args);
1252                     ProjectErrorUtilities.ThrowInvalidProject(null, "InvalidFunctionPropertyExpression", partiallyEvaluated, ex.InnerException.Message.Replace("\r\n", " "));
1253                     return null;
1254                 }
1255                 // Any other exception was thrown by trying to call it
1256                 catch (Exception ex)
1257                 {
1258                     if (ExceptionHandling.NotExpectedFunctionException(ex))
1259                     {
1260                         throw;
1261                     }
1262 
1263                     // We ended up with something other than a function expression
1264                     string partiallyEvaluated = GenerateStringOfMethodExecuted(expression, objectInstance, name, args);
1265                     ProjectErrorUtilities.ThrowInvalidProject(null, "InvalidFunctionPropertyExpression", partiallyEvaluated, ex.Message);
1266                     return null;
1267                 }
1268             }
1269 
1270             /// <summary>
1271             /// Make an attempt to create a string showing what we were trying to execute when we failed.
1272             /// This will show any intermediate evaluation which may help the user figure out what happened.
1273             /// </summary>
GenerateStringOfMethodExecuted(string expression, object objectInstance, string name, object[] args)1274             private string GenerateStringOfMethodExecuted(string expression, object objectInstance, string name, object[] args)
1275             {
1276                 string parameters = String.Empty;
1277                 if (args != null)
1278                 {
1279                     foreach (object arg in args)
1280                     {
1281                         if (arg == null)
1282                         {
1283                             parameters += "null";
1284                         }
1285                         else
1286                         {
1287                             string argString = arg.ToString();
1288                             if (arg is string && argString.Length == 0)
1289                             {
1290                                 parameters += "''";
1291                             }
1292                             else
1293                             {
1294                                 parameters += arg.ToString();
1295                             }
1296                         }
1297 
1298                         parameters += ", ";
1299                     }
1300 
1301                     if (parameters.Length > 2)
1302                     {
1303                         parameters = parameters.Substring(0, parameters.Length - 2);
1304                     }
1305                 }
1306 
1307                 if (objectInstance == null)
1308                 {
1309                     string typeName = objectType.FullName;
1310 
1311                     // We don't want to expose the real type name of our intrinsics
1312                     // so we'll replace it with "MSBuild"
1313                     if (objectType == typeof(Microsoft.Build.BuildEngine.IntrinsicFunctions))
1314                     {
1315                         typeName = "MSBuild";
1316                     }
1317 
1318                     if ((bindingFlags & BindingFlags.InvokeMethod) == BindingFlags.InvokeMethod)
1319                     {
1320                         return "[" + typeName + "]::" + name + "(" + parameters + ")";
1321                     }
1322                     else
1323                     {
1324                         return "[" + typeName + "]::" + name;
1325                     }
1326                 }
1327                 else
1328                 {
1329                     string propertyValue = "\"" + objectInstance as string + "\"";
1330 
1331                     if ((bindingFlags & BindingFlags.InvokeMethod) == BindingFlags.InvokeMethod)
1332                     {
1333                         return propertyValue + "." + name + "(" + parameters + ")";
1334                     }
1335                     else
1336                     {
1337                         return propertyValue + "." + name;
1338                     }
1339                 }
1340             }
1341 
1342             /// <summary>
1343             /// Return a Type object for the type we're trying to call static methods on
1344             /// </summary>
GetTypeForStaticMethod(string typeName)1345             private static Type GetTypeForStaticMethod(string typeName)
1346             {
1347                 // Ultimately this will be a more in-depth lookup, including assembly name etc.
1348                 // for now, we're only supporting a subset of what's in mscorlib + specific additional types
1349                 // If the env var MSBUILDENABLEALLPROPERTYFUNCTIONS=1 then we'll allow pretty much anything
1350                 Type objectType;
1351                 Tuple<string, Type> functionType;
1352 
1353                 // For whole types we support them being in different assemblies than mscorlib
1354                 // Get the assembly qualified type name if one exists
1355                 if (FunctionConstants.AvailableStaticMethods.TryGetValue(typeName, out functionType) && functionType != null)
1356                 {
1357                     // We need at least one of these set
1358                     ErrorUtilities.VerifyThrow(functionType.Item1 != null || functionType.Item2 != null, "Function type information needs either string or type represented.");
1359 
1360                     // If we have the type information in Type form, then just return that
1361                     if (functionType.Item2 != null)
1362                     {
1363                         return functionType.Item2;
1364                     }
1365                     else if (functionType.Item1 != null)
1366                     {
1367                         // This is a case where the Type is not available at compile time, so
1368                         // we are forced to bind by name instead
1369                         typeName = functionType.Item1;
1370 
1371                         // Get the type from the assembly qualified type name from AvailableStaticMethods
1372                         objectType = Type.GetType(typeName, false /* do not throw TypeLoadException if not found */, true /* ignore case */);
1373 
1374                         // If we've used it once, chances are that we'll be using it again
1375                         // We can record the type here since we know it's available for calling from the fact that is was in the AvailableStaticMethods table
1376                         FunctionConstants.AvailableStaticMethods[typeName] = new Tuple<string, Type>(typeName, objectType);
1377 
1378                         return objectType;
1379                     }
1380                 }
1381 
1382                 // Get the type from mscorlib (or the currently running assembly)
1383                 objectType = Type.GetType(typeName, false /* do not throw TypeLoadException if not found */, true /* ignore case */);
1384 
1385                 if (objectType != null)
1386                 {
1387                     // DO NOT CACHE THE TYPE HERE!
1388                     // We don't add the resolved type here in the AvailableStaticMethods. This is because that table is used
1389                     // during function parse, but only later during execution do we check for the ability to call specific methods on specific types.
1390                     return objectType;
1391                 }
1392 
1393                 // Note the following code path is only entered when MSBUILDENABLEALLPROPERTYFUNCTIONS == 1
1394                 if (Environment.GetEnvironmentVariable("MSBUILDENABLEALLPROPERTYFUNCTIONS") == "1")
1395                 {
1396                     // We didn't find the type, so go probing. First in System
1397                     if (objectType == null)
1398                     {
1399                         objectType = GetTypeFromAssembly(typeName, "System");
1400                     }
1401 
1402                     // Next in System.Core
1403                     if (objectType == null)
1404                     {
1405                         objectType = GetTypeFromAssembly(typeName, "System.Core");
1406                     }
1407 
1408                     // We didn't find the type, so try to find it using the namespace
1409                     if (objectType == null)
1410                     {
1411                         objectType = GetTypeFromAssemblyUsingNamespace(typeName);
1412                     }
1413 
1414                     if (objectType != null)
1415                     {
1416                         // If we've used it once, chances are that we'll be using it again
1417                         // We can cache the type here, since all functions are enabled
1418                         FunctionConstants.AvailableStaticMethods[typeName] = new Tuple<string, Type>(typeName, objectType);
1419                     }
1420                 }
1421 
1422                 return objectType;
1423             }
1424 
1425             /// <summary>
1426             /// Gets the specified type using the namespace to guess the assembly that its in
1427             /// </summary>
GetTypeFromAssemblyUsingNamespace(string typeName)1428             private static Type GetTypeFromAssemblyUsingNamespace(string typeName)
1429             {
1430                 string baseName = typeName;
1431                 int assemblyNameEnd = baseName.LastIndexOf('.');
1432                 Type foundType = null;
1433 
1434                 ErrorUtilities.VerifyThrow(assemblyNameEnd > 0, "Invalid typename: {0}", typeName);
1435 
1436                 // We will work our way up the namespace looking for an assembly that matches
1437                 while (assemblyNameEnd > 0)
1438                 {
1439                     string candidateAssemblyName = null;
1440 
1441                     candidateAssemblyName = baseName.Substring(0, assemblyNameEnd);
1442 
1443                     // Try to load the assembly with the computed name
1444                     foundType = GetTypeFromAssembly(typeName, candidateAssemblyName);
1445 
1446                     if (foundType != null)
1447                     {
1448                         // We have a match, so get the type from that assembly
1449                         return foundType;
1450                     }
1451                     else
1452                     {
1453                         // Keep looking as we haven't found a match yet
1454                         baseName = candidateAssemblyName;
1455                         assemblyNameEnd = baseName.LastIndexOf('.');
1456                     }
1457                 }
1458 
1459                 // We didn't find it, so we need to give up
1460                 return null;
1461             }
1462 
1463 
1464             /// <summary>
1465             /// Get the specified type from the assembly partial name supplied
1466             /// </summary>
GetTypeFromAssembly(string typeName, string candidateAssemblyName)1467             private static Type GetTypeFromAssembly(string typeName, string candidateAssemblyName)
1468             {
1469                 Type objectType = null;
1470 
1471                 // Try to load the assembly with the computed name
1472 #pragma warning disable 618
1473                 // Unfortunately Assembly.Load is not an alternative to LoadWithPartialName, since
1474                 // Assembly.Load requires the full assembly name to be passed to it.
1475                 // Therefore we must ignore the deprecated warning.
1476                 Assembly candidateAssembly = Assembly.LoadWithPartialName(candidateAssemblyName);
1477 #pragma warning restore 618
1478 
1479                 if (candidateAssembly != null)
1480                 {
1481                     objectType = candidateAssembly.GetType(typeName, false /* do not throw TypeLoadException if not found */, true /* ignore case */);
1482                 }
1483 
1484                 return objectType;
1485             }
1486 
1487             /// <summary>
1488             /// Factory method to construct a function for property evaluation
1489             /// </summary>
ConstructFunction(string expressionFunction, string expressionRootName, Type objectType, int argumentStartIndex, int methodStartIndex)1490             private static Function ConstructFunction(string expressionFunction, string expressionRootName, Type objectType, int argumentStartIndex, int methodStartIndex)
1491             {
1492                 // The unevaluated and unexpanded arguments for this function
1493                 string[] functionArguments;
1494 
1495                 // The name of the function that will be invoked
1496                 string functionToInvoke;
1497 
1498                 // What's left of the expression once the function has been constructed
1499                 string remainder = String.Empty;
1500 
1501                 // The binding flags that we will use for this function's execution
1502                 BindingFlags defaultBindingFlags = BindingFlags.IgnoreCase | BindingFlags.Public;
1503 
1504                 // There are arguments that need to be passed to the function
1505                 if (argumentStartIndex > -1 && !expressionFunction.Substring(methodStartIndex, argumentStartIndex - methodStartIndex).Contains("."))
1506                 {
1507                     string argumentsContent;
1508 
1509                     // separate the function and the arguments
1510                     functionToInvoke = expressionFunction.Substring(methodStartIndex, argumentStartIndex - methodStartIndex).Trim();
1511 
1512                     // Skip the '('
1513                     argumentStartIndex++;
1514 
1515                     // Scan for the matching closing bracket, skipping any nested ones
1516                     int argumentsEndIndex = ScanForClosingParenthesis(expressionFunction, argumentStartIndex);
1517 
1518                     // We should never end up in this situation, since the brackets will have been
1519                     // validated at the very outermost level (in the initial property parse). However if we
1520                     // end up with unmatched brackets here for some reason.. it's an error!
1521                     ErrorUtilities.VerifyThrow(argumentsEndIndex != -1, "Unmatched braces when constructing function.", "$(" + expressionFunction + ")");
1522 
1523                     // We have been asked for a method invocation
1524                     defaultBindingFlags |= BindingFlags.InvokeMethod;
1525 
1526                     // It may be that there are '()' but no actual arguments content
1527                     if (argumentStartIndex == expressionFunction.Length - 1)
1528                     {
1529                         argumentsContent = String.Empty;
1530                         functionArguments = new string[0];
1531                     }
1532                     else
1533                     {
1534                         // we have content within the '()' so let's extract and deal with it
1535                         argumentsContent = expressionFunction.Substring(argumentStartIndex, argumentsEndIndex - argumentStartIndex);
1536 
1537                         // If there are no arguments, then just create an empty array
1538                         if (String.IsNullOrEmpty(argumentsContent))
1539                         {
1540                             functionArguments = new string[0];
1541                         }
1542                         else
1543                         {
1544                             // We will keep empty entries so that we can treat them as null
1545                             functionArguments = ExtractFunctionArguments(expressionFunction, argumentsContent);
1546                         }
1547 
1548                         remainder = expressionFunction.Substring(argumentsEndIndex + 1);
1549                     }
1550                 }
1551                 else
1552                 {
1553                     int nextMethodIndex = expressionFunction.IndexOf('.', methodStartIndex);
1554                     int methodLength = expressionFunction.Length - methodStartIndex;
1555 
1556                     functionArguments = new string[0];
1557 
1558                     if (nextMethodIndex > 0)
1559                     {
1560                         methodLength = nextMethodIndex - methodStartIndex;
1561                         remainder = expressionFunction.Substring(nextMethodIndex);
1562                     }
1563 
1564                     string netPropertyName = expressionFunction.Substring(methodStartIndex, methodLength).Trim();
1565 
1566                     ProjectErrorUtilities.VerifyThrowInvalidProject(netPropertyName.Length > 0, null, "InvalidFunctionPropertyExpression", expressionFunction, String.Empty);
1567 
1568                     // We have been asked for a property or a field
1569                     defaultBindingFlags |= (BindingFlags.GetProperty | BindingFlags.GetField);
1570 
1571                     functionToInvoke = netPropertyName;
1572                 }
1573 
1574                 // either there are no functions left or what we have is another function
1575                 if (String.IsNullOrEmpty(remainder) || remainder[0] == '.')
1576                 {
1577                     // Construct a FunctionInfo will all the content that we just gathered
1578                     return new Function(objectType, expressionFunction, expressionRootName, functionToInvoke, functionArguments, defaultBindingFlags, remainder);
1579                 }
1580                 else
1581                 {
1582                     // We ended up with something other than a function expression
1583                     ProjectErrorUtilities.ThrowInvalidProject(null, "InvalidFunctionPropertyExpression", expressionFunction, String.Empty);
1584                     return null;
1585                 }
1586             }
1587 
1588             /// <summary>
1589             /// Extract the first level of arguments from the content.
1590             /// Splits the content passed in at commas.
1591             /// Returns an array of unexpanded arguments.
1592             /// If there are no arguments, returns an empty array.
1593             /// </summary>
ExtractFunctionArguments(string expressionFunction, string argumentsContent)1594             private static string[] ExtractFunctionArguments(string expressionFunction, string argumentsContent)
1595             {
1596                 List<string> arguments = new List<string>();
1597                 StringBuilder argumentBuilder = new StringBuilder(argumentsContent.Length); ;
1598 
1599                 // Iterate over the contents of the arguments extracting the
1600                 // the individual arguments as we go
1601                 for (int n = 0; n < argumentsContent.Length; n++)
1602                 {
1603                     // We found a property expression.. skip over all of it.
1604                     if ((n < argumentsContent.Length - 1) && (argumentsContent[n] == '$' && argumentsContent[n + 1] == '('))
1605                     {
1606                         int nestedPropertyStart = n;
1607                         n += 2; // skip over the opening '$('
1608 
1609                         // Scan for the matching closing bracket, skipping any nested ones
1610                         n = ScanForClosingParenthesis(argumentsContent, n);
1611 
1612                         ProjectErrorUtilities.VerifyThrowInvalidProject(n != 0, null, "InvalidFunctionPropertyExpression", expressionFunction, String.Empty);
1613 
1614                         argumentBuilder.Append(argumentsContent.Substring(nestedPropertyStart, (n - nestedPropertyStart) + 1));
1615                     }
1616                     else if (argumentsContent[n] == '`' || argumentsContent[n] == '"' || argumentsContent[n] == '\'')
1617                     {
1618                         int quoteStart = n;
1619                         n += 1; // skip over the opening quote
1620 
1621                         n = ScanForClosingQuote(argumentsContent[quoteStart], argumentsContent, n);
1622 
1623                         ProjectErrorUtilities.VerifyThrowInvalidProject(n != 0, null, "InvalidFunctionPropertyExpression", expressionFunction, String.Empty);
1624 
1625                         argumentBuilder.Append(argumentsContent.Substring(quoteStart, (n - quoteStart) + 1));
1626                     }
1627                     else if (argumentsContent[n] == ',')
1628                     {
1629                         // We have reached the end of the current argument, go ahead and add it
1630                         // to our list
1631                         AddArgument(arguments, argumentBuilder);
1632                         // Create a new argument builder ready for the next argument
1633                         argumentBuilder = new StringBuilder(argumentsContent.Length);
1634                     }
1635                     else
1636                     {
1637                         argumentBuilder.Append(argumentsContent[n]);
1638                     }
1639                 }
1640 
1641                 // This will either be the one and only argument, or the last one
1642                 // so add it to our list
1643                 AddArgument(arguments, argumentBuilder);
1644 
1645                 return arguments.ToArray();
1646             }
1647 
1648             /// <summary>
1649             /// Skip all characters until we find the matching quote character
1650             /// </summary>
ScanForClosingQuote(char quoteChar, string expression, int index)1651             private static int ScanForClosingQuote(char quoteChar, string expression, int index)
1652             {
1653                 // Scan for our closing quoteChar
1654                 while (index < expression.Length)
1655                 {
1656                     if (expression[index] == quoteChar)
1657                     {
1658                         return index;
1659                     }
1660                     index++;
1661                 }
1662 
1663                 return -1;
1664             }
1665 
1666             /// <summary>
1667             /// Add the argument in the StringBuilder to the arguments list, handling nulls
1668             /// appropriately
1669             /// </summary>
AddArgument(List<string> arguments, StringBuilder argumentBuilder)1670             private static void AddArgument(List<string> arguments, StringBuilder argumentBuilder)
1671             {
1672                 // If we don't have something that can be treated as an argument
1673                 // then we should treat it as a null so that passing nulls
1674                 // becomes possible through an empty argument between commas.
1675                 ErrorUtilities.VerifyThrowArgumentNull(argumentBuilder, "argumentBuilder");
1676                 // we reached the end of an argument, add the builder's final result
1677                 // to our arguments.
1678                 string argValue = argumentBuilder.ToString().Trim();
1679                 // We support passing of null through the argument constant value null
1680                 if (String.Compare("null", argValue, StringComparison.OrdinalIgnoreCase) == 0)
1681                 {
1682                     arguments.Add(null);
1683                 }
1684                 else
1685                 {
1686                     arguments.Add(argValue);
1687                 }
1688             }
1689 
1690             /// <summary>
1691             /// Coerce the arguments according to the parameter types
1692             /// Will only return null if the coercion didn't work due to an InvalidCastException
1693             /// </summary>
CoerceArguments(object[] args, ParameterInfo[] parameters)1694             private static object[] CoerceArguments(object[] args, ParameterInfo[] parameters)
1695             {
1696                 object[] coercedArguments = new object[args.Length];
1697 
1698                 try
1699                 {
1700                     // Do our best to coerce types into the arguments for the function
1701                     for (int n = 0; n < parameters.Length; n++)
1702                     {
1703                         if (args[n] == null)
1704                         {
1705                             // We can't coerce (object)null -- that's as general
1706                             // as it can get!
1707                             continue;
1708                         }
1709 
1710                         // Here we have special case conversions on a type basis
1711                         if (parameters[n].ParameterType == typeof(char[]))
1712                         {
1713                             coercedArguments[n] = args[n].ToString().ToCharArray();
1714                         }
1715                         else if (parameters[n].ParameterType.IsEnum && args[n] is string && ((string)args[n]).Contains("."))
1716                         {
1717                             Type enumType = parameters[n].ParameterType;
1718                             string typeLeafName = enumType.Name + ".";
1719                             string typeFullName = enumType.FullName + ".";
1720 
1721                             // Enum.parse expects commas between enum components
1722                             // We'll support the C# type | syntax too
1723                             // We'll also allow the user to specify the leaf or full type name on the enum
1724                             string argument = args[n].ToString().Replace('|', ',').Replace(typeFullName, "").Replace(typeLeafName, "");
1725 
1726                             // Parse the string representation of the argument into the destination enum
1727                             coercedArguments[n] = Enum.Parse(enumType, argument);
1728                         }
1729                         else
1730                         {
1731                             // change the type of the final unescaped string into the destination
1732                             coercedArguments[n] = Convert.ChangeType(args[n], parameters[n].ParameterType, CultureInfo.InvariantCulture);
1733                         }
1734                     }
1735                 }
1736                 catch (InvalidCastException)
1737                 {
1738                     // The coercion failed therefore we return null
1739                     return null;
1740                 }
1741 
1742                 return coercedArguments;
1743             }
1744 
1745             /// <summary>
1746             /// For this initial implementation of inline functions, only very specific static methods on specific types are
1747             /// available
1748             /// </summary>
IsStaticMethodAvailable(Type objectType, string methodName)1749             private bool IsStaticMethodAvailable(Type objectType, string methodName)
1750             {
1751                 if (objectType == typeof(Microsoft.Build.BuildEngine.IntrinsicFunctions))
1752                 {
1753                     // These are our intrinsic functions, so we're OK with those
1754                     return true;
1755                 }
1756                 else
1757                 {
1758                     string typeMethod = objectType.FullName + "::" + methodName;
1759 
1760                     if (FunctionConstants.AvailableStaticMethods.ContainsKey(objectType.FullName))
1761                     {
1762                         // Check our set for the type name
1763                         // This enables all statics on the given type
1764                         return true;
1765                     }
1766                     else if (FunctionConstants.AvailableStaticMethods.ContainsKey(typeMethod))
1767                     {
1768                         // Check for specific methods on types
1769                         return true;
1770                     }
1771                     else if (Environment.GetEnvironmentVariable("MSBUILDENABLEALLPROPERTYFUNCTIONS") == "1")
1772                     {
1773                         // If MSBUILDENABLEALLPROPERTYFUNCTION == 1, then anything goes
1774                         return true;
1775                     }
1776                 }
1777 
1778                 return false;
1779             }
1780 
1781             /// <summary>
1782             /// Construct and instance of objectType based on the constructor or method arguments provided.
1783             /// Arguments must never be null.
1784             /// </summary>
LateBindExecute(Exception ex, BindingFlags bindingFlags, object objectInstance , object[] args, bool isConstructor)1785             private object LateBindExecute(Exception ex, BindingFlags bindingFlags, object objectInstance /* null unless instance method */, object[] args, bool isConstructor)
1786             {
1787                 ParameterInfo[] parameters = null;
1788                 MethodBase[] members = null;
1789                 MethodBase memberInfo = null;
1790 
1791                 // First let's try for a method where all arguments are strings..
1792                 Type[] types = new Type[arguments.Length];
1793                 for (int n = 0; n < arguments.Length; n++)
1794                 {
1795                     types[n] = typeof(string);
1796                 }
1797 
1798                 if (isConstructor)
1799                 {
1800                     memberInfo = objectType.GetConstructor(bindingFlags, null, types, null);
1801                 }
1802                 else
1803                 {
1804                     memberInfo = objectType.GetMethod(this.name, bindingFlags, null, types, null);
1805                 }
1806 
1807                 // If we didn't get a match on all string arguments,
1808                 // search for a method with the right number of arguments
1809                 if (memberInfo == null)
1810                 {
1811                     // Gather all methods that may match
1812                     if (isConstructor)
1813                     {
1814                         members = objectType.GetConstructors(bindingFlags);
1815                     }
1816                     else
1817                     {
1818                         members = objectType.GetMethods(bindingFlags);
1819                     }
1820 
1821                     // Try to find a method with the right name, number of arguments and
1822                     // compatible argument types
1823                     object[] coercedArguments = null;
1824                     foreach (MethodBase member in members)
1825                     {
1826                         parameters = member.GetParameters();
1827 
1828                         // Simple match on name and number of params, we will be case insensitive
1829                         if (parameters.Length == this.arguments.Length)
1830                         {
1831                             if (isConstructor || String.Equals(member.Name, this.name, StringComparison.OrdinalIgnoreCase))
1832                             {
1833                                 // we have a match on the name and argument number
1834                                 // now let's try to coerce the arguments we have
1835                                 // into the arguments on the matching method
1836                                 coercedArguments = CoerceArguments(args, parameters);
1837 
1838                                 if (coercedArguments != null)
1839                                 {
1840                                     // We have a complete match
1841                                     memberInfo = member;
1842                                     args = coercedArguments;
1843                                     break;
1844                                 }
1845                             }
1846                         }
1847                     }
1848                 }
1849 
1850                 object functionResult = null;
1851 
1852                 // We have a match and coerced arguments, let's construct..
1853                 if (memberInfo != null && args != null)
1854                 {
1855                     if (isConstructor)
1856                     {
1857                         functionResult = ((ConstructorInfo)memberInfo).Invoke(args);
1858                     }
1859                     else
1860                     {
1861                         functionResult = ((MethodInfo)memberInfo).Invoke(objectInstance /* null if static method */, args);
1862                     }
1863                 }
1864                 else if (!isConstructor)
1865                 {
1866                     throw ex;
1867                 }
1868 
1869                 if (functionResult == null && isConstructor)
1870                 {
1871                     throw new TargetInvocationException(new MissingMethodException());
1872                 }
1873 
1874                 return functionResult;
1875             }
1876         }
1877 
1878         /// <summary>
1879         /// Expands all embedded item metadata in the given string, using the bucketed items.
1880         ///
1881         /// This method leaves the expression escaped.  Callers may need to unescape on their own as appropriate.
1882         /// </summary>
1883         /// <remarks>
1884         /// This method is marked internal only for unit-testing purposes. Ideally
1885         /// it should be private.
1886         /// </remarks>
1887         /// <owner>SumedhK</owner>
1888         /// <param name="expression"></param>
1889         /// <returns>the expanded string</returns>
ExpandMetadataLeaveEscaped( string expression )1890         internal string ExpandMetadataLeaveEscaped
1891             (
1892             string expression
1893             )
1894         {
1895             if ((options & ExpanderOptions.ExpandMetadata) != ExpanderOptions.ExpandMetadata)
1896             {
1897                 return expression;
1898             }
1899 
1900             string result;
1901 
1902             // PERF NOTE: Regex matching is expensive, so if the string doesn't contain any item metadata references, just bail
1903             // out -- pre-scanning the string is actually cheaper than running the Regex, even when there are no matches!
1904             if (expression.IndexOf(ItemExpander.itemMetadataPrefix, StringComparison.Ordinal) == -1)
1905             {
1906                 result = expression;
1907             }
1908             // if there are no item vectors in the string
1909             else if (expression.IndexOf(ItemExpander.itemVectorPrefix, StringComparison.Ordinal) == -1)
1910             {
1911                 // run a simpler Regex to find item metadata references
1912                 result = ItemExpander.itemMetadataPattern.Replace(expression, new MatchEvaluator(ExpandSingleMetadata));
1913             }
1914             // PERF NOTE: this is a highly targeted optimization for a common pattern observed during profiling
1915             // if the string is a list of item vectors with no separator specifications
1916             else if (ItemExpander.listOfItemVectorsWithoutSeparatorsPattern.IsMatch(expression))
1917             {
1918                 // then even if the string contains item metadata references, those references will only be inside transform
1919                 // expressions, and can be safely skipped
1920                 result = expression;
1921             }
1922             else
1923             {
1924                 // otherwise, run the more complex Regex to find item metadata references not contained in transforms
1925                 result = ItemExpander.nonTransformItemMetadataPattern.Replace(expression, new MatchEvaluator(ExpandSingleMetadata));
1926             }
1927 
1928             return result;
1929         }
1930 
1931         /// <summary>
1932         /// Expands a single item metadata.
1933         /// </summary>
1934         /// <remarks>This method is a callback for Regex.Replace().</remarks>
1935         /// <owner>SumedhK</owner>
1936         /// <param name="itemMetadataMatch"></param>
1937         /// <returns>the expanded item metadata</returns>
ExpandSingleMetadata(Match itemMetadataMatch)1938         private string ExpandSingleMetadata(Match itemMetadataMatch)
1939         {
1940             ErrorUtilities.VerifyThrow(itemMetadataMatch.Success, "Need a valid item metadata.");
1941 
1942             string metadataName = itemMetadataMatch.Groups["NAME"].Value;
1943             string itemType = null;
1944 
1945             // check if the metadata is qualified with the item type
1946             if (itemMetadataMatch.Groups["ITEM_SPECIFICATION"].Length > 0)
1947             {
1948                 itemType = itemMetadataMatch.Groups["TYPE"].Value;
1949             }
1950 
1951             // look up the metadata - we may not have a value for it
1952             string metadataValue = null;
1953 
1954             metadataValue = GetValueFromMetadataTable(itemType, metadataName, metadataValue);
1955 
1956             if (metadataValue == null)
1957             {
1958                 metadataValue = GetDefaultMetadataValue(itemType, metadataName, metadataValue);
1959             }
1960 
1961             return metadataValue ?? String.Empty;
1962         }
1963 
1964         /// <summary>
1965         /// Retrieves any value we have in our metadata table for the metadata name specified.
1966         /// If no value is available, returns null.
1967         /// </summary>
GetValueFromMetadataTable(string itemType, string metadataName, string metadataValue)1968         private string GetValueFromMetadataTable(string itemType, string metadataName, string metadataValue)
1969         {
1970             if (itemMetadata == null) return null;
1971 
1972             if (implicitMetadataItemType == null)
1973             {
1974                 // metadata table has some qualified keys; if the expression is
1975                 // qualified, look it up with a qualified key
1976                 string key;
1977                 if (itemType == null)
1978                 {
1979                     key = metadataName;
1980                 }
1981                 else
1982                 {
1983                     key = itemType + "." + metadataName;
1984                 }
1985 
1986                 itemMetadata.TryGetValue(key, out metadataValue);
1987             }
1988             else
1989             {
1990                 // metadata table has all unqualified keys.
1991                 // if we found a qualified metadata, it must match the type
1992                 // of all the metadata in our table of unqualified metadata
1993                 if (itemType == null || String.Equals(itemType, implicitMetadataItemType, StringComparison.OrdinalIgnoreCase))
1994                 {
1995                     itemMetadata.TryGetValue(metadataName, out metadataValue);
1996                 }
1997             }
1998 
1999             return metadataValue;
2000         }
2001 
2002         /// <summary>
2003         /// Retrieves any value we have for the specified metadata in any table of default metadata we've been assigned.
2004         /// If no value is available, returns null.
2005         /// </summary>
GetDefaultMetadataValue(string itemType, string metadataName, string metadataValue)2006         private string GetDefaultMetadataValue(string itemType, string metadataName, string metadataValue)
2007         {
2008             if (specificItemDefinitionLibrary == null) return null;
2009 
2010             if (itemType == null || String.Equals(itemType, specificItemDefinitionLibrary.ItemType, StringComparison.OrdinalIgnoreCase))
2011             {
2012                 metadataValue = specificItemDefinitionLibrary.GetDefaultMetadataValue(metadataName);
2013             }
2014 
2015             return metadataValue;
2016         }
2017 
2018         /// <summary>
2019         /// Takes the specified string and expands all item vectors embedded in it. The expansion is done in 2 passes:
2020         /// 1) the first pass expands only the item vectors that refer to the bucketed items
2021         /// 2) the second pass expands out the remaining item vectors using the project items
2022         ///
2023         /// This method leaves the expression escaped.  Callers may need to unescape on their own as appropriate.
2024         /// </summary>
2025         /// <param name="expression"></param>
2026         /// <param name="expressionNode">The XML attribute containing the string we're trying to expand here.  Solely
2027         /// for the purposes of providing line/column number information when there's an error.</param>
2028         /// <returns>expanded string</returns>
2029         /// <owner>SumedhK</owner>
ExpandItemsIntoStringLeaveEscaped( string expression, XmlNode expressionNode )2030         private string ExpandItemsIntoStringLeaveEscaped
2031             (
2032             string expression,
2033             XmlNode expressionNode
2034             )
2035         {
2036             // An empty string always expands to empty
2037             if (String.IsNullOrEmpty(expression) || lookup == null)
2038             {
2039                 return expression;
2040             }
2041 
2042             if ((options & ExpanderOptions.ExpandItems) != ExpanderOptions.ExpandItems)
2043             {
2044                 return expression;
2045             }
2046 
2047             return ItemExpander.ExpandEmbeddedItemVectors(expression, expressionNode, lookup);
2048         }
2049 
2050         /// <summary>
2051         /// Attempts to extract the contents of the given item vector. Returns a virtual BuildItemGroup.
2052         /// This method leaves all the items escaped.  Caller may need to unescape them himself if appropriate.
2053         /// </summary>
2054         /// <param name="singleItemVectorExpression"></param>
2055         /// <param name="itemVectorAttribute">The XML attribute that contains the thing we're expanding here.  (Only needed
2056         /// for the purpose of logging good error messages with line/column information.</param>
2057         /// <returns>a virtual BuildItemGroup containing the items resulting from the expression, or null if the expression was invalid.</returns>
2058         /// <owner>SumedhK;RGoel</owner>
ExpandSingleItemListExpressionIntoItemsLeaveEscaped( string singleItemVectorExpression, XmlAttribute itemVectorAttribute )2059         internal BuildItemGroup ExpandSingleItemListExpressionIntoItemsLeaveEscaped
2060         (
2061             string singleItemVectorExpression,
2062             XmlAttribute itemVectorAttribute
2063         )
2064         {
2065             if (lookup == null)
2066             {
2067                 return null;
2068             }
2069 
2070             Match throwAwayMatch;
2071             return this.ExpandSingleItemListExpressionIntoItemsLeaveEscaped(singleItemVectorExpression, itemVectorAttribute, out throwAwayMatch);
2072         }
2073 
2074         /// <summary>
2075         /// Attempts to extract the contents of the given item vector.
2076         /// </summary>
2077         /// <param name="singleItemVectorExpression"></param>
2078         /// <param name="itemVectorAttribute">The XML attribute that contains the thing we're expanding here.  (Only needed
2079         /// for the purpose of logging good error messages with line/column information.</param>
2080         /// <param name="itemVectorMatch"></param>
2081         /// <returns>a virtual BuildItemGroup containing the items resulting from the expression, or null if the expression was invalid.</returns>
2082         /// <owner>SumedhK;RGoel</owner>
ExpandSingleItemListExpressionIntoItemsLeaveEscaped( string singleItemVectorExpression, XmlAttribute itemVectorAttribute, out Match itemVectorMatch )2083         internal BuildItemGroup ExpandSingleItemListExpressionIntoItemsLeaveEscaped
2084         (
2085             string singleItemVectorExpression,
2086             XmlAttribute itemVectorAttribute,
2087             out Match itemVectorMatch
2088         )
2089         {
2090             ErrorUtilities.VerifyThrow(lookup != null, "Need items");
2091 
2092             if ((options & ExpanderOptions.ExpandItems) != ExpanderOptions.ExpandItems)
2093             {
2094                 itemVectorMatch = null;
2095                 return null;
2096             }
2097 
2098             return ItemExpander.ItemizeItemVector(singleItemVectorExpression, itemVectorAttribute, lookup, out itemVectorMatch);
2099         }
2100     }
2101 
2102     /// <summary>
2103     /// Indicates to an expander what exactly it should expand
2104     /// </summary>
2105     [Flags]
2106     internal enum ExpanderOptions
2107     {
2108         Invalid = 0x0,
2109         ExpandProperties = 0x1,
2110         ExpandItems = 0x2,
2111         ExpandPropertiesAndItems = ExpandProperties | ExpandItems,
2112         ExpandMetadata = 0x4,
2113         ExpandPropertiesAndMetadata = ExpandProperties | ExpandMetadata,
2114         ExpandAll = ExpandPropertiesAndItems | ExpandMetadata
2115     };
2116 }
2117 
2118 
2119