// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Resources; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.ComponentModel; #if BUILDINGAPPXTASKS namespace Microsoft.Build.AppxPackage.Shared #else //EPIC BEGIN #nullable disable //EPIC END namespace Microsoft.Build.Shared #endif { /// /// This class contains utility methods for dealing with resources. /// internal static class ResourceUtilities { /// /// Extracts the message code (if any) prefixed to the given string. /// MSB\d\d\d\d):\s*(?.*)$" /// Arbitrary codes match "^\s*(?[A-Za-z]+\d+):\s*(?.*)$" /// ]]> /// Thread safe. /// /// Whether to match only MSBuild error codes, or any error code. /// The string to parse. /// [out] The message code, or null if there was no code. /// The string without its message code prefix, if any. [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Scope = "member", Target = "Microsoft.Build.Shared.ResourceUtilities.#ExtractMessageCode(System.Boolean,System.String,System.String&)", Justification = "Unavoidable complexity")] internal static string ExtractMessageCode(bool msbuildCodeOnly, string message, out string code) { #if !BUILDINGAPPXTASKS ErrorUtilities.VerifyThrowInternalNull(message, nameof(message)); #endif code = null; int i = 0; while (i < message.Length && Char.IsWhiteSpace(message[i])) { i++; } #if !BUILDINGAPPXTASKS if (msbuildCodeOnly) { if ( message.Length < i + 8 || message[i] != 'M' || message[i + 1] != 'S' || message[i + 2] != 'B' || message[i + 3] < '0' || message[i + 3] > '9' || message[i + 4] < '0' || message[i + 4] > '9' || message[i + 5] < '0' || message[i + 5] > '9' || message[i + 6] < '0' || message[i + 6] > '9' || message[i + 7] != ':' ) { return message; } code = message.Substring(i, 7); i += 8; } else #endif { int j = i; for (; j < message.Length; j++) { char c = message[j]; if (((c < 'a') || (c > 'z')) && ((c < 'A') || (c > 'Z'))) { break; } } if (j == i) { return message; // Should have been at least one letter } int k = j; for (; k < message.Length; k++) { char c = message[k]; if (c < '0' || c > '9') { break; } } if (k == j) { return message; // Should have been at least one digit } if (k == message.Length || message[k] != ':') { return message; } code = message.Substring(i, k - i); i = k + 1; } while (i < message.Length && Char.IsWhiteSpace(message[i])) { i++; } if (i < message.Length) { message = message.Substring(i, message.Length - i); } return message; } /// /// Retrieves the MSBuild F1-help keyword for the given resource string. Help keywords are used to index help topics in /// host IDEs. /// /// Resource string to get the MSBuild F1-keyword for. /// The MSBuild F1-help keyword string. private static string GetHelpKeyword(string resourceName) { return "MSBuild." + resourceName; } #if !BUILDINGAPPXTASKS /// /// Retrieves the contents of the named resource string. /// /// Resource string name. /// Resource string contents. internal static string GetResourceString(string resourceName) { #if EPIC_GAMES_REMOVED string result = AssemblyResources.GetString(resourceName); #else string result = "ResourceStringNotSupported"; #endif return result; } /// /// Loads the specified string resource and formats it with the arguments passed in. If the string resource has an MSBuild /// message code and help keyword associated with it, they too are returned. /// /// PERF WARNING: calling a method that takes a variable number of arguments is expensive, because memory is allocated for /// the array of arguments -- do not call this method repeatedly in performance-critical scenarios /// /// This method is thread-safe. /// [out] The MSBuild message code, or null. /// [out] The MSBuild F1-help keyword for the host IDE, or null. /// Resource string to load. /// Optional arguments for formatting the resource string. /// The formatted resource string. internal static string FormatResourceStringStripCodeAndKeyword(out string code, out string helpKeyword, string resourceName, params object[] args) { helpKeyword = GetHelpKeyword(resourceName); // NOTE: the AssemblyResources.GetString() method is thread-safe return ExtractMessageCode(true /* msbuildCodeOnly */, FormatString(GetResourceString(resourceName), args), out code); } /// /// Looks up a string in the resources, and formats it with the arguments passed in. If the string resource has an MSBuild /// message code and help keyword associated with it, they are discarded. /// /// PERF WARNING: calling a method that takes a variable number of arguments is expensive, because memory is allocated for /// the array of arguments -- do not call this method repeatedly in performance-critical scenarios /// /// This method is thread-safe. /// Resource string to load. /// Optional arguments for formatting the resource string. /// The formatted resource string. internal static string FormatResourceStringStripCodeAndKeyword(string resourceName, params object[] args) { string code; string helpKeyword; return FormatResourceStringStripCodeAndKeyword(out code, out helpKeyword, resourceName, args); } /// /// Formats the resource string with the given arguments. /// Ignores error codes and keywords /// /// /// /// internal static string FormatResourceStringIgnoreCodeAndKeyword(string resourceName, params object[] args) { // NOTE: the AssemblyResources.GetString() method is thread-safe return FormatString(GetResourceString(resourceName), args); } /// /// Formats the given string using the variable arguments passed in. /// /// PERF WARNING: calling a method that takes a variable number of arguments is expensive, because memory is allocated for /// the array of arguments -- do not call this method repeatedly in performance-critical scenarios /// /// Thread safe. /// /// The string to format. /// Optional arguments for formatting the given string. /// The formatted string. internal static string FormatString(string unformatted, params object[] args) { string formatted = unformatted; // NOTE: String.Format() does not allow a null arguments array if ((args?.Length > 0)) { #if DEBUG // If you accidentally pass some random type in that can't be converted to a string, // FormatResourceString calls ToString() which returns the full name of the type! foreach (object param in args) { // Check it has a real implementation of ToString() if (param != null) { if (String.Equals(param.GetType().ToString(), param.ToString(), StringComparison.Ordinal)) { ErrorUtilities.ThrowInternalError("Invalid resource parameter type, was {0}", param.GetType().FullName); } } } #endif // Format the string, using the variable arguments passed in. // NOTE: all String methods are thread-safe formatted = String.Format(CultureInfo.CurrentCulture, unformatted, args); } return formatted; } /// /// Verifies that a particular resource string actually exists in the string table. This will only be called in debug /// builds. It helps catch situations where a dev calls VerifyThrowXXX with a new resource string, but forgets to add the /// resource string to the string table, or misspells it! /// /// This method is thread-safe. /// Resource string to check. internal static void VerifyResourceStringExists(string resourceName) { #if DEBUG try { // Look up the resource string in the engine's string table. // NOTE: the AssemblyResources.GetString() method is thread-safe #if EPIC_GAMES_REMOVED string unformattedMessage = AssemblyResources.GetString(resourceName); #else string unformattedMessage = GetResourceString(resourceName); #endif if (unformattedMessage == null) { ErrorUtilities.ThrowInternalError("The resource string \"" + resourceName + "\" was not found."); } } catch (ArgumentException e) { #if FEATURE_DEBUG_LAUNCH Debug.Fail("The resource string \"" + resourceName + "\" was not found."); #endif ErrorUtilities.ThrowInternalError(e.Message); } catch (InvalidOperationException e) { #if FEATURE_DEBUG_LAUNCH Debug.Fail("The resource string \"" + resourceName + "\" was not found."); #endif ErrorUtilities.ThrowInternalError(e.Message); } catch (MissingManifestResourceException e) { #if FEATURE_DEBUG_LAUNCH Debug.Fail("The resource string \"" + resourceName + "\" was not found."); #endif ErrorUtilities.ThrowInternalError(e.Message); } #endif } #endif } }