aboutsummaryrefslogtreecommitdiff
path: root/src/WixToolset.Core.Burn/Bundles/ProcessMsiPackageCommand.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/WixToolset.Core.Burn/Bundles/ProcessMsiPackageCommand.cs')
-rw-r--r--src/WixToolset.Core.Burn/Bundles/ProcessMsiPackageCommand.cs576
1 files changed, 576 insertions, 0 deletions
diff --git a/src/WixToolset.Core.Burn/Bundles/ProcessMsiPackageCommand.cs b/src/WixToolset.Core.Burn/Bundles/ProcessMsiPackageCommand.cs
new file mode 100644
index 00000000..322187f9
--- /dev/null
+++ b/src/WixToolset.Core.Burn/Bundles/ProcessMsiPackageCommand.cs
@@ -0,0 +1,576 @@
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.Burn.Bundles
4{
5 using System;
6 using System.Collections;
7 using System.Collections.Generic;
8 using System.Diagnostics;
9 using System.Globalization;
10 using System.IO;
11 using System.Linq;
12 using WixToolset.Data;
13 using WixToolset.Data.Rows;
14 using WixToolset.Extensibility;
15 using WixToolset.Core.Native;
16 using Dtf = WixToolset.Dtf.WindowsInstaller;
17 using WixToolset.Bind;
18 using WixToolset.Data.Bind;
19
20 /// <summary>
21 /// Initializes package state from the MSI contents.
22 /// </summary>
23 internal class ProcessMsiPackageCommand
24 {
25 private const string PropertySqlFormat = "SELECT `Value` FROM `Property` WHERE `Property` = '{0}'";
26
27 public RowDictionary<WixBundlePayloadRow> AuthoredPayloads { private get; set; }
28
29 public PackageFacade Facade { private get; set; }
30
31 public IEnumerable<IBurnBackendExtension> BackendExtensions { private get; set; }
32
33 public Table MsiFeatureTable { private get; set; }
34
35 public Table MsiPropertyTable { private get; set; }
36
37 public Table PayloadTable { private get; set; }
38
39 public Table RelatedPackageTable { private get; set; }
40
41 /// <summary>
42 /// Processes the MSI packages to add properties and payloads from the MSI packages.
43 /// </summary>
44 public void Execute()
45 {
46 WixBundlePayloadRow packagePayload = this.AuthoredPayloads.Get(this.Facade.Package.PackagePayload);
47
48 string sourcePath = packagePayload.FullFileName;
49 bool longNamesInImage = false;
50 bool compressed = false;
51 bool x64 = false;
52 try
53 {
54 // Read data out of the msi database...
55 using (Dtf.SummaryInfo sumInfo = new Dtf.SummaryInfo(sourcePath, false))
56 {
57 // 1 is the Word Count summary information stream bit that means
58 // the MSI uses short file names when set. We care about long file
59 // names so check when the bit is not set.
60 longNamesInImage = 0 == (sumInfo.WordCount & 1);
61
62 // 2 is the Word Count summary information stream bit that means
63 // files are compressed in the MSI by default when the bit is set.
64 compressed = 2 == (sumInfo.WordCount & 2);
65
66 x64 = (sumInfo.Template.Contains("x64") || sumInfo.Template.Contains("Intel64"));
67
68 // 8 is the Word Count summary information stream bit that means
69 // "Elevated privileges are not required to install this package."
70 // in MSI 4.5 and below, if this bit is 0, elevation is required.
71 this.Facade.Package.PerMachine = (0 == (sumInfo.WordCount & 8)) ? YesNoDefaultType.Yes : YesNoDefaultType.No;
72 this.Facade.Package.x64 = x64 ? YesNoType.Yes : YesNoType.No;
73 }
74
75 using (Dtf.Database db = new Dtf.Database(sourcePath))
76 {
77 this.Facade.MsiPackage.ProductCode = ProcessMsiPackageCommand.GetProperty(db, "ProductCode");
78 this.Facade.MsiPackage.UpgradeCode = ProcessMsiPackageCommand.GetProperty(db, "UpgradeCode");
79 this.Facade.MsiPackage.Manufacturer = ProcessMsiPackageCommand.GetProperty(db, "Manufacturer");
80 this.Facade.MsiPackage.ProductLanguage = Convert.ToInt32(ProcessMsiPackageCommand.GetProperty(db, "ProductLanguage"), CultureInfo.InvariantCulture);
81 this.Facade.MsiPackage.ProductVersion = ProcessMsiPackageCommand.GetProperty(db, "ProductVersion");
82
83 if (!Common.IsValidModuleOrBundleVersion(this.Facade.MsiPackage.ProductVersion))
84 {
85 // not a proper .NET version (e.g., five fields); can we get a valid four-part version number?
86 string version = null;
87 string[] versionParts = this.Facade.MsiPackage.ProductVersion.Split('.');
88 int count = versionParts.Length;
89 if (0 < count)
90 {
91 version = versionParts[0];
92 for (int i = 1; i < 4 && i < count; ++i)
93 {
94 version = String.Concat(version, ".", versionParts[i]);
95 }
96 }
97
98 if (!String.IsNullOrEmpty(version) && Common.IsValidModuleOrBundleVersion(version))
99 {
100 Messaging.Instance.OnMessage(WixWarnings.VersionTruncated(this.Facade.Package.SourceLineNumbers, this.Facade.MsiPackage.ProductVersion, sourcePath, version));
101 this.Facade.MsiPackage.ProductVersion = version;
102 }
103 else
104 {
105 Messaging.Instance.OnMessage(WixErrors.InvalidProductVersion(this.Facade.Package.SourceLineNumbers, this.Facade.MsiPackage.ProductVersion, sourcePath));
106 }
107 }
108
109 if (String.IsNullOrEmpty(this.Facade.Package.CacheId))
110 {
111 this.Facade.Package.CacheId = String.Format("{0}v{1}", this.Facade.MsiPackage.ProductCode, this.Facade.MsiPackage.ProductVersion);
112 }
113
114 if (String.IsNullOrEmpty(this.Facade.Package.DisplayName))
115 {
116 this.Facade.Package.DisplayName = ProcessMsiPackageCommand.GetProperty(db, "ProductName");
117 }
118
119 if (String.IsNullOrEmpty(this.Facade.Package.Description))
120 {
121 this.Facade.Package.Description = ProcessMsiPackageCommand.GetProperty(db, "ARPCOMMENTS");
122 }
123
124 ISet<string> payloadNames = this.GetPayloadTargetNames();
125
126 ISet<string> msiPropertyNames = this.GetMsiPropertyNames();
127
128 this.SetPerMachineAppropriately(db, sourcePath);
129
130 // Ensure the MSI package is appropriately marked visible or not.
131 this.SetPackageVisibility(db, msiPropertyNames);
132
133 // Unless the MSI or setup code overrides the default, set MSIFASTINSTALL for best performance.
134 if (!msiPropertyNames.Contains("MSIFASTINSTALL") && !ProcessMsiPackageCommand.HasProperty(db, "MSIFASTINSTALL"))
135 {
136 this.AddMsiProperty("MSIFASTINSTALL", "7");
137 }
138
139 this.CreateRelatedPackages(db);
140
141 // If feature selection is enabled, represent the Feature table in the manifest.
142 if (this.Facade.MsiPackage.EnableFeatureSelection)
143 {
144 this.CreateMsiFeatures(db);
145 }
146
147 // Add all external cabinets as package payloads.
148 this.ImportExternalCabinetAsPayloads(db, packagePayload, payloadNames);
149
150 // Add all external files as package payloads and calculate the total install size as the rollup of
151 // File table's sizes.
152 this.Facade.Package.InstallSize = this.ImportExternalFileAsPayloadsAndReturnInstallSize(db, packagePayload, longNamesInImage, compressed, payloadNames);
153
154 // Add all dependency providers from the MSI.
155 this.ImportDependencyProviders(db);
156 }
157 }
158 catch (Dtf.InstallerException e)
159 {
160 Messaging.Instance.OnMessage(WixErrors.UnableToReadPackageInformation(this.Facade.Package.SourceLineNumbers, sourcePath, e.Message));
161 }
162 }
163
164 private ISet<string> GetPayloadTargetNames()
165 {
166 IEnumerable<string> payloadNames = this.PayloadTable.RowsAs<WixBundlePayloadRow>()
167 .Where(r => r.Package == this.Facade.Package.WixChainItemId)
168 .Select(r => r.Name);
169
170 return new HashSet<string>(payloadNames, StringComparer.OrdinalIgnoreCase);
171 }
172
173 private ISet<string> GetMsiPropertyNames()
174 {
175 IEnumerable<string> properties = this.MsiPropertyTable.RowsAs<WixBundleMsiPropertyRow>()
176 .Where(r => r.ChainPackageId == this.Facade.Package.WixChainItemId)
177 .Select(r => r.Name);
178
179 return new HashSet<string>(properties, StringComparer.Ordinal);
180 }
181
182 private void SetPerMachineAppropriately(Dtf.Database db, string sourcePath)
183 {
184 if (this.Facade.MsiPackage.ForcePerMachine)
185 {
186 if (YesNoDefaultType.No == this.Facade.Package.PerMachine)
187 {
188 Messaging.Instance.OnMessage(WixWarnings.PerUserButForcingPerMachine(this.Facade.Package.SourceLineNumbers, sourcePath));
189 this.Facade.Package.PerMachine = YesNoDefaultType.Yes; // ensure that we think the package is per-machine.
190 }
191
192 // Force ALLUSERS=1 via the MSI command-line.
193 this.AddMsiProperty("ALLUSERS", "1");
194 }
195 else
196 {
197 string allusers = ProcessMsiPackageCommand.GetProperty(db, "ALLUSERS");
198
199 if (String.IsNullOrEmpty(allusers))
200 {
201 // Not forced per-machine and no ALLUSERS property, flip back to per-user.
202 if (YesNoDefaultType.Yes == this.Facade.Package.PerMachine)
203 {
204 Messaging.Instance.OnMessage(WixWarnings.ImplicitlyPerUser(this.Facade.Package.SourceLineNumbers, sourcePath));
205 this.Facade.Package.PerMachine = YesNoDefaultType.No;
206 }
207 }
208 else if (allusers.Equals("1", StringComparison.Ordinal))
209 {
210 if (YesNoDefaultType.No == this.Facade.Package.PerMachine)
211 {
212 Messaging.Instance.OnMessage(WixErrors.PerUserButAllUsersEquals1(this.Facade.Package.SourceLineNumbers, sourcePath));
213 }
214 }
215 else if (allusers.Equals("2", StringComparison.Ordinal))
216 {
217 Messaging.Instance.OnMessage(WixWarnings.DiscouragedAllUsersValue(this.Facade.Package.SourceLineNumbers, sourcePath, (YesNoDefaultType.Yes == this.Facade.Package.PerMachine) ? "machine" : "user"));
218 }
219 else
220 {
221 Messaging.Instance.OnMessage(WixErrors.UnsupportedAllUsersValue(this.Facade.Package.SourceLineNumbers, sourcePath, allusers));
222 }
223 }
224 }
225
226 private void SetPackageVisibility(Dtf.Database db, ISet<string> msiPropertyNames)
227 {
228 bool alreadyVisible = !ProcessMsiPackageCommand.HasProperty(db, "ARPSYSTEMCOMPONENT");
229
230 if (alreadyVisible != this.Facade.Package.Visible) // if not already set to the correct visibility.
231 {
232 // If the authoring specifically added "ARPSYSTEMCOMPONENT", don't do it again.
233 if (!msiPropertyNames.Contains("ARPSYSTEMCOMPONENT"))
234 {
235 this.AddMsiProperty("ARPSYSTEMCOMPONENT", this.Facade.Package.Visible ? String.Empty : "1");
236 }
237 }
238 }
239
240 private void CreateRelatedPackages(Dtf.Database db)
241 {
242 // Represent the Upgrade table as related packages.
243 if (db.Tables.Contains("Upgrade"))
244 {
245 using (Dtf.View view = db.OpenView("SELECT `UpgradeCode`, `VersionMin`, `VersionMax`, `Language`, `Attributes` FROM `Upgrade`"))
246 {
247 view.Execute();
248 while (true)
249 {
250 using (Dtf.Record record = view.Fetch())
251 {
252 if (null == record)
253 {
254 break;
255 }
256
257 WixBundleRelatedPackageRow related = (WixBundleRelatedPackageRow)this.RelatedPackageTable.CreateRow(this.Facade.Package.SourceLineNumbers);
258 related.ChainPackageId = this.Facade.Package.WixChainItemId;
259 related.Id = record.GetString(1);
260 related.MinVersion = record.GetString(2);
261 related.MaxVersion = record.GetString(3);
262 related.Languages = record.GetString(4);
263
264 int attributes = record.GetInteger(5);
265 related.OnlyDetect = (attributes & MsiInterop.MsidbUpgradeAttributesOnlyDetect) == MsiInterop.MsidbUpgradeAttributesOnlyDetect;
266 related.MinInclusive = (attributes & MsiInterop.MsidbUpgradeAttributesVersionMinInclusive) == MsiInterop.MsidbUpgradeAttributesVersionMinInclusive;
267 related.MaxInclusive = (attributes & MsiInterop.MsidbUpgradeAttributesVersionMaxInclusive) == MsiInterop.MsidbUpgradeAttributesVersionMaxInclusive;
268 related.LangInclusive = (attributes & MsiInterop.MsidbUpgradeAttributesLanguagesExclusive) == 0;
269 }
270 }
271 }
272 }
273 }
274
275 private void CreateMsiFeatures(Dtf.Database db)
276 {
277 if (db.Tables.Contains("Feature"))
278 {
279 using (Dtf.View featureView = db.OpenView("SELECT `Component_` FROM `FeatureComponents` WHERE `Feature_` = ?"))
280 using (Dtf.View componentView = db.OpenView("SELECT `FileSize` FROM `File` WHERE `Component_` = ?"))
281 {
282 using (Dtf.Record featureRecord = new Dtf.Record(1))
283 using (Dtf.Record componentRecord = new Dtf.Record(1))
284 {
285 using (Dtf.View allFeaturesView = db.OpenView("SELECT * FROM `Feature`"))
286 {
287 allFeaturesView.Execute();
288
289 while (true)
290 {
291 using (Dtf.Record allFeaturesResultRecord = allFeaturesView.Fetch())
292 {
293 if (null == allFeaturesResultRecord)
294 {
295 break;
296 }
297
298 string featureName = allFeaturesResultRecord.GetString(1);
299
300 // Calculate the Feature size.
301 featureRecord.SetString(1, featureName);
302 featureView.Execute(featureRecord);
303
304 // Loop over all the components for the feature to calculate the size of the feature.
305 long size = 0;
306 while (true)
307 {
308 using (Dtf.Record componentResultRecord = featureView.Fetch())
309 {
310 if (null == componentResultRecord)
311 {
312 break;
313 }
314 string component = componentResultRecord.GetString(1);
315 componentRecord.SetString(1, component);
316 componentView.Execute(componentRecord);
317
318 while (true)
319 {
320 using (Dtf.Record fileResultRecord = componentView.Fetch())
321 {
322 if (null == fileResultRecord)
323 {
324 break;
325 }
326
327 string fileSize = fileResultRecord.GetString(1);
328 size += Convert.ToInt32(fileSize, CultureInfo.InvariantCulture.NumberFormat);
329 }
330 }
331 }
332 }
333
334 WixBundleMsiFeatureRow feature = (WixBundleMsiFeatureRow)this.MsiFeatureTable.CreateRow(this.Facade.Package.SourceLineNumbers);
335 feature.ChainPackageId = this.Facade.Package.WixChainItemId;
336 feature.Name = featureName;
337 feature.Parent = allFeaturesResultRecord.GetString(2);
338 feature.Title = allFeaturesResultRecord.GetString(3);
339 feature.Description = allFeaturesResultRecord.GetString(4);
340 feature.Display = allFeaturesResultRecord.GetInteger(5);
341 feature.Level = allFeaturesResultRecord.GetInteger(6);
342 feature.Directory = allFeaturesResultRecord.GetString(7);
343 feature.Attributes = allFeaturesResultRecord.GetInteger(8);
344 feature.Size = size;
345 }
346 }
347 }
348 }
349 }
350 }
351 }
352
353 private void ImportExternalCabinetAsPayloads(Dtf.Database db, WixBundlePayloadRow packagePayload, ISet<string> payloadNames)
354 {
355 if (db.Tables.Contains("Media"))
356 {
357 foreach (string cabinet in db.ExecuteStringQuery("SELECT `Cabinet` FROM `Media`"))
358 {
359 if (!String.IsNullOrEmpty(cabinet) && !cabinet.StartsWith("#", StringComparison.Ordinal))
360 {
361 // If we didn't find the Payload as an existing child of the package, we need to
362 // add it. We expect the file to exist on-disk in the same relative location as
363 // the MSI expects to find it...
364 string cabinetName = Path.Combine(Path.GetDirectoryName(packagePayload.Name), cabinet);
365
366 if (!payloadNames.Contains(cabinetName))
367 {
368 string generatedId = Common.GenerateIdentifier("cab", packagePayload.Id, cabinet);
369 string payloadSourceFile = this.ResolveRelatedFile(packagePayload.UnresolvedSourceFile, cabinet, "Cabinet", this.Facade.Package.SourceLineNumbers, BindStage.Normal);
370
371 WixBundlePayloadRow payload = (WixBundlePayloadRow)this.PayloadTable.CreateRow(this.Facade.Package.SourceLineNumbers);
372 payload.Id = generatedId;
373 payload.Name = cabinetName;
374 payload.SourceFile = payloadSourceFile;
375 payload.Compressed = packagePayload.Compressed;
376 payload.UnresolvedSourceFile = cabinetName;
377 payload.Package = packagePayload.Package;
378 payload.Container = packagePayload.Container;
379 payload.ContentFile = true;
380 payload.EnableSignatureValidation = packagePayload.EnableSignatureValidation;
381 payload.Packaging = packagePayload.Packaging;
382 payload.ParentPackagePayload = packagePayload.Id;
383 }
384 }
385 }
386 }
387 }
388
389 private long ImportExternalFileAsPayloadsAndReturnInstallSize(Dtf.Database db, WixBundlePayloadRow packagePayload, bool longNamesInImage, bool compressed, ISet<string> payloadNames)
390 {
391 long size = 0;
392
393 if (db.Tables.Contains("Component") && db.Tables.Contains("Directory") && db.Tables.Contains("File"))
394 {
395 Hashtable directories = new Hashtable();
396
397 // Load up the directory hash table so we will be able to resolve source paths
398 // for files in the MSI database.
399 using (Dtf.View view = db.OpenView("SELECT `Directory`, `Directory_Parent`, `DefaultDir` FROM `Directory`"))
400 {
401 view.Execute();
402 while (true)
403 {
404 using (Dtf.Record record = view.Fetch())
405 {
406 if (null == record)
407 {
408 break;
409 }
410
411 string sourceName = Common.GetName(record.GetString(3), true, longNamesInImage);
412 directories.Add(record.GetString(1), new ResolvedDirectory(record.GetString(2), sourceName));
413 }
414 }
415 }
416
417 // Resolve the source paths to external files and add each file size to the total
418 // install size of the package.
419 using (Dtf.View view = db.OpenView("SELECT `Directory_`, `File`, `FileName`, `File`.`Attributes`, `FileSize` FROM `Component`, `File` WHERE `Component`.`Component`=`File`.`Component_`"))
420 {
421 view.Execute();
422 while (true)
423 {
424 using (Dtf.Record record = view.Fetch())
425 {
426 if (null == record)
427 {
428 break;
429 }
430
431 // Skip adding the loose files as payloads if it was suppressed.
432 if (!this.Facade.MsiPackage.SuppressLooseFilePayloadGeneration)
433 {
434 // If the file is explicitly uncompressed or the MSI is uncompressed and the file is not
435 // explicitly marked compressed then this is an external file.
436 if (MsiInterop.MsidbFileAttributesNoncompressed == (record.GetInteger(4) & MsiInterop.MsidbFileAttributesNoncompressed) ||
437 (!compressed && 0 == (record.GetInteger(4) & MsiInterop.MsidbFileAttributesCompressed)))
438 {
439 string fileSourcePath = Binder.GetFileSourcePath(directories, record.GetString(1), record.GetString(3), compressed, longNamesInImage);
440 string name = Path.Combine(Path.GetDirectoryName(packagePayload.Name), fileSourcePath);
441
442 if (!payloadNames.Contains(name))
443 {
444 string generatedId = Common.GenerateIdentifier("f", packagePayload.Id, record.GetString(2));
445 string payloadSourceFile = this.ResolveRelatedFile(packagePayload.UnresolvedSourceFile, fileSourcePath, "File", this.Facade.Package.SourceLineNumbers, BindStage.Normal);
446
447 WixBundlePayloadRow payload = (WixBundlePayloadRow)this.PayloadTable.CreateRow(this.Facade.Package.SourceLineNumbers);
448 payload.Id = generatedId;
449 payload.Name = name;
450 payload.SourceFile = payloadSourceFile;
451 payload.Compressed = packagePayload.Compressed;
452 payload.UnresolvedSourceFile = name;
453 payload.Package = packagePayload.Package;
454 payload.Container = packagePayload.Container;
455 payload.ContentFile = true;
456 payload.EnableSignatureValidation = packagePayload.EnableSignatureValidation;
457 payload.Packaging = packagePayload.Packaging;
458 payload.ParentPackagePayload = packagePayload.Id;
459 }
460 }
461 }
462
463 size += record.GetInteger(5);
464 }
465 }
466 }
467 }
468
469 return size;
470 }
471
472 private void AddMsiProperty(string name, string value)
473 {
474 WixBundleMsiPropertyRow row = (WixBundleMsiPropertyRow)this.MsiPropertyTable.CreateRow(this.Facade.MsiPackage.SourceLineNumbers);
475 row.ChainPackageId = this.Facade.Package.WixChainItemId;
476 row.Name = name;
477 row.Value = value;
478 }
479
480 private void ImportDependencyProviders(Dtf.Database db)
481 {
482 if (db.Tables.Contains("WixDependencyProvider"))
483 {
484 string query = "SELECT `ProviderKey`, `Version`, `DisplayName`, `Attributes` FROM `WixDependencyProvider`";
485
486 using (Dtf.View view = db.OpenView(query))
487 {
488 view.Execute();
489 while (true)
490 {
491 using (Dtf.Record record = view.Fetch())
492 {
493 if (null == record)
494 {
495 break;
496 }
497
498 // Import the provider key and attributes.
499 string providerKey = record.GetString(1);
500 string version = record.GetString(2) ?? this.Facade.MsiPackage.ProductVersion;
501 string displayName = record.GetString(3) ?? this.Facade.Package.DisplayName;
502 int attributes = record.GetInteger(4);
503
504 ProvidesDependency dependency = new ProvidesDependency(providerKey, version, displayName, attributes);
505 dependency.Imported = true;
506
507 this.Facade.Provides.Add(dependency);
508 }
509 }
510 }
511 }
512 }
513
514 private string ResolveRelatedFile(string sourceFile, string relatedSource, string type, SourceLineNumber sourceLineNumbers, BindStage stage)
515 {
516 foreach (var extension in this.BackendExtensions)
517 {
518 var relatedFile = extension.ResolveRelatedFile(sourceFile, relatedSource, type, sourceLineNumbers, stage);
519
520 if (!String.IsNullOrEmpty(relatedFile))
521 {
522 return relatedFile;
523 }
524 }
525
526 return null;
527 }
528
529 /// <summary>
530 /// Queries a Windows Installer database for a Property value.
531 /// </summary>
532 /// <param name="db">Database to query.</param>
533 /// <param name="property">Property to examine.</param>
534 /// <returns>String value for result or null if query doesn't match a single result.</returns>
535 private static string GetProperty(Dtf.Database db, string property)
536 {
537 try
538 {
539 return db.ExecuteScalar(PropertyQuery(property)).ToString();
540 }
541 catch (Dtf.InstallerException)
542 {
543 }
544
545 return null;
546 }
547
548 /// <summary>
549 /// Queries a Windows Installer database to determine if one or more rows exist in the Property table.
550 /// </summary>
551 /// <param name="db">Database to query.</param>
552 /// <param name="property">Property to examine.</param>
553 /// <returns>True if query matches at least one result.</returns>
554 private static bool HasProperty(Dtf.Database db, string property)
555 {
556 try
557 {
558 return 0 < db.ExecuteQuery(PropertyQuery(property)).Count;
559 }
560 catch (Dtf.InstallerException)
561 {
562 }
563
564 return false;
565 }
566
567 private static string PropertyQuery(string property)
568 {
569 // quick sanity check that we'll be creating a valid query...
570 // TODO: Are there any other special characters we should be looking for?
571 Debug.Assert(!property.Contains("'"));
572
573 return String.Format(CultureInfo.InvariantCulture, ProcessMsiPackageCommand.PropertySqlFormat, property);
574 }
575 }
576}