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