aboutsummaryrefslogtreecommitdiff
path: root/src/WixToolset.Core/ExtensibilityServices/PreprocessHelper.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/WixToolset.Core/ExtensibilityServices/PreprocessHelper.cs')
-rw-r--r--src/WixToolset.Core/ExtensibilityServices/PreprocessHelper.cs479
1 files changed, 479 insertions, 0 deletions
diff --git a/src/WixToolset.Core/ExtensibilityServices/PreprocessHelper.cs b/src/WixToolset.Core/ExtensibilityServices/PreprocessHelper.cs
new file mode 100644
index 00000000..3b8011c4
--- /dev/null
+++ b/src/WixToolset.Core/ExtensibilityServices/PreprocessHelper.cs
@@ -0,0 +1,479 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Core.ExtensibilityServices
4{
5 using System;
6 using System.Collections.Generic;
7 using System.IO;
8 using System.Text;
9 using System.Xml.Linq;
10 using WixToolset.Data;
11 using WixToolset.Extensibility;
12 using WixToolset.Extensibility.Services;
13
14 internal class PreprocessHelper : IPreprocessHelper
15 {
16 private static readonly char[] VariableSplitter = new char[] { '.' };
17 private static readonly char[] ArgumentSplitter = new char[] { ',' };
18
19 public PreprocessHelper(IServiceProvider serviceProvider)
20 {
21 this.ServiceProvider = serviceProvider;
22 }
23
24 private IServiceProvider ServiceProvider { get; }
25
26 private Dictionary<string, IPreprocessorExtension> ExtensionsByPrefix { get; set; }
27
28 public void AddVariable(IPreprocessContext context, string name, string value)
29 {
30 this.AddVariable(context, name, value, true);
31 }
32
33 public void AddVariable(IPreprocessContext context, string name, string value, bool showWarning)
34 {
35 var currentValue = this.GetVariableValue(context, "var", name);
36
37 if (null == currentValue)
38 {
39 context.Variables.Add(name, value);
40 }
41 else
42 {
43 if (showWarning)
44 {
45 context.Messaging.OnMessage(WixWarnings.VariableDeclarationCollision(context.CurrentSourceLineNumber, name, value, currentValue));
46 }
47
48 context.Variables[name] = value;
49 }
50 }
51
52 public string EvaluateFunction(IPreprocessContext context, string function)
53 {
54 var prefixParts = function.Split(VariableSplitter, 2);
55
56 // Check to make sure there are 2 parts and neither is an empty string.
57 if (2 != prefixParts.Length || 0 >= prefixParts[0].Length || 0 >= prefixParts[1].Length)
58 {
59 throw new WixException(WixErrors.InvalidPreprocessorFunction(context.CurrentSourceLineNumber, function));
60 }
61
62 var prefix = prefixParts[0];
63 var functionParts = prefixParts[1].Split(new char[] { '(' }, 2);
64
65 // Check to make sure there are 2 parts, neither is an empty string, and the second part ends with a closing paren.
66 if (2 != functionParts.Length || 0 >= functionParts[0].Length || 0 >= functionParts[1].Length || !functionParts[1].EndsWith(")", StringComparison.Ordinal))
67 {
68 throw new WixException(WixErrors.InvalidPreprocessorFunction(context.CurrentSourceLineNumber, function));
69 }
70
71 var functionName = functionParts[0];
72
73 // Remove the trailing closing paren.
74 var allArgs = functionParts[1].Substring(0, functionParts[1].Length - 1);
75
76 // Parse the arguments and preprocess them.
77 var args = allArgs.Split(ArgumentSplitter);
78 for (var i = 0; i < args.Length; i++)
79 {
80 args[i] = this.PreprocessString(context, args[i].Trim());
81 }
82
83 var result = this.EvaluateFunction(context, prefix, functionName, args);
84
85 // If the function didn't evaluate, try to evaluate the original value as a variable to support
86 // the use of open and closed parens inside variable names. Example: $(env.ProgramFiles(x86)) should resolve.
87 if (result == null)
88 {
89 result = this.GetVariableValue(context, function, false);
90 }
91
92 return result;
93 }
94
95 public string EvaluateFunction(IPreprocessContext context, string prefix, string function, string[] args)
96 {
97 if (String.IsNullOrEmpty(prefix))
98 {
99 throw new ArgumentNullException("prefix");
100 }
101
102 if (String.IsNullOrEmpty(function))
103 {
104 throw new ArgumentNullException("function");
105 }
106
107 switch (prefix)
108 {
109 case "fun":
110 switch (function)
111 {
112 case "AutoVersion":
113 // Make sure the base version is specified
114 if (args.Length == 0 || String.IsNullOrEmpty(args[0]))
115 {
116 throw new WixException(WixErrors.InvalidPreprocessorFunctionAutoVersion(context.CurrentSourceLineNumber));
117 }
118
119 // Build = days since 1/1/2000; Revision = seconds since midnight / 2
120 var now = DateTime.UtcNow;
121 var build = now - new DateTime(2000, 1, 1);
122 var revision = now - new DateTime(now.Year, now.Month, now.Day);
123
124 return String.Join(".", args[0], (int)build.TotalDays, (int)(revision.TotalSeconds / 2));
125
126 default:
127 return null;
128 }
129
130 default:
131 var extensionsByPrefix = this.GetExtensionsByPrefix(context);
132 if (extensionsByPrefix.TryGetValue(prefix, out var extension))
133 {
134 try
135 {
136 return extension.EvaluateFunction(prefix, function, args);
137 }
138 catch (Exception e)
139 {
140 throw new WixException(WixErrors.PreprocessorExtensionEvaluateFunctionFailed(context.CurrentSourceLineNumber, prefix, function, String.Join(",", args), e.Message));
141 }
142 }
143 else
144 {
145 return null;
146 }
147 }
148 }
149
150 public string GetVariableValue(IPreprocessContext context, string variable, bool allowMissingPrefix)
151 {
152 // Strip the "$(" off the front.
153 if (variable.StartsWith("$(", StringComparison.Ordinal))
154 {
155 variable = variable.Substring(2);
156 }
157
158 var parts = variable.Split(VariableSplitter, 2);
159
160 if (1 == parts.Length) // missing prefix
161 {
162 if (allowMissingPrefix)
163 {
164 return this.GetVariableValue(context, "var", parts[0]);
165 }
166 else
167 {
168 throw new WixException(WixErrors.InvalidPreprocessorVariable(context.CurrentSourceLineNumber, variable));
169 }
170 }
171 else
172 {
173 // check for empty variable name
174 if (0 < parts[1].Length)
175 {
176 string result = this.GetVariableValue(context, parts[0], parts[1]);
177
178 // If we didn't find it and we allow missing prefixes and the variable contains a dot, perhaps the dot isn't intended to indicate a prefix
179 if (null == result && allowMissingPrefix && variable.Contains("."))
180 {
181 result = this.GetVariableValue(context, "var", variable);
182 }
183
184 return result;
185 }
186 else
187 {
188 throw new WixException(WixErrors.InvalidPreprocessorVariable(context.CurrentSourceLineNumber, variable));
189 }
190 }
191 }
192
193 public string GetVariableValue(IPreprocessContext context, string prefix, string name)
194 {
195 if (String.IsNullOrEmpty(prefix))
196 {
197 throw new ArgumentNullException("prefix");
198 }
199
200 if (String.IsNullOrEmpty(name))
201 {
202 throw new ArgumentNullException("name");
203 }
204
205 switch (prefix)
206 {
207 case "env":
208 return Environment.GetEnvironmentVariable(name);
209
210 case "sys":
211 switch (name)
212 {
213 case "CURRENTDIR":
214 return String.Concat(Directory.GetCurrentDirectory(), Path.DirectorySeparatorChar);
215
216 case "SOURCEFILEDIR":
217 return String.Concat(Path.GetDirectoryName(context.CurrentSourceLineNumber.FileName), Path.DirectorySeparatorChar);
218
219 case "SOURCEFILEPATH":
220 return context.CurrentSourceLineNumber.FileName;
221
222 case "PLATFORM":
223 context.Messaging.OnMessage(WixWarnings.DeprecatedPreProcVariable(context.CurrentSourceLineNumber, "$(sys.PLATFORM)", "$(sys.BUILDARCH)"));
224
225 goto case "BUILDARCH";
226
227 case "BUILDARCH":
228 switch (context.Platform)
229 {
230 case Platform.X86:
231 return "x86";
232
233 case Platform.X64:
234 return "x64";
235
236 case Platform.IA64:
237 return "ia64";
238
239 case Platform.ARM:
240 return "arm";
241
242 default:
243 throw new ArgumentException(WixStrings.EXP_UnknownPlatformEnum, context.Platform.ToString());
244 }
245
246 default:
247 return null;
248 }
249
250 case "var":
251 return context.Variables.TryGetValue(name, out var result) ? result : null;
252
253 default:
254 var extensionsByPrefix = this.GetExtensionsByPrefix(context);
255 if (extensionsByPrefix.TryGetValue(prefix, out var extension))
256 {
257 try
258 {
259 return extension.GetVariableValue(prefix, name);
260 }
261 catch (Exception e)
262 {
263 throw new WixException(WixErrors.PreprocessorExtensionGetVariableValueFailed(context.CurrentSourceLineNumber, prefix, name, e.Message));
264 }
265 }
266 else
267 {
268 return null;
269 }
270 }
271 }
272
273 public void PreprocessPragma(IPreprocessContext context, string pragmaName, string args, XContainer parent)
274 {
275 var prefixParts = pragmaName.Split(VariableSplitter, 2);
276
277 // Check to make sure there are 2 parts and neither is an empty string.
278 if (2 != prefixParts.Length)
279 {
280 throw new WixException(WixErrors.InvalidPreprocessorPragma(context.CurrentSourceLineNumber, pragmaName));
281 }
282
283 var prefix = prefixParts[0];
284 var pragma = prefixParts[1];
285
286 if (String.IsNullOrEmpty(prefix) || String.IsNullOrEmpty(pragma))
287 {
288 throw new WixException(WixErrors.InvalidPreprocessorPragma(context.CurrentSourceLineNumber, pragmaName));
289 }
290
291 switch (prefix)
292 {
293 case "wix":
294 switch (pragma)
295 {
296 // Add any core defined pragmas here
297 default:
298 context.Messaging.OnMessage(WixWarnings.PreprocessorUnknownPragma(context.CurrentSourceLineNumber, pragmaName));
299 break;
300 }
301 break;
302
303 default:
304 var extensionsByPrefix = this.GetExtensionsByPrefix(context);
305 if (extensionsByPrefix.TryGetValue(prefix, out var extension))
306 {
307 if (!extension.ProcessPragma(prefix, pragma, args, parent))
308 {
309 context.Messaging.OnMessage(WixWarnings.PreprocessorUnknownPragma(context.CurrentSourceLineNumber, pragmaName));
310 }
311 }
312 break;
313 }
314 }
315
316 public string PreprocessString(IPreprocessContext context, string value)
317 {
318 var sb = new StringBuilder();
319 var currentPosition = 0;
320 var end = 0;
321
322 while (-1 != (currentPosition = value.IndexOf('$', end)))
323 {
324 if (end < currentPosition)
325 {
326 sb.Append(value, end, currentPosition - end);
327 }
328
329 end = currentPosition + 1;
330
331 var remainder = value.Substring(end);
332 if (remainder.StartsWith("$", StringComparison.Ordinal))
333 {
334 sb.Append("$");
335 end++;
336 }
337 else if (remainder.StartsWith("(loc.", StringComparison.Ordinal))
338 {
339 currentPosition = remainder.IndexOf(')');
340 if (-1 == currentPosition)
341 {
342 context.Messaging.OnMessage(WixErrors.InvalidPreprocessorVariable(context.CurrentSourceLineNumber, remainder));
343 break;
344 }
345
346 sb.Append("$"); // just put the resource reference back as was
347 sb.Append(remainder, 0, currentPosition + 1);
348
349 end += currentPosition + 1;
350 }
351 else if (remainder.StartsWith("(", StringComparison.Ordinal))
352 {
353 var openParenCount = 1;
354 var closingParenCount = 0;
355 var isFunction = false;
356 var foundClosingParen = false;
357
358 // find the closing paren
359 int closingParenPosition;
360 for (closingParenPosition = 1; closingParenPosition < remainder.Length; closingParenPosition++)
361 {
362 switch (remainder[closingParenPosition])
363 {
364 case '(':
365 openParenCount++;
366 isFunction = true;
367 break;
368
369 case ')':
370 closingParenCount++;
371 break;
372 }
373
374 if (openParenCount == closingParenCount)
375 {
376 foundClosingParen = true;
377 break;
378 }
379 }
380
381 // move the currentPosition to the closing paren
382 currentPosition += closingParenPosition;
383
384 if (!foundClosingParen)
385 {
386 if (isFunction)
387 {
388 context.Messaging.OnMessage(WixErrors.InvalidPreprocessorFunction(context.CurrentSourceLineNumber, remainder));
389 break;
390 }
391 else
392 {
393 context.Messaging.OnMessage(WixErrors.InvalidPreprocessorVariable(context.CurrentSourceLineNumber, remainder));
394 break;
395 }
396 }
397
398 var subString = remainder.Substring(1, closingParenPosition - 1);
399 string result = null;
400 if (isFunction)
401 {
402 result = this.EvaluateFunction(context, subString);
403 }
404 else
405 {
406 result = this.GetVariableValue(context, subString, false);
407 }
408
409 if (null == result)
410 {
411 if (isFunction)
412 {
413 context.Messaging.OnMessage(WixErrors.UndefinedPreprocessorFunction(context.CurrentSourceLineNumber, subString));
414 break;
415 }
416 else
417 {
418 context.Messaging.OnMessage(WixErrors.UndefinedPreprocessorVariable(context.CurrentSourceLineNumber, subString));
419 break;
420 }
421 }
422 else
423 {
424 if (!isFunction)
425 {
426 //this.OnResolvedVariable(new ResolvedVariableEventArgs(context.CurrentSourceLineNumber, subString, result));
427 }
428 }
429
430 sb.Append(result);
431 end += closingParenPosition + 1;
432 }
433 else // just a floating "$" so put it in the final string (i.e. leave it alone) and keep processing
434 {
435 sb.Append('$');
436 }
437 }
438
439 if (end < value.Length)
440 {
441 sb.Append(value.Substring(end));
442 }
443
444 return sb.ToString();
445 }
446
447 public void RemoveVariable(IPreprocessContext context, string name)
448 {
449 if (!context.Variables.Remove(name))
450 {
451 context.Messaging.OnMessage(WixErrors.CannotReundefineVariable(context.CurrentSourceLineNumber, name));
452 }
453 }
454
455 private Dictionary<string, IPreprocessorExtension> GetExtensionsByPrefix(IPreprocessContext context)
456 {
457 if (this.ExtensionsByPrefix == null)
458 {
459 this.ExtensionsByPrefix = new Dictionary<string, IPreprocessorExtension>();
460
461 foreach (var extension in context.Extensions)
462 {
463 if (null != extension.Prefixes)
464 {
465 foreach (string prefix in extension.Prefixes)
466 {
467 if (!this.ExtensionsByPrefix.ContainsKey(prefix))
468 {
469 this.ExtensionsByPrefix.Add(prefix, extension);
470 }
471 }
472 }
473 }
474 }
475
476 return this.ExtensionsByPrefix;
477 }
478 }
479}