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