main
cs 441 lines 17.4 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.Diagnostics;
8 using System.IO;
9 using System.Text;
10 using WixToolset.Data;
11 using WixToolset.Extensibility.Data;
12 using WixToolset.Extensibility.Services;
13
14 internal class BundleValidator : IBundleValidator
15 {
16 public BundleValidator(IServiceProvider serviceProvider)
17 {
18 this.Messaging = serviceProvider.GetService<IMessaging>();
19 }
20
21 protected IMessaging Messaging { get; }
22
23 private enum ValueListKind
24 {
25 /// <summary>
26 /// A list of values with nothing before the final value.
27 /// </summary>
28 None,
29
30 /// <summary>
31 /// A list of values with 'and' before the final value.
32 /// </summary>
33 And,
34
35 /// <summary>
36 /// A list of values with 'or' before the final value.
37 /// </summary>
38 Or,
39 }
40
41 // Built-in variables (from burn\engine\variable.cpp, "vrgBuiltInVariables", around line 225)
42 private static readonly List<string> BuiltinBundleVariables = new List<string>(
43 new string[] {
44 "AdminToolsFolder",
45 "AppDataFolder",
46 "CommonAppDataFolder",
47 "CommonFiles64Folder",
48 "CommonFiles6432Folder",
49 "CommonFilesFolder",
50 "CompatibilityMode",
51 "ComputerName",
52 "Date",
53 "DesktopFolder",
54 "FavoritesFolder",
55 "FontsFolder",
56 "InstallerName",
57 "InstallerVersion",
58 "LocalAppDataFolder",
59 "LogonUser",
60 "MyPicturesFolder",
61 "NativeMachine",
62 "NTProductType",
63 "NTSuiteBackOffice",
64 "NTSuiteDataCenter",
65 "NTSuiteEnterprise",
66 "NTSuitePersonal",
67 "NTSuiteSmallBusiness",
68 "NTSuiteSmallBusinessRestricted",
69 "NTSuiteWebServer",
70 "PersonalFolder",
71 "Privileged",
72 "ProcessorArchitecture",
73 "ProgramFiles64Folder",
74 "ProgramFiles6432Folder",
75 "ProgramFilesFolder",
76 "ProgramMenuFolder",
77 "RebootPending",
78 "SendToFolder",
79 "ServicePackLevel",
80 "StartMenuFolder",
81 "StartupFolder",
82 "System64Folder",
83 "SystemFolder",
84 "SystemLanguageID",
85 "TempFolder",
86 "TemplateFolder",
87 "TerminalServer",
88 "UserLanguageID",
89 "UserUILanguageID",
90 "VersionMsi",
91 "VersionNT",
92 "VersionNT64",
93 "WindowsBuildNumber",
94 "WindowsFolder",
95 "WindowsVolume",
96 "WixBundleAction",
97 "WixBundleActiveParent",
98 "WixBundleCommandLineAction",
99 "WixBundleElevated",
100 "WixBundleExecutePackageAction",
101 "WixBundleExecutePackageCacheFolder",
102 "WixBundleForcedRestartPackage",
103 "WixBundleInstalled",
104 "WixBundleProviderKey",
105 "WixBundleSourceProcessFolder",
106 "WixBundleSourceProcessPath",
107 "WixBundleTag",
108 "WixBundleUILevel",
109 "WixBundleVersion",
110 });
111
112 // Well-known variables (from burn\engine\variable.cpp, "vrgWellKnownVariables", around line 304)
113 private static readonly List<string> WellKnownBundleVariables = new List<string>(
114 new string[] {
115 "WixBundleInProgressName",
116 "WixBundleLastUsedSource",
117 "WixBundleLayoutDirectory",
118 "WixBundleLog",
119 "WixBundleLog_*",
120 "WixBundleRollbackLog_*",
121 "WixBundleManufacturer",
122 "WixBundleName",
123 "WixBundleOriginalSource",
124 "WixBundleOriginalSourceFolder",
125 });
126
127 private static readonly List<string> DisallowedMsiProperties = new List<string>(
128 new string[] {
129 "ACTION",
130 "ADDLOCAL",
131 "ADDSOURCE",
132 "ADDDEFAULT",
133 "ADVERTISE",
134 "ALLUSERS",
135 "REBOOT",
136 "REINSTALL",
137 "REINSTALLMODE",
138 "REMOVE"
139 });
140
141 private static readonly List<string> UnavailableStartupVariables = new List<string>(
142 new string[] {
143 "RebootPending",
144 "WixBundleAction",
145 "WixBundleInstalled",
146 });
147
148 private static readonly List<string> UnavailableDetectVariables = new List<string>(
149 new string[] {
150 "WixBundleAction",
151 });
152
153 public string GetCanonicalRelativePath(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string relativePath)
154 {
155 const string root = @"C:\";
156 if (!Path.IsPathRooted(relativePath))
157 {
158 var normalizedPath = Path.GetFullPath(root + relativePath);
159 if (normalizedPath.StartsWith(root))
160 {
161 var canonicalizedPath = normalizedPath.Substring(root.Length);
162 if (canonicalizedPath != relativePath)
163 {
164 this.Messaging.Write(WarningMessages.PathCanonicalized(sourceLineNumbers, elementName, attributeName, relativePath, canonicalizedPath));
165 }
166 return canonicalizedPath;
167 }
168 }
169
170 this.Messaging.Write(ErrorMessages.PayloadMustBeRelativeToCache(sourceLineNumbers, elementName, attributeName, relativePath));
171 return relativePath;
172 }
173
174 public bool ValidateBundleVariableNameDeclaration(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string variableName)
175 {
176 if (String.IsNullOrEmpty(variableName))
177 {
178 this.Messaging.Write(ErrorMessages.IllegalEmptyAttributeValue(sourceLineNumbers, elementName, attributeName));
179
180 return false;
181 }
182 else if (!Common.IsBundleVariableName(variableName))
183 {
184 this.Messaging.Write(CompilerErrors.IllegalBundleVariableName(sourceLineNumbers, elementName, attributeName, variableName));
185
186 return false;
187 }
188 else if (BuiltinBundleVariables.Contains(variableName))
189 {
190 var illegalValues = CreateValueList(ValueListKind.Or, BuiltinBundleVariables);
191 this.Messaging.Write(ErrorMessages.IllegalAttributeValueWithIllegalList(sourceLineNumbers, elementName, attributeName, variableName, illegalValues));
192
193 return false;
194 }
195 else
196 {
197 return true;
198 }
199 }
200
201 public bool ValidateBundleVariableNameValue(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string variableName, BundleVariableNameRule nameRule)
202 {
203 if (String.IsNullOrEmpty(variableName))
204 {
205 this.Messaging.Write(ErrorMessages.IllegalEmptyAttributeValue(sourceLineNumbers, elementName, attributeName));
206
207 return false;
208 }
209 else if (!Common.IsBundleVariableName(variableName))
210 {
211 this.Messaging.Write(CompilerErrors.IllegalBundleVariableName(sourceLineNumbers, elementName, attributeName, variableName));
212
213 return false;
214 }
215 else if (BuiltinBundleVariables.Contains(variableName))
216 {
217 var allowed = nameRule.HasFlag(BundleVariableNameRule.CanBeBuiltIn);
218 if (!allowed)
219 {
220 var illegalValues = CreateValueList(ValueListKind.Or, BuiltinBundleVariables);
221 this.Messaging.Write(ErrorMessages.IllegalAttributeValueWithIllegalList(sourceLineNumbers, elementName, attributeName, variableName, illegalValues));
222 }
223
224 return allowed;
225 }
226 else if (WellKnownBundleVariables.Contains(variableName) ||
227 variableName.StartsWith("WixBundleLog_", StringComparison.OrdinalIgnoreCase) ||
228 variableName.StartsWith("WixBundleRollbackLog_", StringComparison.OrdinalIgnoreCase))
229 {
230 var allowed = nameRule.HasFlag(BundleVariableNameRule.CanBeWellKnown);
231 if (!allowed)
232 {
233 var illegalValues = CreateValueList(ValueListKind.Or, WellKnownBundleVariables);
234 this.Messaging.Write(ErrorMessages.IllegalAttributeValueWithIllegalList(sourceLineNumbers, elementName, attributeName, variableName, illegalValues));
235 }
236
237 return allowed;
238 }
239 else
240 {
241 return true;
242 }
243 }
244
245 public bool ValidateBundleVariableNameTarget(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string variableName)
246 {
247 if (String.IsNullOrEmpty(variableName))
248 {
249 this.Messaging.Write(ErrorMessages.IllegalEmptyAttributeValue(sourceLineNumbers, elementName, attributeName));
250
251 return false;
252 }
253 else if (!Common.IsBundleVariableName(variableName))
254 {
255 this.Messaging.Write(CompilerErrors.IllegalBundleVariableName(sourceLineNumbers, elementName, attributeName, variableName));
256
257 return false;
258 }
259 else if (BuiltinBundleVariables.Contains(variableName))
260 {
261 var illegalValues = CreateValueList(ValueListKind.Or, BuiltinBundleVariables);
262 this.Messaging.Write(ErrorMessages.IllegalAttributeValueWithIllegalList(sourceLineNumbers, elementName, attributeName, variableName, illegalValues));
263
264 return false;
265 }
266 else if (variableName.Equals("WixBundleLog", StringComparison.OrdinalIgnoreCase) ||
267 variableName.StartsWith("WixBundleLog_", StringComparison.OrdinalIgnoreCase) ||
268 variableName.StartsWith("WixBundleRollbackLog_", StringComparison.OrdinalIgnoreCase))
269 {
270 this.Messaging.Write(CompilerWarnings.ReadonlyLogVariableTarget(sourceLineNumbers, elementName, attributeName, variableName));
271
272 return true;
273 }
274 else if (WellKnownBundleVariables.Contains(variableName))
275 {
276 return true;
277 }
278 else
279 {
280 return true;
281 }
282 }
283
284 public bool ValidateBundleMsiPropertyName(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string propertyName)
285 {
286 if (String.IsNullOrEmpty(propertyName))
287 {
288 this.Messaging.Write(ErrorMessages.IllegalEmptyAttributeValue(sourceLineNumbers, elementName, attributeName));
289
290 return false;
291 }
292 else if (DisallowedMsiProperties.Contains(propertyName))
293 {
294 var illegalValues = CreateValueList(ValueListKind.Or, DisallowedMsiProperties);
295 this.Messaging.Write(ErrorMessages.DisallowedMsiProperty(sourceLineNumbers, propertyName, illegalValues));
296
297 return false;
298 }
299 else
300 {
301 return true;
302 }
303 }
304
305 public bool ValidateBundleCondition(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string condition, BundleConditionPhase phase)
306 {
307 if (!this.TryParseCondition(sourceLineNumbers, elementName, attributeName, condition))
308 {
309 return false;
310 }
311
312
313 // TODO: These lists are incomplete.
314 List<string> unavailableVariables = null;
315 switch (phase)
316 {
317 case BundleConditionPhase.Startup:
318 unavailableVariables = UnavailableStartupVariables;
319 break;
320 case BundleConditionPhase.Detect:
321 unavailableVariables = UnavailableDetectVariables;
322 break;
323 }
324
325 if (unavailableVariables != null)
326 {
327 return this.ValidateBundleConditionUnavailableVariables(sourceLineNumbers, elementName, attributeName, condition, unavailableVariables);
328 }
329 else
330 {
331 return true;
332 }
333 }
334
335 private bool ValidateBundleConditionUnavailableVariables(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string condition, List<string> unavailableVariables)
336 {
337 foreach (var variableName in unavailableVariables)
338 {
339 //TODO: use the results of parsing to validate that the restricted variables are actually used as variables
340 if (condition.Contains(variableName))
341 {
342 var illegalValues = CreateValueList(ValueListKind.Or, unavailableVariables);
343 this.Messaging.Write(WarningMessages.UnavailableBundleConditionVariable(sourceLineNumbers, elementName, attributeName, variableName, illegalValues));
344
345 return false;
346 }
347 }
348
349 return true;
350 }
351
352 private bool TryParseCondition(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string condition)
353 {
354 if (String.IsNullOrEmpty(condition))
355 {
356 this.Messaging.Write(ErrorMessages.IllegalEmptyAttributeValue(sourceLineNumbers, elementName, attributeName));
357
358 return false;
359 }
360 //TODO: Actually parse the condition to definitively tell which Variables are referenced.
361 else if (condition.Trim() == "=")
362 {
363 this.Messaging.Write(ErrorMessages.InvalidBundleCondition(sourceLineNumbers, elementName, attributeName, condition));
364 return false;
365 }
366 else
367 {
368 return true;
369 }
370 }
371
372 private static string CreateValueList(ValueListKind kind, IEnumerable<string> values)
373 {
374 // Ideally, we could denote the list kind (and the list itself) directly in the
375 // message XML, and detect and expand in the MessageHandler.GenerateMessageString()
376 // method. Doing so would make vararg-style messages much easier, but impacts
377 // every single message we format. For now, callers just have to know when a
378 // message takes a list of values in a single string argument, the caller will
379 // have to do the expansion themselves. (And, unfortunately, hard-code the knowledge
380 // that the list is an 'and' or 'or' list.)
381
382 // For a localizable solution, we need to be able to get the list format string
383 // from resources. We aren't currently localized right now, so the values are
384 // just hard-coded.
385 const string valueFormat = "'{0}'";
386 const string valueSeparator = ", ";
387 var terminalTerm = String.Empty;
388
389 switch (kind)
390 {
391 case ValueListKind.None:
392 terminalTerm = "";
393 break;
394 case ValueListKind.And:
395 terminalTerm = "and ";
396 break;
397 case ValueListKind.Or:
398 terminalTerm = "or ";
399 break;
400 }
401
402 var list = new StringBuilder();
403
404 // This weird construction helps us determine when we're adding the last value
405 // to the list. Instead of adding them as we encounter them, we cache the current
406 // value and append the *previous* one.
407 string previousValue = null;
408 var haveValues = false;
409 foreach (var value in values)
410 {
411 if (null != previousValue)
412 {
413 if (haveValues)
414 {
415 list.Append(valueSeparator);
416 }
417 list.AppendFormat(valueFormat, previousValue);
418 haveValues = true;
419 }
420
421 previousValue = value;
422 }
423
424 // If we have no previous value, that means that the list contained no values, and
425 // something has gone very wrong.
426 Debug.Assert(null != previousValue);
427 if (null != previousValue)
428 {
429 if (haveValues)
430 {
431 list.Append(valueSeparator);
432 list.Append(terminalTerm);
433 }
434 list.AppendFormat(valueFormat, previousValue);
435 //haveValues = true;
436 }
437
438 return list.ToString();
439 }
440 }
441 }