aboutsummaryrefslogtreecommitdiff
path: root/src/dtf/WixToolset.Dtf.WindowsInstaller/InstallerUtils.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/dtf/WixToolset.Dtf.WindowsInstaller/InstallerUtils.cs')
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/InstallerUtils.cs472
1 files changed, 472 insertions, 0 deletions
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/InstallerUtils.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/InstallerUtils.cs
new file mode 100644
index 00000000..8d9cf0a1
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/InstallerUtils.cs
@@ -0,0 +1,472 @@
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.Dtf.WindowsInstaller
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using System.Resources;
9 using System.Reflection;
10 using System.Collections.Generic;
11 using System.Globalization;
12 using System.Runtime.InteropServices;
13 using System.Diagnostics.CodeAnalysis;
14
15 public static partial class Installer
16 {
17 /// <summary>
18 /// Gets the current version of the installer.
19 /// </summary>
20 public static Version Version
21 {
22 get
23 {
24 // TODO: Use the extended form of version info to get the 4th component of the verison.
25 uint[] dllVersionInfo = new uint[5];
26 dllVersionInfo[0] = 20;
27 int hr = NativeMethods.DllGetVersion(dllVersionInfo);
28 if (hr != 0)
29 {
30 Marshal.ThrowExceptionForHR(hr);
31 }
32
33 return new Version((int) dllVersionInfo[1], (int) dllVersionInfo[2], (int) dllVersionInfo[3]);
34 }
35 }
36
37 internal static ResourceManager ErrorResources
38 {
39 get
40 {
41 if (errorResources == null)
42 {
43 errorResources = new ResourceManager(typeof(Installer).Namespace + ".Errors", typeof(Installer).Assembly);
44 }
45 return errorResources;
46 }
47 }
48
49 /// <summary>
50 /// Gets a Windows Installer error message in the system default language.
51 /// </summary>
52 /// <param name="errorNumber">The error number.</param>
53 /// <returns>The message string, or null if the error message is not found.</returns>
54 /// <remarks><p>
55 /// The returned string may have tokens such as [2] and [3] that are meant to be substituted
56 /// with context-specific values.
57 /// </p><p>
58 /// Error numbers greater than 2000 refer to MSI "internal" errors, and are always
59 /// returned in English.
60 /// </p></remarks>
61 public static string GetErrorMessage(int errorNumber)
62 {
63 return Installer.GetErrorMessage(errorNumber, null);
64 }
65
66 /// <summary>
67 /// Gets a Windows Installer error message in a specified language.
68 /// </summary>
69 /// <param name="errorNumber">The error number.</param>
70 /// <param name="culture">The locale for the message.</param>
71 /// <returns>The message string, or null if the error message or locale is not found.</returns>
72 /// <remarks><p>
73 /// The returned string may have tokens such as [2] and [3] that are meant to be substituted
74 /// with context-specific values.
75 /// </p><p>
76 /// Error numbers greater than 2000 refer to MSI "internal" errors, and are always
77 /// returned in English.
78 /// </p></remarks>
79 [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
80 public static string GetErrorMessage(int errorNumber, CultureInfo culture)
81 {
82 if (culture == null)
83 {
84 culture = CultureInfo.CurrentCulture;
85 }
86
87 string msg = Installer.ErrorResources.GetString(
88 errorNumber.ToString(CultureInfo.InvariantCulture.NumberFormat),
89 culture);
90 if (msg == null)
91 {
92 string msiMsgModule = Path.Combine(
93 Environment.SystemDirectory, "msimsg.dll");
94 msg = Installer.GetMessageFromModule(
95 msiMsgModule, errorNumber, culture);
96 }
97 return msg;
98 }
99
100 private static string GetMessageFromModule(
101 string modulePath, int errorNumber, CultureInfo culture)
102 {
103 const uint LOAD_LIBRARY_AS_DATAFILE = 2;
104 const int RT_RCDATA = 10;
105
106 IntPtr msgModule = NativeMethods.LoadLibraryEx(
107 modulePath, IntPtr.Zero, LOAD_LIBRARY_AS_DATAFILE);
108 if (msgModule == IntPtr.Zero)
109 {
110 return null;
111 }
112
113 try
114 {
115 // On pre-Vista systems, the messages are stored as RCDATA resources.
116
117 int lcid = (culture == CultureInfo.InvariantCulture) ?
118 0 : culture.LCID;
119 IntPtr resourceInfo = NativeMethods.FindResourceEx(
120 msgModule,
121 new IntPtr(RT_RCDATA),
122 new IntPtr(errorNumber),
123 (ushort) lcid);
124 if (resourceInfo != IntPtr.Zero)
125 {
126 IntPtr resourceData = NativeMethods.LoadResource(
127 msgModule, resourceInfo);
128 IntPtr resourcePtr = NativeMethods.LockResource(resourceData);
129
130 if (lcid == 0)
131 {
132 string msg = Marshal.PtrToStringAnsi(resourcePtr);
133 return msg;
134 }
135 else
136 {
137 int len = 0;
138 while (Marshal.ReadByte(resourcePtr, len) != 0)
139 {
140 len++;
141 }
142 byte[] msgBytes = new byte[len + 1];
143 Marshal.Copy(resourcePtr, msgBytes, 0, msgBytes.Length);
144 Encoding encoding = Encoding.GetEncoding(
145 culture.TextInfo.ANSICodePage);
146 string msg = encoding.GetString(msgBytes);
147 return msg;
148 }
149 }
150 else
151 {
152 // On Vista (and above?), the messages are stored in the module message table.
153 // They're actually in MUI files, and the redirection happens automatically here.
154
155 const uint FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200;
156 const uint FORMAT_MESSAGE_FROM_HMODULE = 0x00000800;
157 const uint MESSAGE_OFFSET = 20000; // Not documented, but observed on Vista
158
159 StringBuilder buf = new StringBuilder(1024);
160 uint formatCount = NativeMethods.FormatMessage(
161 FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_IGNORE_INSERTS,
162 msgModule,
163 (uint) errorNumber + MESSAGE_OFFSET,
164 (ushort) lcid,
165 buf,
166 (uint) buf.Capacity,
167 IntPtr.Zero);
168
169 return formatCount != 0 ? buf.ToString().Trim() : null;
170 }
171 }
172 finally
173 {
174 NativeMethods.FreeLibrary(msgModule);
175 }
176 }
177
178 /// <summary>
179 /// Gets a formatted Windows Installer error message in the system default language.
180 /// </summary>
181 /// <param name="errorRecord">Error record containing the error number in the first field, and
182 /// error-specific parameters in the other fields.</param>
183 /// <returns>The message string, or null if the error message is not found.</returns>
184 /// <remarks><p>
185 /// Error numbers greater than 2000 refer to MSI "internal" errors, and are always
186 /// returned in English.
187 /// </p></remarks>
188 public static string GetErrorMessage(Record errorRecord) { return Installer.GetErrorMessage(errorRecord, null); }
189
190 /// <summary>
191 /// Gets a formatted Windows Installer error message in a specified language.
192 /// </summary>
193 /// <param name="errorRecord">Error record containing the error number in the first field, and
194 /// error-specific parameters in the other fields.</param>
195 /// <param name="culture">The locale for the message.</param>
196 /// <returns>The message string, or null if the error message or locale is not found.</returns>
197 /// <remarks><p>
198 /// Error numbers greater than 2000 refer to MSI "internal" errors, and are always
199 /// returned in English.
200 /// </p></remarks>
201 public static string GetErrorMessage(Record errorRecord, CultureInfo culture)
202 {
203 if (errorRecord == null)
204 {
205 throw new ArgumentNullException("errorRecord");
206 }
207 int errorNumber;
208 if (errorRecord.FieldCount < 1 || (errorNumber = (int) errorRecord.GetInteger(1)) == 0)
209 {
210 throw new ArgumentOutOfRangeException("errorRecord");
211 }
212
213 string msg = Installer.GetErrorMessage(errorNumber, culture);
214 if (msg != null)
215 {
216 errorRecord.FormatString = msg;
217 msg = errorRecord.ToString((IFormatProvider)null);
218 }
219 return msg;
220 }
221
222 /// <summary>
223 /// Gets the version string of the path specified using the format that the installer
224 /// expects to find it in in the database.
225 /// </summary>
226 /// <param name="path">Path to the file</param>
227 /// <returns>Version string in the "#.#.#.#" format, or an empty string if the file
228 /// does not contain version information</returns>
229 /// <exception cref="FileNotFoundException">the file does not exist or could not be read</exception>
230 /// <remarks><p>
231 /// Win32 MSI API:
232 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetfileversion.asp">MsiGetFileVersion</a>
233 /// </p></remarks>
234 public static string GetFileVersion(string path)
235 {
236 StringBuilder version = new StringBuilder(20);
237 uint verBufSize = 0, langBufSize = 0;
238 uint ret = NativeMethods.MsiGetFileVersion(path, version, ref verBufSize, null, ref langBufSize);
239 if (ret == (uint) NativeMethods.Error.MORE_DATA)
240 {
241 version.Capacity = (int) ++verBufSize;
242 ret = NativeMethods.MsiGetFileVersion(path, version, ref verBufSize, null, ref langBufSize);
243 }
244
245 if (ret != 0 && ret != (uint) NativeMethods.Error.FILE_INVALID)
246 {
247 if (ret == (uint) NativeMethods.Error.FILE_NOT_FOUND ||
248 ret == (uint) NativeMethods.Error.ACCESS_DENIED)
249 {
250 throw new FileNotFoundException(null, path);
251 }
252 else
253 {
254 throw InstallerException.ExceptionFromReturnCode(ret);
255 }
256 }
257 return version.ToString();
258 }
259
260 /// <summary>
261 /// Gets the language string of the path specified using the format that the installer
262 /// expects to find them in in the database.
263 /// </summary>
264 /// <param name="path">Path to the file</param>
265 /// <returns>Language string in the form of a decimal language ID, or an empty string if the file
266 /// does not contain a language ID</returns>
267 /// <exception cref="FileNotFoundException">the file does not exist or could not be read</exception>
268 /// <remarks><p>
269 /// Win32 MSI API:
270 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetfileversion.asp">MsiGetFileVersion</a>
271 /// </p></remarks>
272 public static string GetFileLanguage(string path)
273 {
274 StringBuilder language = new StringBuilder("", 10);
275 uint verBufSize = 0, langBufSize = 0;
276 uint ret = NativeMethods.MsiGetFileVersion(path, null, ref verBufSize, language, ref langBufSize);
277 if (ret == (uint) NativeMethods.Error.MORE_DATA)
278 {
279 language.Capacity = (int) ++langBufSize;
280 ret = NativeMethods.MsiGetFileVersion(path, null, ref verBufSize, language, ref langBufSize);
281 }
282
283 if (ret != 0 && ret != (uint) NativeMethods.Error.FILE_INVALID)
284 {
285 if (ret == (uint) NativeMethods.Error.FILE_NOT_FOUND ||
286 ret == (uint) NativeMethods.Error.ACCESS_DENIED)
287 {
288 throw new FileNotFoundException(null, path);
289 }
290 else
291 {
292 throw InstallerException.ExceptionFromReturnCode(ret);
293 }
294 }
295 return language.ToString();
296 }
297
298 /// <summary>
299 /// Gets a 128-bit hash of the specified file.
300 /// </summary>
301 /// <param name="path">Path to the file</param>
302 /// <param name="hash">Integer array of length 4 which receives the
303 /// four 32-bit parts of the hash value.</param>
304 /// <exception cref="FileNotFoundException">the file does not exist or
305 /// could not be read</exception>
306 /// <remarks><p>
307 /// Win32 MSI API:
308 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetfilehash.asp">MsiGetFileHash</a>
309 /// </p></remarks>
310 public static void GetFileHash(string path, int[] hash)
311 {
312 if (hash == null)
313 {
314 throw new ArgumentNullException("hash");
315 }
316
317 uint[] tempHash = new uint[5];
318 tempHash[0] = 20;
319 uint ret = NativeMethods.MsiGetFileHash(path, 0, tempHash);
320 if (ret != 0)
321 {
322 if (ret == (uint) NativeMethods.Error.FILE_NOT_FOUND ||
323 ret == (uint) NativeMethods.Error.ACCESS_DENIED)
324 {
325 throw new FileNotFoundException(null, path);
326 }
327 else
328 {
329 throw InstallerException.ExceptionFromReturnCode(ret);
330 }
331 }
332
333 for (int i = 0; i < 4; i++)
334 {
335 hash[i] = unchecked ((int) tempHash[i + 1]);
336 }
337 }
338
339 /// <summary>
340 /// Examines a shortcut and returns its product, feature name, and component if available.
341 /// </summary>
342 /// <param name="shortcut">Full path to a shortcut</param>
343 /// <returns>ShortcutTarget structure containing target product code, feature, and component code</returns>
344 /// <remarks><p>
345 /// Win32 MSI API:
346 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetshortcuttarget.asp">MsiGetShortcutTarget</a>
347 /// </p></remarks>
348 public static ShortcutTarget GetShortcutTarget(string shortcut)
349 {
350 StringBuilder productBuf = new StringBuilder(40);
351 StringBuilder featureBuf = new StringBuilder(40);
352 StringBuilder componentBuf = new StringBuilder(40);
353
354 uint ret = NativeMethods.MsiGetShortcutTarget(shortcut, productBuf, featureBuf, componentBuf);
355 if (ret != 0)
356 {
357 throw InstallerException.ExceptionFromReturnCode(ret);
358 }
359 return new ShortcutTarget(
360 productBuf.Length > 0 ? productBuf.ToString() : null,
361 featureBuf.Length > 0 ? featureBuf.ToString() : null,
362 componentBuf.Length > 0 ? componentBuf.ToString() : null);
363 }
364
365 /// <summary>
366 /// Verifies that the given file is an installation package.
367 /// </summary>
368 /// <param name="packagePath">Path to the package</param>
369 /// <returns>True if the file is an installation package; false otherwise.</returns>
370 /// <exception cref="FileNotFoundException">the specified package file does not exist</exception>
371 /// <exception cref="InstallerException">the package file could not be opened</exception>
372 /// <remarks><p>
373 /// Win32 MSI API:
374 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiverifypackage.asp">MsiVerifyPackage</a>
375 /// </p></remarks>
376 public static bool VerifyPackage(string packagePath)
377 {
378 if (String.IsNullOrEmpty(packagePath))
379 {
380 throw new ArgumentNullException("packagePath");
381 }
382
383 if (!File.Exists(packagePath))
384 {
385 throw new FileNotFoundException(null, packagePath);
386 }
387
388 uint ret = NativeMethods.MsiVerifyPackage(packagePath);
389 if (ret == (uint) NativeMethods.Error.INSTALL_PACKAGE_INVALID)
390 {
391 return false;
392 }
393 else if (ret != 0)
394 {
395 throw InstallerException.ExceptionFromReturnCode(ret);
396 }
397 return true;
398 }
399
400 /// <summary>
401 /// [MSI 4.0] Gets the list of files that can be updated by one or more patches.
402 /// </summary>
403 /// <param name="productCode">ProductCode (GUID) of the product which is
404 /// the target of the patches</param>
405 /// <param name="patches">list of file paths of one or more patches to be
406 /// analyzed</param>
407 /// <returns>List of absolute paths of files that can be updated when the
408 /// patches are applied on this system.</returns>
409 /// <remarks><p>
410 /// Win32 MSI API:
411 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetpatchfilelist.asp">MsiGetPatchFileList</a>
412 /// </p></remarks>
413 public static IList<string> GetPatchFileList(string productCode, IList<string> patches)
414 {
415 if (String.IsNullOrEmpty(productCode))
416 {
417 throw new ArgumentNullException("productCode");
418 }
419
420 if (patches == null || patches.Count == 0)
421 {
422 throw new ArgumentNullException("patches");
423 }
424
425 StringBuilder patchList = new StringBuilder();
426 foreach (string patch in patches)
427 {
428 if (patch != null)
429 {
430 if (patchList.Length != 0)
431 {
432 patchList.Append(';');
433 }
434
435 patchList.Append(patch);
436 }
437 }
438
439 if (patchList.Length == 0)
440 {
441 throw new ArgumentNullException("patches");
442 }
443
444 IntPtr phFileRecords;
445 uint cFiles;
446
447 uint ret = NativeMethods.MsiGetPatchFileList(
448 productCode,
449 patchList.ToString(),
450 out cFiles,
451 out phFileRecords);
452 if (ret != 0)
453 {
454 throw InstallerException.ExceptionFromReturnCode(ret);
455 }
456
457 List<string> files = new List<string>();
458
459 for (uint i = 0; i < cFiles; i++)
460 {
461 int hFileRec = Marshal.ReadInt32(phFileRecords, (int) i);
462
463 using (Record fileRec = new Record(hFileRec, true, null))
464 {
465 files.Add(fileRec.GetString(1));
466 }
467 }
468
469 return files;
470 }
471 }
472}