aboutsummaryrefslogtreecommitdiff
path: root/src/WixToolset.Core.WindowsInstaller/Bind/UpdateFileFacadesCommand.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/WixToolset.Core.WindowsInstaller/Bind/UpdateFileFacadesCommand.cs')
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/UpdateFileFacadesCommand.cs533
1 files changed, 533 insertions, 0 deletions
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/UpdateFileFacadesCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/UpdateFileFacadesCommand.cs
new file mode 100644
index 00000000..cd9444ee
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/UpdateFileFacadesCommand.cs
@@ -0,0 +1,533 @@
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.WindowsInstaller.Databases
4{
5 using System;
6 using System.Collections.Generic;
7 using System.Collections.Specialized;
8 using System.ComponentModel;
9 using System.Globalization;
10 using System.IO;
11 using System.Linq;
12 using System.Xml;
13 using System.Xml.XPath;
14 using WixToolset.Clr.Interop;
15 using WixToolset.Core.Bind;
16 using WixToolset.Data;
17 using WixToolset.Data.Rows;
18 using WixToolset.Msi;
19
20 /// <summary>
21 /// Update file information.
22 /// </summary>
23 internal class UpdateFileFacadesCommand
24 {
25 public IEnumerable<FileFacade> FileFacades { private get; set; }
26
27 public IEnumerable<FileFacade> UpdateFileFacades { private get; set; }
28
29 public string ModularizationGuid { private get; set; }
30
31 public Output Output { private get; set; }
32
33 public bool OverwriteHash { private get; set; }
34
35 public TableDefinitionCollection TableDefinitions { private get; set; }
36
37 public IDictionary<string, string> VariableCache { private get; set; }
38
39 public void Execute()
40 {
41 foreach (FileFacade file in this.UpdateFileFacades)
42 {
43 this.UpdateFileFacade(file);
44 }
45 }
46
47 private void UpdateFileFacade(FileFacade file)
48 {
49 FileInfo fileInfo = null;
50 try
51 {
52 fileInfo = new FileInfo(file.WixFile.Source);
53 }
54 catch (ArgumentException)
55 {
56 Messaging.Instance.OnMessage(WixDataErrors.InvalidFileName(file.File.SourceLineNumbers, file.WixFile.Source));
57 return;
58 }
59 catch (PathTooLongException)
60 {
61 Messaging.Instance.OnMessage(WixDataErrors.InvalidFileName(file.File.SourceLineNumbers, file.WixFile.Source));
62 return;
63 }
64 catch (NotSupportedException)
65 {
66 Messaging.Instance.OnMessage(WixDataErrors.InvalidFileName(file.File.SourceLineNumbers, file.WixFile.Source));
67 return;
68 }
69
70 if (!fileInfo.Exists)
71 {
72 Messaging.Instance.OnMessage(WixErrors.CannotFindFile(file.File.SourceLineNumbers, file.File.File, file.File.FileName, file.WixFile.Source));
73 return;
74 }
75
76 using (FileStream fileStream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.Read))
77 {
78 if (Int32.MaxValue < fileStream.Length)
79 {
80 throw new WixException(WixErrors.FileTooLarge(file.File.SourceLineNumbers, file.WixFile.Source));
81 }
82
83 file.File.FileSize = Convert.ToInt32(fileStream.Length, CultureInfo.InvariantCulture);
84 }
85
86 string version = null;
87 string language = null;
88 try
89 {
90 Installer.GetFileVersion(fileInfo.FullName, out version, out language);
91 }
92 catch (Win32Exception e)
93 {
94 if (0x2 == e.NativeErrorCode) // ERROR_FILE_NOT_FOUND
95 {
96 throw new WixException(WixErrors.FileNotFound(file.File.SourceLineNumbers, fileInfo.FullName));
97 }
98 else
99 {
100 throw new WixException(WixErrors.Win32Exception(e.NativeErrorCode, e.Message));
101 }
102 }
103
104 // If there is no version, it is assumed there is no language because it won't matter in the versioning of the install.
105 if (String.IsNullOrEmpty(version)) // unversioned files have their hashes added to the MsiFileHash table
106 {
107 if (!this.OverwriteHash)
108 {
109 // not overwriting hash, so don't do the rest of these options.
110 }
111 else if (null != file.File.Version)
112 {
113 // Search all of the file rows available to see if the specified version is actually a companion file. Yes, this looks
114 // very expensive and you're probably thinking it would be better to create an index of some sort to do an O(1) look up.
115 // That's a reasonable thought but companion file usage is usually pretty rare so we'd be doing something expensive (indexing
116 // all the file rows) for a relatively uncommon situation. Let's not do that.
117 //
118 // Also, if we do not find a matching file identifier then the user provided a default version and is providing a version
119 // for unversioned file. That's allowed but generally a dangerous thing to do so let's point that out to the user.
120 if (!this.FileFacades.Any(r => file.File.Version.Equals(r.File.File, StringComparison.Ordinal)))
121 {
122 Messaging.Instance.OnMessage(WixWarnings.DefaultVersionUsedForUnversionedFile(file.File.SourceLineNumbers, file.File.Version, file.File.File));
123 }
124 }
125 else
126 {
127 if (null != file.File.Language)
128 {
129 Messaging.Instance.OnMessage(WixWarnings.DefaultLanguageUsedForUnversionedFile(file.File.SourceLineNumbers, file.File.Language, file.File.File));
130 }
131
132 int[] hash;
133 try
134 {
135 Installer.GetFileHash(fileInfo.FullName, 0, out hash);
136 }
137 catch (Win32Exception e)
138 {
139 if (0x2 == e.NativeErrorCode) // ERROR_FILE_NOT_FOUND
140 {
141 throw new WixException(WixErrors.FileNotFound(file.File.SourceLineNumbers, fileInfo.FullName));
142 }
143 else
144 {
145 throw new WixException(WixErrors.Win32Exception(e.NativeErrorCode, fileInfo.FullName, e.Message));
146 }
147 }
148
149 if (null == file.Hash)
150 {
151 Table msiFileHashTable = this.Output.EnsureTable(this.TableDefinitions["MsiFileHash"]);
152 file.Hash = msiFileHashTable.CreateRow(file.File.SourceLineNumbers);
153 }
154
155 file.Hash[0] = file.File.File;
156 file.Hash[1] = 0;
157 file.Hash[2] = hash[0];
158 file.Hash[3] = hash[1];
159 file.Hash[4] = hash[2];
160 file.Hash[5] = hash[3];
161 }
162 }
163 else // update the file row with the version and language information.
164 {
165 // If no version was provided by the user, use the version from the file itself.
166 // This is the most common case.
167 if (String.IsNullOrEmpty(file.File.Version))
168 {
169 file.File.Version = version;
170 }
171 else if (!this.FileFacades.Any(r => file.File.Version.Equals(r.File.File, StringComparison.Ordinal))) // this looks expensive, but see explanation below.
172 {
173 // The user provided a default version for the file row so we looked for a companion file (a file row with Id matching
174 // the version value). We didn't find it so, we will override the default version they provided with the actual
175 // version from the file itself. Now, I know it looks expensive to search through all the file rows trying to match
176 // on the Id. However, the alternative is to build a big index of all file rows to do look ups. Since this case
177 // where the file version is already present is rare (companion files are pretty uncommon), we'll do the more
178 // CPU intensive search to save on the memory intensive index that wouldn't be used much.
179 //
180 // Also note this case can occur when the file is being updated using the WixBindUpdatedFiles extension mechanism.
181 // That's typically even more rare than companion files so again, no index, just search.
182 file.File.Version = version;
183 }
184
185 if (!String.IsNullOrEmpty(file.File.Language) && String.IsNullOrEmpty(language))
186 {
187 Messaging.Instance.OnMessage(WixWarnings.DefaultLanguageUsedForVersionedFile(file.File.SourceLineNumbers, file.File.Language, file.File.File));
188 }
189 else // override the default provided by the user (usually nothing) with the actual language from the file itself.
190 {
191 file.File.Language = language;
192 }
193
194 // Populate the binder variables for this file information if requested.
195 if (null != this.VariableCache)
196 {
197 if (!String.IsNullOrEmpty(file.File.Version))
198 {
199 string key = String.Format(CultureInfo.InvariantCulture, "fileversion.{0}", Common.Demodularize(this.Output.Type, this.ModularizationGuid, file.File.File));
200 this.VariableCache[key] = file.File.Version;
201 }
202
203 if (!String.IsNullOrEmpty(file.File.Language))
204 {
205 string key = String.Format(CultureInfo.InvariantCulture, "filelanguage.{0}", Common.Demodularize(this.Output.Type, ModularizationGuid, file.File.File));
206 this.VariableCache[key] = file.File.Language;
207 }
208 }
209 }
210
211 // If this is a CLR assembly, load the assembly and get the assembly name information
212 if (FileAssemblyType.DotNetAssembly == file.WixFile.AssemblyType)
213 {
214 bool targetNetfx1 = false;
215 StringDictionary assemblyNameValues = new StringDictionary();
216
217 ClrInterop.IReferenceIdentity referenceIdentity = null;
218 Guid referenceIdentityGuid = ClrInterop.ReferenceIdentityGuid;
219 uint result = ClrInterop.GetAssemblyIdentityFromFile(fileInfo.FullName, ref referenceIdentityGuid, out referenceIdentity);
220 if (0 == result && null != referenceIdentity)
221 {
222 string imageRuntimeVersion = referenceIdentity.GetAttribute(null, "ImageRuntimeVersion");
223 if (null != imageRuntimeVersion)
224 {
225 targetNetfx1 = imageRuntimeVersion.StartsWith("v1", StringComparison.OrdinalIgnoreCase);
226 }
227
228 string culture = referenceIdentity.GetAttribute(null, "Culture") ?? "neutral";
229 assemblyNameValues.Add("Culture", culture);
230
231 string name = referenceIdentity.GetAttribute(null, "Name");
232 if (null != name)
233 {
234 assemblyNameValues.Add("Name", name);
235 }
236
237 string processorArchitecture = referenceIdentity.GetAttribute(null, "ProcessorArchitecture");
238 if (null != processorArchitecture)
239 {
240 assemblyNameValues.Add("ProcessorArchitecture", processorArchitecture);
241 }
242
243 string publicKeyToken = referenceIdentity.GetAttribute(null, "PublicKeyToken");
244 if (null != publicKeyToken)
245 {
246 bool publicKeyIsNeutral = (String.Equals(publicKeyToken, "neutral", StringComparison.OrdinalIgnoreCase));
247
248 // Managed code expects "null" instead of "neutral", and
249 // this won't be installed to the GAC since it's not signed anyway.
250 assemblyNameValues.Add("publicKeyToken", publicKeyIsNeutral ? "null" : publicKeyToken.ToUpperInvariant());
251 assemblyNameValues.Add("publicKeyTokenPreservedCase", publicKeyIsNeutral ? "null" : publicKeyToken);
252 }
253 else if (file.WixFile.AssemblyApplication == null)
254 {
255 throw new WixException(WixErrors.GacAssemblyNoStrongName(file.File.SourceLineNumbers, fileInfo.FullName, file.File.Component));
256 }
257
258 string assemblyVersion = referenceIdentity.GetAttribute(null, "Version");
259 if (null != version)
260 {
261 assemblyNameValues.Add("Version", assemblyVersion);
262 }
263 }
264 else
265 {
266 Messaging.Instance.OnMessage(WixErrors.InvalidAssemblyFile(file.File.SourceLineNumbers, fileInfo.FullName, String.Format(CultureInfo.InvariantCulture, "HRESULT: 0x{0:x8}", result)));
267 return;
268 }
269
270 Table assemblyNameTable = this.Output.EnsureTable(this.TableDefinitions["MsiAssemblyName"]);
271 if (assemblyNameValues.ContainsKey("name"))
272 {
273 this.SetMsiAssemblyName(assemblyNameTable, file, "name", assemblyNameValues["name"]);
274 }
275
276 if (!String.IsNullOrEmpty(version))
277 {
278 this.SetMsiAssemblyName(assemblyNameTable, file, "fileVersion", version);
279 }
280
281 if (assemblyNameValues.ContainsKey("version"))
282 {
283 string assemblyVersion = assemblyNameValues["version"];
284
285 if (!targetNetfx1)
286 {
287 // There is a bug in v1 fusion that requires the assembly's "version" attribute
288 // to be equal to or longer than the "fileVersion" in length when its present;
289 // the workaround is to prepend zeroes to the last version number in the assembly
290 // version.
291 if (null != version && version.Length > assemblyVersion.Length)
292 {
293 string padding = new string('0', version.Length - assemblyVersion.Length);
294 string[] assemblyVersionNumbers = assemblyVersion.Split('.');
295
296 if (assemblyVersionNumbers.Length > 0)
297 {
298 assemblyVersionNumbers[assemblyVersionNumbers.Length - 1] = String.Concat(padding, assemblyVersionNumbers[assemblyVersionNumbers.Length - 1]);
299 assemblyVersion = String.Join(".", assemblyVersionNumbers);
300 }
301 }
302 }
303
304 this.SetMsiAssemblyName(assemblyNameTable, file, "version", assemblyVersion);
305 }
306
307 if (assemblyNameValues.ContainsKey("culture"))
308 {
309 this.SetMsiAssemblyName(assemblyNameTable, file, "culture", assemblyNameValues["culture"]);
310 }
311
312 if (assemblyNameValues.ContainsKey("publicKeyToken"))
313 {
314 this.SetMsiAssemblyName(assemblyNameTable, file, "publicKeyToken", assemblyNameValues["publicKeyToken"]);
315 }
316
317 if (!String.IsNullOrEmpty(file.WixFile.ProcessorArchitecture))
318 {
319 this.SetMsiAssemblyName(assemblyNameTable, file, "processorArchitecture", file.WixFile.ProcessorArchitecture);
320 }
321
322 if (assemblyNameValues.ContainsKey("processorArchitecture"))
323 {
324 this.SetMsiAssemblyName(assemblyNameTable, file, "processorArchitecture", assemblyNameValues["processorArchitecture"]);
325 }
326
327 // add the assembly name to the information cache
328 if (null != this.VariableCache)
329 {
330 string fileId = Common.Demodularize(this.Output.Type, this.ModularizationGuid, file.File.File);
331 string key = String.Concat("assemblyfullname.", fileId);
332 string assemblyName = String.Concat(assemblyNameValues["name"], ", version=", assemblyNameValues["version"], ", culture=", assemblyNameValues["culture"], ", publicKeyToken=", String.IsNullOrEmpty(assemblyNameValues["publicKeyToken"]) ? "null" : assemblyNameValues["publicKeyToken"]);
333 if (assemblyNameValues.ContainsKey("processorArchitecture"))
334 {
335 assemblyName = String.Concat(assemblyName, ", processorArchitecture=", assemblyNameValues["processorArchitecture"]);
336 }
337
338 this.VariableCache[key] = assemblyName;
339
340 // Add entries with the preserved case publicKeyToken
341 string pcAssemblyNameKey = String.Concat("assemblyfullnamepreservedcase.", fileId);
342 this.VariableCache[pcAssemblyNameKey] = (assemblyNameValues["publicKeyToken"] == assemblyNameValues["publicKeyTokenPreservedCase"]) ? assemblyName : assemblyName.Replace(assemblyNameValues["publicKeyToken"], assemblyNameValues["publicKeyTokenPreservedCase"]);
343
344 string pcPublicKeyTokenKey = String.Concat("assemblypublickeytokenpreservedcase.", fileId);
345 this.VariableCache[pcPublicKeyTokenKey] = assemblyNameValues["publicKeyTokenPreservedCase"];
346 }
347 }
348 else if (FileAssemblyType.Win32Assembly == file.WixFile.AssemblyType)
349 {
350 // TODO: Consider passing in the this.FileFacades as an indexed collection instead of searching through
351 // all files like this. Even though this is a rare case it looks like we might be able to index the
352 // file earlier.
353 FileFacade fileManifest = this.FileFacades.SingleOrDefault(r => r.File.File.Equals(file.WixFile.AssemblyManifest, StringComparison.Ordinal));
354 if (null == fileManifest)
355 {
356 Messaging.Instance.OnMessage(WixErrors.MissingManifestForWin32Assembly(file.File.SourceLineNumbers, file.File.File, file.WixFile.AssemblyManifest));
357 }
358
359 string win32Type = null;
360 string win32Name = null;
361 string win32Version = null;
362 string win32ProcessorArchitecture = null;
363 string win32PublicKeyToken = null;
364
365 // loading the dom is expensive we want more performant APIs than the DOM
366 // Navigator is cheaper than dom. Perhaps there is a cheaper API still.
367 try
368 {
369 XPathDocument doc = new XPathDocument(fileManifest.WixFile.Source);
370 XPathNavigator nav = doc.CreateNavigator();
371 nav.MoveToRoot();
372
373 // this assumes a particular schema for a win32 manifest and does not
374 // provide error checking if the file does not conform to schema.
375 // The fallback case here is that nothing is added to the MsiAssemblyName
376 // table for an out of tolerance Win32 manifest. Perhaps warnings needed.
377 if (nav.MoveToFirstChild())
378 {
379 while (nav.NodeType != XPathNodeType.Element || nav.Name != "assembly")
380 {
381 nav.MoveToNext();
382 }
383
384 if (nav.MoveToFirstChild())
385 {
386 bool hasNextSibling = true;
387 while (nav.NodeType != XPathNodeType.Element || nav.Name != "assemblyIdentity" && hasNextSibling)
388 {
389 hasNextSibling = nav.MoveToNext();
390 }
391 if (!hasNextSibling)
392 {
393 Messaging.Instance.OnMessage(WixErrors.InvalidManifestContent(file.File.SourceLineNumbers, fileManifest.WixFile.Source));
394 return;
395 }
396
397 if (nav.MoveToAttribute("type", String.Empty))
398 {
399 win32Type = nav.Value;
400 nav.MoveToParent();
401 }
402
403 if (nav.MoveToAttribute("name", String.Empty))
404 {
405 win32Name = nav.Value;
406 nav.MoveToParent();
407 }
408
409 if (nav.MoveToAttribute("version", String.Empty))
410 {
411 win32Version = nav.Value;
412 nav.MoveToParent();
413 }
414
415 if (nav.MoveToAttribute("processorArchitecture", String.Empty))
416 {
417 win32ProcessorArchitecture = nav.Value;
418 nav.MoveToParent();
419 }
420
421 if (nav.MoveToAttribute("publicKeyToken", String.Empty))
422 {
423 win32PublicKeyToken = nav.Value;
424 nav.MoveToParent();
425 }
426 }
427 }
428 }
429 catch (FileNotFoundException fe)
430 {
431 Messaging.Instance.OnMessage(WixErrors.FileNotFound(new SourceLineNumber(fileManifest.WixFile.Source), fe.FileName, "AssemblyManifest"));
432 }
433 catch (XmlException xe)
434 {
435 Messaging.Instance.OnMessage(WixErrors.InvalidXml(new SourceLineNumber(fileManifest.WixFile.Source), "manifest", xe.Message));
436 }
437
438 Table assemblyNameTable = this.Output.EnsureTable(this.TableDefinitions["MsiAssemblyName"]);
439 if (!String.IsNullOrEmpty(win32Name))
440 {
441 this.SetMsiAssemblyName(assemblyNameTable, file, "name", win32Name);
442 }
443
444 if (!String.IsNullOrEmpty(win32Version))
445 {
446 this.SetMsiAssemblyName(assemblyNameTable, file, "version", win32Version);
447 }
448
449 if (!String.IsNullOrEmpty(win32Type))
450 {
451 this.SetMsiAssemblyName(assemblyNameTable, file, "type", win32Type);
452 }
453
454 if (!String.IsNullOrEmpty(win32ProcessorArchitecture))
455 {
456 this.SetMsiAssemblyName(assemblyNameTable, file, "processorArchitecture", win32ProcessorArchitecture);
457 }
458
459 if (!String.IsNullOrEmpty(win32PublicKeyToken))
460 {
461 this.SetMsiAssemblyName(assemblyNameTable, file, "publicKeyToken", win32PublicKeyToken);
462 }
463 }
464 }
465
466 /// <summary>
467 /// Set an MsiAssemblyName row. If it was directly authored, override the value, otherwise
468 /// create a new row.
469 /// </summary>
470 /// <param name="assemblyNameTable">MsiAssemblyName table.</param>
471 /// <param name="file">FileFacade containing the assembly read for the MsiAssemblyName row.</param>
472 /// <param name="name">MsiAssemblyName name.</param>
473 /// <param name="value">MsiAssemblyName value.</param>
474 private void SetMsiAssemblyName(Table assemblyNameTable, FileFacade file, string name, string value)
475 {
476 // check for null value (this can occur when grabbing the file version from an assembly without one)
477 if (String.IsNullOrEmpty(value))
478 {
479 Messaging.Instance.OnMessage(WixWarnings.NullMsiAssemblyNameValue(file.File.SourceLineNumbers, file.File.Component, name));
480 }
481 else
482 {
483 Row assemblyNameRow = null;
484
485 // override directly authored value
486 foreach (Row row in assemblyNameTable.Rows)
487 {
488 if ((string)row[0] == file.File.Component && (string)row[1] == name)
489 {
490 assemblyNameRow = row;
491 break;
492 }
493 }
494
495 // if the assembly will be GAC'd and the name in the file table doesn't match the name in the MsiAssemblyName table, error because the install will fail.
496 if ("name" == name && FileAssemblyType.DotNetAssembly == file.WixFile.AssemblyType &&
497 String.IsNullOrEmpty(file.WixFile.AssemblyApplication) &&
498 !String.Equals(Path.GetFileNameWithoutExtension(file.File.LongFileName), value, StringComparison.OrdinalIgnoreCase))
499 {
500 Messaging.Instance.OnMessage(WixErrors.GACAssemblyIdentityWarning(file.File.SourceLineNumbers, Path.GetFileNameWithoutExtension(file.File.LongFileName), value));
501 }
502
503 if (null == assemblyNameRow)
504 {
505 assemblyNameRow = assemblyNameTable.CreateRow(file.File.SourceLineNumbers);
506 assemblyNameRow[0] = file.File.Component;
507 assemblyNameRow[1] = name;
508 assemblyNameRow[2] = value;
509
510 // put the MsiAssemblyName row in the same section as the related File row
511 assemblyNameRow.SectionId = file.File.SectionId;
512
513 if (null == file.AssemblyNames)
514 {
515 file.AssemblyNames = new List<Row>();
516 }
517
518 file.AssemblyNames.Add(assemblyNameRow);
519 }
520 else
521 {
522 assemblyNameRow[2] = value;
523 }
524
525 if (this.VariableCache != null)
526 {
527 string key = String.Format(CultureInfo.InvariantCulture, "assembly{0}.{1}", name, Common.Demodularize(this.Output.Type, this.ModularizationGuid, file.File.File)).ToLowerInvariant();
528 this.VariableCache[key] = (string)assemblyNameRow[2];
529 }
530 }
531 }
532 }
533}