/src/Sdk/DTExpressions2/Expressions2/Sdk/Functions/Format.cs

https://github.com/actions/runner · C# · 298 lines · 233 code · 29 blank · 36 comment · 34 complexity · 31580f9cd4851e928038b5f9e85af9da MD5 · raw file

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.Linq;
  5. using System.Text;
  6. using ExpressionResources = GitHub.DistributedTask.Expressions.ExpressionResources;
  7. namespace GitHub.DistributedTask.Expressions2.Sdk.Functions
  8. {
  9. internal sealed class Format : Function
  10. {
  11. protected sealed override Object EvaluateCore(
  12. EvaluationContext context,
  13. out ResultMemory resultMemory)
  14. {
  15. resultMemory = null;
  16. var format = Parameters[0].Evaluate(context).ConvertToString();
  17. var index = 0;
  18. var result = new FormatResultBuilder(this, context, CreateMemoryCounter(context));
  19. while (index < format.Length)
  20. {
  21. var lbrace = format.IndexOf('{', index);
  22. var rbrace = format.IndexOf('}', index);
  23. // Left brace
  24. if (lbrace >= 0 && (rbrace < 0 || rbrace > lbrace))
  25. {
  26. // Escaped left brace
  27. if (SafeCharAt(format, lbrace + 1) == '{')
  28. {
  29. result.Append(format.Substring(index, lbrace - index + 1));
  30. index = lbrace + 2;
  31. }
  32. // Left brace, number, optional format specifiers, right brace
  33. else if (rbrace > lbrace + 1 &&
  34. ReadArgIndex(format, lbrace + 1, out Byte argIndex, out Int32 endArgIndex) &&
  35. ReadFormatSpecifiers(format, endArgIndex + 1, out String formatSpecifiers, out rbrace))
  36. {
  37. // Check parameter count
  38. if (argIndex > Parameters.Count - 2)
  39. {
  40. throw new FormatException(ExpressionResources.InvalidFormatArgIndex(format));
  41. }
  42. // Append the portion before the left brace
  43. if (lbrace > index)
  44. {
  45. result.Append(format.Substring(index, lbrace - index));
  46. }
  47. // Append the arg
  48. result.Append(argIndex, formatSpecifiers);
  49. index = rbrace + 1;
  50. }
  51. else
  52. {
  53. throw new FormatException(ExpressionResources.InvalidFormatString(format));
  54. }
  55. }
  56. // Right brace
  57. else if (rbrace >= 0)
  58. {
  59. // Escaped right brace
  60. if (SafeCharAt(format, rbrace + 1) == '}')
  61. {
  62. result.Append(format.Substring(index, rbrace - index + 1));
  63. index = rbrace + 2;
  64. }
  65. else
  66. {
  67. throw new FormatException(ExpressionResources.InvalidFormatString(format));
  68. }
  69. }
  70. // Last segment
  71. else
  72. {
  73. result.Append(format.Substring(index));
  74. break;
  75. }
  76. }
  77. return result.ToString();
  78. }
  79. private Boolean ReadArgIndex(
  80. String str,
  81. Int32 startIndex,
  82. out Byte result,
  83. out Int32 endIndex)
  84. {
  85. // Count the number of digits
  86. var length = 0;
  87. while (Char.IsDigit(SafeCharAt(str, startIndex + length)))
  88. {
  89. length++;
  90. }
  91. // Validate at least one digit
  92. if (length < 1)
  93. {
  94. result = default;
  95. endIndex = default;
  96. return false;
  97. }
  98. // Parse the number
  99. endIndex = startIndex + length - 1;
  100. return Byte.TryParse(str.Substring(startIndex, length), NumberStyles.None, CultureInfo.InvariantCulture, out result);
  101. }
  102. private Boolean ReadFormatSpecifiers(
  103. String str,
  104. Int32 startIndex,
  105. out String result,
  106. out Int32 rbrace)
  107. {
  108. // No format specifiers
  109. var c = SafeCharAt(str, startIndex);
  110. if (c == '}')
  111. {
  112. result = String.Empty;
  113. rbrace = startIndex;
  114. return true;
  115. }
  116. // Validate starts with ":"
  117. if (c != ':')
  118. {
  119. result = default;
  120. rbrace = default;
  121. return false;
  122. }
  123. // Read the specifiers
  124. var specifiers = new StringBuilder();
  125. var index = startIndex + 1;
  126. while (true)
  127. {
  128. // Validate not the end of the string
  129. if (index >= str.Length)
  130. {
  131. result = default;
  132. rbrace = default;
  133. return false;
  134. }
  135. c = str[index];
  136. // Not right-brace
  137. if (c != '}')
  138. {
  139. specifiers.Append(c);
  140. index++;
  141. }
  142. // Escaped right-brace
  143. else if (SafeCharAt(str, index + 1) == '}')
  144. {
  145. specifiers.Append('}');
  146. index += 2;
  147. }
  148. // Closing right-brace
  149. else
  150. {
  151. result = specifiers.ToString();
  152. rbrace = index;
  153. return true;
  154. }
  155. }
  156. }
  157. private Char SafeCharAt(
  158. String str,
  159. Int32 index)
  160. {
  161. if (str.Length > index)
  162. {
  163. return str[index];
  164. }
  165. return '\0';
  166. }
  167. private sealed class FormatResultBuilder
  168. {
  169. internal FormatResultBuilder(
  170. Format node,
  171. EvaluationContext context,
  172. MemoryCounter counter)
  173. {
  174. m_node = node;
  175. m_context = context;
  176. m_counter = counter;
  177. m_cache = new ArgValue[node.Parameters.Count - 1];
  178. }
  179. // Build the final string. This is when lazy segments are evaluated.
  180. public override String ToString()
  181. {
  182. return String.Join(
  183. String.Empty,
  184. m_segments.Select(obj =>
  185. {
  186. if (obj is Lazy<String> lazy)
  187. {
  188. return lazy.Value;
  189. }
  190. else
  191. {
  192. return obj as String;
  193. }
  194. }));
  195. }
  196. // Append a static value
  197. internal void Append(String value)
  198. {
  199. if (value?.Length > 0)
  200. {
  201. // Track memory
  202. m_counter.Add(value);
  203. // Append the segment
  204. m_segments.Add(value);
  205. }
  206. }
  207. // Append an argument
  208. internal void Append(
  209. Int32 argIndex,
  210. String formatSpecifiers)
  211. {
  212. // Delay execution until the final ToString
  213. m_segments.Add(new Lazy<String>(() =>
  214. {
  215. String result;
  216. // Get the arg from the cache
  217. var argValue = m_cache[argIndex];
  218. // Evaluate the arg and cache the result
  219. if (argValue == null)
  220. {
  221. // The evaluation result is required when format specifiers are used. Otherwise the string
  222. // result is required. Go ahead and store both values. Since ConvertToString produces tracing,
  223. // we need to run that now so the tracing appears in order in the log.
  224. var evaluationResult = m_node.Parameters[argIndex + 1].Evaluate(m_context);
  225. var stringResult = evaluationResult.ConvertToString();
  226. argValue = new ArgValue(evaluationResult, stringResult);
  227. m_cache[argIndex] = argValue;
  228. }
  229. // No format specifiers
  230. if (String.IsNullOrEmpty(formatSpecifiers))
  231. {
  232. result = argValue.StringResult;
  233. }
  234. // Invalid
  235. else
  236. {
  237. throw new FormatException(ExpressionResources.InvalidFormatSpecifiers(formatSpecifiers, argValue.EvaluationResult.Kind));
  238. }
  239. // Track memory
  240. if (!String.IsNullOrEmpty(result))
  241. {
  242. m_counter.Add(result);
  243. }
  244. return result;
  245. }));
  246. }
  247. private readonly ArgValue[] m_cache;
  248. private readonly EvaluationContext m_context;
  249. private readonly MemoryCounter m_counter;
  250. private readonly Format m_node;
  251. private readonly List<Object> m_segments = new List<Object>();
  252. }
  253. /// <summary>
  254. /// Stores an EvaluateResult and the value converted to a String.
  255. /// </summary>
  256. private sealed class ArgValue
  257. {
  258. public ArgValue(
  259. EvaluationResult evaluationResult,
  260. String stringResult)
  261. {
  262. EvaluationResult = evaluationResult;
  263. StringResult = stringResult;
  264. }
  265. public EvaluationResult EvaluationResult { get; }
  266. public String StringResult { get; }
  267. }
  268. }
  269. }