aboutsummaryrefslogtreecommitdiff
path: root/src/WixToolset.Core.WindowsInstaller
diff options
context:
space:
mode:
Diffstat (limited to 'src/WixToolset.Core.WindowsInstaller')
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/AssignMediaCommand.cs314
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/BindDatabaseCommand.cs1282
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/BindSummaryInfoCommand.cs135
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/BindTransformCommand.cs470
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/CabinetBuilder.cs177
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/CabinetResolver.cs122
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/CabinetWorkItem.cs79
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/ConfigurationCallback.cs91
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/CopyTransformDataCommand.cs606
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/CreateCabinetsCommand.cs499
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/CreateDeltaPatchesCommand.cs87
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/CreateSpecialPropertiesCommand.cs68
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/ExtractMergeModuleFilesCommand.cs226
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/GenerateDatabaseCommand.cs332
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/GetFileFacadesCommand.cs149
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/MergeModulesCommand.cs351
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/ProcessUncompressedFilesCommand.cs118
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/UpdateControlTextCommand.cs80
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/UpdateFileFacadesCommand.cs533
-rw-r--r--src/WixToolset.Core.WindowsInstaller/CLR/Interop/CLRInterop.cs147
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Differ.cs611
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Inscribe/InscribeMsiPackageCommand.cs282
-rw-r--r--src/WixToolset.Core.WindowsInstaller/MergeMod/NativeMethods.cs508
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Msi/Database.cs303
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Msi/Installer.cs363
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Msi/Interop/MsiInterop.cs697
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Msi/MsiException.cs78
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Msi/MsiHandle.cs116
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Msi/Record.cs182
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Msi/Session.cs45
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Msi/SummaryInformation.cs245
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Msi/View.cs189
-rw-r--r--src/WixToolset.Core.WindowsInstaller/MsiBackend.cs36
-rw-r--r--src/WixToolset.Core.WindowsInstaller/MsmBackend.cs34
-rw-r--r--src/WixToolset.Core.WindowsInstaller/MspBackend.cs111
-rw-r--r--src/WixToolset.Core.WindowsInstaller/MstBackend.cs37
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Ole32/Storage.cs437
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Patch.cs1245
-rw-r--r--src/WixToolset.Core.WindowsInstaller/PatchAPI/PatchInterop.cs989
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Unbind/ExtractCabinetsCommand.cs146
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Unbind/UnbindDatabaseCommand.cs791
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Unbind/UnbindMsiOrMsmCommand.cs53
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Unbind/UnbindTranformCommand.cs317
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Validator.cs385
-rw-r--r--src/WixToolset.Core.WindowsInstaller/ValidatorExtension.cs299
-rw-r--r--src/WixToolset.Core.WindowsInstaller/WindowsInstallerBackendFactory.cs50
-rw-r--r--src/WixToolset.Core.WindowsInstaller/WixToolset.Core.WindowsInstaller.csproj36
47 files changed, 14451 insertions, 0 deletions
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/AssignMediaCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/AssignMediaCommand.cs
new file mode 100644
index 00000000..23c481b7
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/AssignMediaCommand.cs
@@ -0,0 +1,314 @@
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.Globalization;
8 using WixToolset.Core.Bind;
9 using WixToolset.Data;
10 using WixToolset.Data.Rows;
11
12 /// <summary>
13 /// AssignMediaCommand assigns files to cabs based on Media or MediaTemplate rows.
14 /// </summary>
15 public class AssignMediaCommand
16 {
17 public AssignMediaCommand()
18 {
19 this.CabinetNameTemplate = "Cab{0}.cab";
20 }
21
22 public Output Output { private get; set; }
23
24 public bool FilesCompressed { private get; set; }
25
26 public string CabinetNameTemplate { private get; set; }
27
28 public IEnumerable<FileFacade> FileFacades { private get; set; }
29
30 public TableDefinitionCollection TableDefinitions { private get; set; }
31
32 /// <summary>
33 /// Gets cabinets with their file rows.
34 /// </summary>
35 public Dictionary<MediaRow, IEnumerable<FileFacade>> FileFacadesByCabinetMedia { get; private set; }
36
37 /// <summary>
38 /// Get media rows.
39 /// </summary>
40 public RowDictionary<MediaRow> MediaRows { get; private set; }
41
42 /// <summary>
43 /// Get uncompressed file rows. This will contain file rows of File elements that are marked with compression=no.
44 /// This contains all the files when Package element is marked with compression=no
45 /// </summary>
46 public IEnumerable<FileFacade> UncompressedFileFacades { get; private set; }
47
48 public void Execute()
49 {
50 Dictionary<MediaRow, List<FileFacade>> filesByCabinetMedia = new Dictionary<MediaRow, List<FileFacade>>();
51
52 RowDictionary<MediaRow> mediaRows = new RowDictionary<MediaRow>();
53
54 List<FileFacade> uncompressedFiles = new List<FileFacade>();
55
56 MediaRow mergeModuleMediaRow = null;
57 Table mediaTable = this.Output.Tables["Media"];
58 Table mediaTemplateTable = this.Output.Tables["WixMediaTemplate"];
59
60 // If both tables are authored, it is an error.
61 if ((mediaTemplateTable != null && mediaTemplateTable.Rows.Count > 0) && (mediaTable != null && mediaTable.Rows.Count > 1))
62 {
63 throw new WixException(WixErrors.MediaTableCollision(null));
64 }
65
66 // When building merge module, all the files go to "#MergeModule.CABinet".
67 if (OutputType.Module == this.Output.Type)
68 {
69 Table mergeModuleMediaTable = new Table(null, this.TableDefinitions["Media"]);
70 mergeModuleMediaRow = (MediaRow)mergeModuleMediaTable.CreateRow(null);
71 mergeModuleMediaRow.Cabinet = "#MergeModule.CABinet";
72
73 filesByCabinetMedia.Add(mergeModuleMediaRow, new List<FileFacade>());
74 }
75
76 if (OutputType.Module == this.Output.Type || null == mediaTemplateTable)
77 {
78 this.ManuallyAssignFiles(mediaTable, mergeModuleMediaRow, this.FileFacades, filesByCabinetMedia, mediaRows, uncompressedFiles);
79 }
80 else
81 {
82 this.AutoAssignFiles(mediaTable, this.FileFacades, filesByCabinetMedia, mediaRows, uncompressedFiles);
83 }
84
85 this.FileFacadesByCabinetMedia = new Dictionary<MediaRow, IEnumerable<FileFacade>>();
86
87 foreach (var mediaRowWithFiles in filesByCabinetMedia)
88 {
89 this.FileFacadesByCabinetMedia.Add(mediaRowWithFiles.Key, mediaRowWithFiles.Value);
90 }
91
92 this.MediaRows = mediaRows;
93
94 this.UncompressedFileFacades = uncompressedFiles;
95 }
96
97 /// <summary>
98 /// Assign files to cabinets based on MediaTemplate authoring.
99 /// </summary>
100 /// <param name="fileFacades">FileRowCollection</param>
101 private void AutoAssignFiles(Table mediaTable, IEnumerable<FileFacade> fileFacades, Dictionary<MediaRow, List<FileFacade>> filesByCabinetMedia, RowDictionary<MediaRow> mediaRows, List<FileFacade> uncompressedFiles)
102 {
103 const int MaxCabIndex = 999;
104
105 ulong currentPreCabSize = 0;
106 ulong maxPreCabSizeInBytes;
107 int maxPreCabSizeInMB = 0;
108 int currentCabIndex = 0;
109
110 MediaRow currentMediaRow = null;
111
112 Table mediaTemplateTable = this.Output.Tables["WixMediaTemplate"];
113
114 // Auto assign files to cabinets based on maximum uncompressed media size
115 mediaTable.Rows.Clear();
116 WixMediaTemplateRow mediaTemplateRow = (WixMediaTemplateRow)mediaTemplateTable.Rows[0];
117
118 if (!String.IsNullOrEmpty(mediaTemplateRow.CabinetTemplate))
119 {
120 this.CabinetNameTemplate = mediaTemplateRow.CabinetTemplate;
121 }
122
123 string mumsString = Environment.GetEnvironmentVariable("WIX_MUMS");
124
125 try
126 {
127 // Override authored mums value if environment variable is authored.
128 if (!String.IsNullOrEmpty(mumsString))
129 {
130 maxPreCabSizeInMB = Int32.Parse(mumsString);
131 }
132 else
133 {
134 maxPreCabSizeInMB = mediaTemplateRow.MaximumUncompressedMediaSize;
135 }
136
137 maxPreCabSizeInBytes = (ulong)maxPreCabSizeInMB * 1024 * 1024;
138 }
139 catch (FormatException)
140 {
141 throw new WixException(WixErrors.IllegalEnvironmentVariable("WIX_MUMS", mumsString));
142 }
143 catch (OverflowException)
144 {
145 throw new WixException(WixErrors.MaximumUncompressedMediaSizeTooLarge(null, maxPreCabSizeInMB));
146 }
147
148 foreach (FileFacade facade in this.FileFacades)
149 {
150 // When building a product, if the current file is not to be compressed or if
151 // the package set not to be compressed, don't cab it.
152 if (OutputType.Product == this.Output.Type &&
153 (YesNoType.No == facade.File.Compressed ||
154 (YesNoType.NotSet == facade.File.Compressed && !this.FilesCompressed)))
155 {
156 uncompressedFiles.Add(facade);
157 continue;
158 }
159
160 if (currentCabIndex == MaxCabIndex)
161 {
162 // Associate current file with last cab (irrespective of the size) and cab index is not incremented anymore.
163 List<FileFacade> cabinetFiles = filesByCabinetMedia[currentMediaRow];
164 facade.WixFile.DiskId = currentCabIndex;
165 cabinetFiles.Add(facade);
166 continue;
167 }
168
169 // Update current cab size.
170 currentPreCabSize += (ulong)facade.File.FileSize;
171
172 if (currentPreCabSize > maxPreCabSizeInBytes)
173 {
174 // Overflow due to current file
175 currentMediaRow = this.AddMediaRow(mediaTemplateRow, mediaTable, ++currentCabIndex);
176 mediaRows.Add(currentMediaRow);
177 filesByCabinetMedia.Add(currentMediaRow, new List<FileFacade>());
178
179 List<FileFacade> cabinetFileRows = filesByCabinetMedia[currentMediaRow];
180 facade.WixFile.DiskId = currentCabIndex;
181 cabinetFileRows.Add(facade);
182 // Now files larger than MaxUncompressedMediaSize will be the only file in its cabinet so as to respect MaxUncompressedMediaSize
183 currentPreCabSize = (ulong)facade.File.FileSize;
184 }
185 else
186 {
187 // File fits in the current cab.
188 if (currentMediaRow == null)
189 {
190 // Create new cab and MediaRow
191 currentMediaRow = this.AddMediaRow(mediaTemplateRow, mediaTable, ++currentCabIndex);
192 mediaRows.Add(currentMediaRow);
193 filesByCabinetMedia.Add(currentMediaRow, new List<FileFacade>());
194 }
195
196 // Associate current file with current cab.
197 List<FileFacade> cabinetFiles = filesByCabinetMedia[currentMediaRow];
198 facade.WixFile.DiskId = currentCabIndex;
199 cabinetFiles.Add(facade);
200 }
201 }
202
203 // If there are uncompressed files and no MediaRow, create a default one.
204 if (uncompressedFiles.Count > 0 && mediaTable.Rows.Count == 0)
205 {
206 MediaRow defaultMediaRow = (MediaRow)mediaTable.CreateRow(null);
207 defaultMediaRow.DiskId = 1;
208 mediaRows.Add(defaultMediaRow);
209 }
210 }
211
212 /// <summary>
213 /// Assign files to cabinets based on Media authoring.
214 /// </summary>
215 /// <param name="mediaTable"></param>
216 /// <param name="mergeModuleMediaRow"></param>
217 /// <param name="fileFacades"></param>
218 private void ManuallyAssignFiles(Table mediaTable, MediaRow mergeModuleMediaRow, IEnumerable<FileFacade> fileFacades, Dictionary<MediaRow, List<FileFacade>> filesByCabinetMedia, RowDictionary<MediaRow> mediaRows, List<FileFacade> uncompressedFiles)
219 {
220 if (OutputType.Module != this.Output.Type)
221 {
222 if (null != mediaTable)
223 {
224 Dictionary<string, MediaRow> cabinetMediaRows = new Dictionary<string, MediaRow>(StringComparer.InvariantCultureIgnoreCase);
225 foreach (MediaRow mediaRow in mediaTable.Rows)
226 {
227 // If the Media row has a cabinet, make sure it is unique across all Media rows.
228 if (!String.IsNullOrEmpty(mediaRow.Cabinet))
229 {
230 MediaRow existingRow;
231 if (cabinetMediaRows.TryGetValue(mediaRow.Cabinet, out existingRow))
232 {
233 Messaging.Instance.OnMessage(WixErrors.DuplicateCabinetName(mediaRow.SourceLineNumbers, mediaRow.Cabinet));
234 Messaging.Instance.OnMessage(WixErrors.DuplicateCabinetName2(existingRow.SourceLineNumbers, existingRow.Cabinet));
235 }
236 else
237 {
238 cabinetMediaRows.Add(mediaRow.Cabinet, mediaRow);
239 }
240 }
241
242 mediaRows.Add(mediaRow);
243 }
244 }
245
246 foreach (MediaRow mediaRow in mediaRows.Values)
247 {
248 if (null != mediaRow.Cabinet)
249 {
250 filesByCabinetMedia.Add(mediaRow, new List<FileFacade>());
251 }
252 }
253 }
254
255 foreach (FileFacade facade in fileFacades)
256 {
257 if (OutputType.Module == this.Output.Type)
258 {
259 filesByCabinetMedia[mergeModuleMediaRow].Add(facade);
260 }
261 else
262 {
263 MediaRow mediaRow;
264 if (!mediaRows.TryGetValue(facade.WixFile.DiskId.ToString(CultureInfo.InvariantCulture), out mediaRow))
265 {
266 Messaging.Instance.OnMessage(WixErrors.MissingMedia(facade.File.SourceLineNumbers, facade.WixFile.DiskId));
267 continue;
268 }
269
270 // When building a product, if the current file is not to be compressed or if
271 // the package set not to be compressed, don't cab it.
272 if (OutputType.Product == this.Output.Type &&
273 (YesNoType.No == facade.File.Compressed ||
274 (YesNoType.NotSet == facade.File.Compressed && !this.FilesCompressed)))
275 {
276 uncompressedFiles.Add(facade);
277 }
278 else // file is marked compressed.
279 {
280 List<FileFacade> cabinetFiles;
281 if (filesByCabinetMedia.TryGetValue(mediaRow, out cabinetFiles))
282 {
283 cabinetFiles.Add(facade);
284 }
285 else
286 {
287 Messaging.Instance.OnMessage(WixErrors.ExpectedMediaCabinet(facade.File.SourceLineNumbers, facade.File.File, facade.WixFile.DiskId));
288 }
289 }
290 }
291 }
292 }
293
294 /// <summary>
295 /// Adds a row to the media table with cab name template filled in.
296 /// </summary>
297 /// <param name="mediaTable"></param>
298 /// <param name="cabIndex"></param>
299 /// <returns></returns>
300 private MediaRow AddMediaRow(WixMediaTemplateRow mediaTemplateRow, Table mediaTable, int cabIndex)
301 {
302 MediaRow currentMediaRow = (MediaRow)mediaTable.CreateRow(mediaTemplateRow.SourceLineNumbers);
303 currentMediaRow.DiskId = cabIndex;
304 currentMediaRow.Cabinet = String.Format(CultureInfo.InvariantCulture, this.CabinetNameTemplate, cabIndex);
305
306 Table wixMediaTable = this.Output.EnsureTable(this.TableDefinitions["WixMedia"]);
307 WixMediaRow row = (WixMediaRow)wixMediaTable.CreateRow(mediaTemplateRow.SourceLineNumbers);
308 row.DiskId = cabIndex;
309 row.CompressionLevel = mediaTemplateRow.CompressionLevel;
310
311 return currentMediaRow;
312 }
313 }
314}
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/BindDatabaseCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/BindDatabaseCommand.cs
new file mode 100644
index 00000000..2e2c5417
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/BindDatabaseCommand.cs
@@ -0,0 +1,1282 @@
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.Bind
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.Bind;
13 using WixToolset.Core.Bind;
14 using WixToolset.Core.WindowsInstaller.Databases;
15 using WixToolset.Data;
16 using WixToolset.Data.Bind;
17 using WixToolset.Data.Rows;
18 using WixToolset.Extensibility;
19 using WixToolset.Msi;
20
21 /// <summary>
22 /// Binds a databse.
23 /// </summary>
24 internal class BindDatabaseCommand
25 {
26 // As outlined in RFC 4122, this is our namespace for generating name-based (version 3) UUIDs.
27 private static readonly Guid WixComponentGuidNamespace = new Guid("{3064E5C6-FB63-4FE9-AC49-E446A792EFA5}");
28
29 public BindDatabaseCommand(IBindContext context, Validator validator)
30 {
31 this.TableDefinitions = WindowsInstallerStandard.GetTableDefinitions();
32
33 this.BindPaths = context.BindPaths;
34 this.CabbingThreadCount = context.CabbingThreadCount;
35 this.CabCachePath = context.CabCachePath;
36 this.Codepage = context.Codepage;
37 this.DefaultCompressionLevel = context.DefaultCompressionLevel;
38 this.DelayedFields = context.DelayedFields;
39 this.ExpectedEmbeddedFiles = context.ExpectedEmbeddedFiles;
40 this.Extensions = context.Extensions;
41 this.Output = context.IntermediateRepresentation;
42 this.OutputPath = context.OutputPath;
43 this.PdbFile = context.OutputPdbPath;
44 this.IntermediateFolder = context.IntermediateFolder;
45 this.Validator = validator;
46 this.WixVariableResolver = context.WixVariableResolver;
47
48 this.BackendExtensions = context.ExtensionManager.Create<IWindowsInstallerBackendExtension>();
49 }
50
51 private IEnumerable<BindPath> BindPaths { get; }
52
53 private int Codepage { get; }
54
55 private int CabbingThreadCount { get; }
56
57 private string CabCachePath { get; }
58
59 private CompressionLevel DefaultCompressionLevel { get; }
60
61 public IEnumerable<IDelayedField> DelayedFields { get; }
62
63 public IEnumerable<IExpectedExtractFile> ExpectedEmbeddedFiles { get; }
64
65 public bool DeltaBinaryPatch { get; set; }
66
67 private IEnumerable<IWindowsInstallerBackendExtension> BackendExtensions { get; }
68
69 private IEnumerable<IBinderExtension> Extensions { get; }
70
71 private IEnumerable<InspectorExtension> InspectorExtensions { get; }
72
73 private string PdbFile { get; }
74
75 private Output Output { get; }
76
77 private string OutputPath { get; }
78
79 private bool SuppressAddingValidationRows { get; }
80
81 private bool SuppressLayout { get; }
82
83 private TableDefinitionCollection TableDefinitions { get; }
84
85 private string IntermediateFolder { get; }
86
87 private Validator Validator { get; }
88
89 private IBindVariableResolver WixVariableResolver { get; }
90
91 public IEnumerable<FileTransfer> FileTransfers { get; private set; }
92
93 public IEnumerable<string> ContentFilePaths { get; private set; }
94
95 public void Execute()
96 {
97 List<FileTransfer> fileTransfers = new List<FileTransfer>();
98
99 HashSet<string> suppressedTableNames = new HashSet<string>();
100
101 // If there are any fields to resolve later, create the cache to populate during bind.
102 IDictionary<string, string> variableCache = null;
103 if (this.DelayedFields.Any())
104 {
105 variableCache = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
106 }
107
108 this.LocalizeUI(this.Output.Tables);
109
110 // Process the summary information table before the other tables.
111 bool compressed;
112 bool longNames;
113 int installerVersion;
114 string modularizationGuid;
115 {
116 BindSummaryInfoCommand command = new BindSummaryInfoCommand();
117 command.Output = this.Output;
118 command.Execute();
119
120 compressed = command.Compressed;
121 longNames = command.LongNames;
122 installerVersion = command.InstallerVersion;
123 modularizationGuid = command.ModularizationGuid;
124 }
125
126 // Stop processing if an error previously occurred.
127 if (Messaging.Instance.EncounteredError)
128 {
129 return;
130 }
131
132 // Modularize identifiers and add tables with real streams to the import tables.
133 if (OutputType.Module == this.Output.Type)
134 {
135 // Gather all the suppress modularization identifiers
136 HashSet<string> suppressModularizationIdentifiers = null;
137 Table wixSuppressModularizationTable = this.Output.Tables["WixSuppressModularization"];
138 if (null != wixSuppressModularizationTable)
139 {
140 suppressModularizationIdentifiers = new HashSet<string>(wixSuppressModularizationTable.Rows.Select(row => (string)row[0]));
141 }
142
143 foreach (Table table in this.Output.Tables)
144 {
145 table.Modularize(modularizationGuid, suppressModularizationIdentifiers);
146 }
147 }
148
149 // This must occur after all variables and source paths have been resolved and after modularization.
150 List<FileFacade> fileFacades;
151 {
152 GetFileFacadesCommand command = new GetFileFacadesCommand();
153 command.FileTable = this.Output.Tables["File"];
154 command.WixFileTable = this.Output.Tables["WixFile"];
155 command.WixDeltaPatchFileTable = this.Output.Tables["WixDeltaPatchFile"];
156 command.WixDeltaPatchSymbolPathsTable = this.Output.Tables["WixDeltaPatchSymbolPaths"];
157 command.Execute();
158
159 fileFacades = command.FileFacades;
160 }
161
162 ////if (OutputType.Patch == this.Output.Type)
163 ////{
164 //// foreach (SubStorage substorage in this.Output.SubStorages)
165 //// {
166 //// Output transform = substorage.Data;
167
168 //// ResolveFieldsCommand command = new ResolveFieldsCommand();
169 //// command.Tables = transform.Tables;
170 //// command.FilesWithEmbeddedFiles = filesWithEmbeddedFiles;
171 //// command.FileManagerCore = this.FileManagerCore;
172 //// command.FileManagers = this.FileManagers;
173 //// command.SupportDelayedResolution = false;
174 //// command.TempFilesLocation = this.TempFilesLocation;
175 //// command.WixVariableResolver = this.WixVariableResolver;
176 //// command.Execute();
177
178 //// this.MergeUnrealTables(transform.Tables);
179 //// }
180 ////}
181
182 {
183 CreateSpecialPropertiesCommand command = new CreateSpecialPropertiesCommand();
184 command.PropertyTable = this.Output.Tables["Property"];
185 command.WixPropertyTable = this.Output.Tables["WixProperty"];
186 command.Execute();
187 }
188
189 if (Messaging.Instance.EncounteredError)
190 {
191 return;
192 }
193
194 // Add binder variables for all properties.
195 Table propertyTable = this.Output.Tables["Property"];
196 if (null != propertyTable)
197 {
198 foreach (PropertyRow propertyRow in propertyTable.Rows)
199 {
200 // Set the ProductCode if it is to be generated.
201 if (OutputType.Product == this.Output.Type && "ProductCode".Equals(propertyRow.Property, StringComparison.Ordinal) && "*".Equals(propertyRow.Value, StringComparison.Ordinal))
202 {
203 propertyRow.Value = Common.GenerateGuid();
204
205 // Update the target ProductCode in any instance transforms.
206 foreach (SubStorage subStorage in this.Output.SubStorages)
207 {
208 Output subStorageOutput = subStorage.Data;
209 if (OutputType.Transform != subStorageOutput.Type)
210 {
211 continue;
212 }
213
214 Table instanceSummaryInformationTable = subStorageOutput.Tables["_SummaryInformation"];
215 foreach (Row row in instanceSummaryInformationTable.Rows)
216 {
217 if ((int)SummaryInformation.Transform.ProductCodes == row.FieldAsInteger(0))
218 {
219 row[1] = row.FieldAsString(1).Replace("*", propertyRow.Value);
220 break;
221 }
222 }
223 }
224 }
225
226 // Add the property name and value to the variableCache.
227 if (null != variableCache)
228 {
229 string key = String.Concat("property.", Common.Demodularize(this.Output.Type, modularizationGuid, propertyRow.Property));
230 variableCache[key] = propertyRow.Value;
231 }
232 }
233 }
234
235 // Extract files that come from cabinet files (this does not extract files from merge modules).
236 {
237 ExtractEmbeddedFilesCommand command = new ExtractEmbeddedFilesCommand();
238 command.FilesWithEmbeddedFiles = this.ExpectedEmbeddedFiles;
239 command.Execute();
240 }
241
242 if (OutputType.Product == this.Output.Type)
243 {
244 // Retrieve files and their information from merge modules.
245 Table wixMergeTable = this.Output.Tables["WixMerge"];
246
247 if (null != wixMergeTable)
248 {
249 ExtractMergeModuleFilesCommand command = new ExtractMergeModuleFilesCommand();
250 command.FileFacades = fileFacades;
251 command.FileTable = this.Output.Tables["File"];
252 command.WixFileTable = this.Output.Tables["WixFile"];
253 command.WixMergeTable = wixMergeTable;
254 command.OutputInstallerVersion = installerVersion;
255 command.SuppressLayout = this.SuppressLayout;
256 command.TempFilesLocation = this.IntermediateFolder;
257 command.Execute();
258
259 fileFacades.AddRange(command.MergeModulesFileFacades);
260 }
261 }
262 else if (OutputType.Patch == this.Output.Type)
263 {
264 // Merge transform data into the output object.
265 IEnumerable<FileFacade> filesFromTransform = this.CopyFromTransformData(this.Output);
266
267 fileFacades.AddRange(filesFromTransform);
268 }
269
270 // stop processing if an error previously occurred
271 if (Messaging.Instance.EncounteredError)
272 {
273 return;
274 }
275
276 Messaging.Instance.OnMessage(WixVerboses.UpdatingFileInformation());
277
278 // Gather information about files that did not come from merge modules (i.e. rows with a reference to the File table).
279 {
280 UpdateFileFacadesCommand command = new UpdateFileFacadesCommand();
281 command.FileFacades = fileFacades;
282 command.UpdateFileFacades = fileFacades.Where(f => !f.FromModule);
283 command.ModularizationGuid = modularizationGuid;
284 command.Output = this.Output;
285 command.OverwriteHash = true;
286 command.TableDefinitions = this.TableDefinitions;
287 command.VariableCache = variableCache;
288 command.Execute();
289 }
290
291 // Set generated component guids.
292 this.SetComponentGuids(this.Output);
293
294 // With the Component Guids set now we can create instance transforms.
295 this.CreateInstanceTransforms(this.Output);
296
297 this.ValidateComponentGuids(this.Output);
298
299 this.UpdateControlText(this.Output);
300
301 if (this.DelayedFields.Any())
302 {
303 ResolveDelayedFieldsCommand command = new ResolveDelayedFieldsCommand();
304 command.OutputType = this.Output.Type;
305 command.DelayedFields = this.DelayedFields;
306 command.ModularizationGuid = null;
307 command.VariableCache = variableCache;
308 command.Execute();
309 }
310
311 // Assign files to media.
312 RowDictionary<MediaRow> assignedMediaRows;
313 Dictionary<MediaRow, IEnumerable<FileFacade>> filesByCabinetMedia;
314 IEnumerable<FileFacade> uncompressedFiles;
315 {
316 AssignMediaCommand command = new AssignMediaCommand();
317 command.FilesCompressed = compressed;
318 command.FileFacades = fileFacades;
319 command.Output = this.Output;
320 command.TableDefinitions = this.TableDefinitions;
321 command.Execute();
322
323 assignedMediaRows = command.MediaRows;
324 filesByCabinetMedia = command.FileFacadesByCabinetMedia;
325 uncompressedFiles = command.UncompressedFileFacades;
326 }
327
328 // Update file sequence.
329 this.UpdateMediaSequences(this.Output.Type, fileFacades, assignedMediaRows);
330
331 // stop processing if an error previously occurred
332 if (Messaging.Instance.EncounteredError)
333 {
334 return;
335 }
336
337 // Extended binder extensions can be called now that fields are resolved.
338 {
339 Table updatedFiles = this.Output.EnsureTable(this.TableDefinitions["WixBindUpdatedFiles"]);
340
341 foreach (IBinderExtension extension in this.Extensions)
342 {
343 extension.AfterResolvedFields(this.Output);
344 }
345
346 List<FileFacade> updatedFileFacades = new List<FileFacade>();
347
348 foreach (Row updatedFile in updatedFiles.Rows)
349 {
350 string updatedId = updatedFile.FieldAsString(0);
351
352 FileFacade updatedFacade = fileFacades.First(f => f.File.File.Equals(updatedId));
353
354 updatedFileFacades.Add(updatedFacade);
355 }
356
357 if (updatedFileFacades.Any())
358 {
359 UpdateFileFacadesCommand command = new UpdateFileFacadesCommand();
360 command.FileFacades = fileFacades;
361 command.UpdateFileFacades = updatedFileFacades;
362 command.ModularizationGuid = modularizationGuid;
363 command.Output = this.Output;
364 command.OverwriteHash = true;
365 command.TableDefinitions = this.TableDefinitions;
366 command.VariableCache = variableCache;
367 command.Execute();
368 }
369 }
370
371 // stop processing if an error previously occurred
372 if (Messaging.Instance.EncounteredError)
373 {
374 return;
375 }
376
377 Directory.CreateDirectory(this.IntermediateFolder);
378
379 if (OutputType.Patch == this.Output.Type && this.DeltaBinaryPatch)
380 {
381 CreateDeltaPatchesCommand command = new CreateDeltaPatchesCommand();
382 command.FileFacades = fileFacades;
383 command.WixPatchIdTable = this.Output.Tables["WixPatchId"];
384 command.TempFilesLocation = this.IntermediateFolder;
385 command.Execute();
386 }
387
388 // create cabinet files and process uncompressed files
389 string layoutDirectory = Path.GetDirectoryName(this.OutputPath);
390 if (!this.SuppressLayout || OutputType.Module == this.Output.Type)
391 {
392 Messaging.Instance.OnMessage(WixVerboses.CreatingCabinetFiles());
393
394 var command = new CreateCabinetsCommand();
395 command.CabbingThreadCount = this.CabbingThreadCount;
396 command.CabCachePath = this.CabCachePath;
397 command.DefaultCompressionLevel = this.DefaultCompressionLevel;
398 command.Output = this.Output;
399 command.BackendExtensions = this.BackendExtensions;
400 command.LayoutDirectory = layoutDirectory;
401 command.Compressed = compressed;
402 command.FileRowsByCabinet = filesByCabinetMedia;
403 command.ResolveMedia = this.ResolveMedia;
404 command.TableDefinitions = this.TableDefinitions;
405 command.TempFilesLocation = this.IntermediateFolder;
406 command.WixMediaTable = this.Output.Tables["WixMedia"];
407 command.Execute();
408
409 fileTransfers.AddRange(command.FileTransfers);
410 }
411
412 if (OutputType.Patch == this.Output.Type)
413 {
414 // copy output data back into the transforms
415 this.CopyToTransformData(this.Output);
416 }
417
418 // stop processing if an error previously occurred
419 if (Messaging.Instance.EncounteredError)
420 {
421 return;
422 }
423
424 // add back suppressed tables which must be present prior to merging in modules
425 if (OutputType.Product == this.Output.Type)
426 {
427 Table wixMergeTable = this.Output.Tables["WixMerge"];
428
429 if (null != wixMergeTable && 0 < wixMergeTable.Rows.Count)
430 {
431 foreach (SequenceTable sequence in Enum.GetValues(typeof(SequenceTable)))
432 {
433 string sequenceTableName = sequence.ToString();
434 Table sequenceTable = this.Output.Tables[sequenceTableName];
435
436 if (null == sequenceTable)
437 {
438 sequenceTable = this.Output.EnsureTable(this.TableDefinitions[sequenceTableName]);
439 }
440
441 if (0 == sequenceTable.Rows.Count)
442 {
443 suppressedTableNames.Add(sequenceTableName);
444 }
445 }
446 }
447 }
448
449 //foreach (BinderExtension extension in this.Extensions)
450 //{
451 // extension.PostBind(this.Context);
452 //}
453
454 // generate database file
455 Messaging.Instance.OnMessage(WixVerboses.GeneratingDatabase());
456 string tempDatabaseFile = Path.Combine(this.IntermediateFolder, Path.GetFileName(this.OutputPath));
457 this.GenerateDatabase(this.Output, tempDatabaseFile, false, false);
458
459 FileTransfer transfer;
460 if (FileTransfer.TryCreate(tempDatabaseFile, this.OutputPath, true, this.Output.Type.ToString(), null, out transfer)) // note where this database needs to move in the future
461 {
462 transfer.Built = true;
463 fileTransfers.Add(transfer);
464 }
465
466 // stop processing if an error previously occurred
467 if (Messaging.Instance.EncounteredError)
468 {
469 return;
470 }
471
472 // Output the output to a file
473 Pdb pdb = new Pdb();
474 pdb.Output = this.Output;
475 if (!String.IsNullOrEmpty(this.PdbFile))
476 {
477 pdb.Save(this.PdbFile);
478 }
479
480 // Merge modules.
481 if (OutputType.Product == this.Output.Type)
482 {
483 Messaging.Instance.OnMessage(WixVerboses.MergingModules());
484
485 MergeModulesCommand command = new MergeModulesCommand();
486 command.FileFacades = fileFacades;
487 command.Output = this.Output;
488 command.OutputPath = tempDatabaseFile;
489 command.SuppressedTableNames = suppressedTableNames;
490 command.Execute();
491
492 // stop processing if an error previously occurred
493 if (Messaging.Instance.EncounteredError)
494 {
495 return;
496 }
497 }
498
499 // inspect the MSI prior to running ICEs
500 //InspectorCore inspectorCore = new InspectorCore();
501 //foreach (InspectorExtension inspectorExtension in this.InspectorExtensions)
502 //{
503 // inspectorExtension.Core = inspectorCore;
504 // inspectorExtension.InspectDatabase(tempDatabaseFile, pdb);
505
506 // inspectorExtension.Core = null; // reset.
507 //}
508
509 if (Messaging.Instance.EncounteredError)
510 {
511 return;
512 }
513
514 // validate the output if there is an MSI validator
515 if (null != this.Validator)
516 {
517 Stopwatch stopwatch = Stopwatch.StartNew();
518
519 // set the output file for source line information
520 this.Validator.Output = this.Output;
521
522 Messaging.Instance.OnMessage(WixVerboses.ValidatingDatabase());
523
524 this.Validator.Validate(tempDatabaseFile);
525
526 stopwatch.Stop();
527 Messaging.Instance.OnMessage(WixVerboses.ValidatedDatabase(stopwatch.ElapsedMilliseconds));
528
529 // Stop processing if an error occurred.
530 if (Messaging.Instance.EncounteredError)
531 {
532 return;
533 }
534 }
535
536 // Process uncompressed files.
537 if (!Messaging.Instance.EncounteredError && !this.SuppressLayout && uncompressedFiles.Any())
538 {
539 var command = new ProcessUncompressedFilesCommand();
540 command.Compressed = compressed;
541 command.FileFacades = uncompressedFiles;
542 command.LayoutDirectory = layoutDirectory;
543 command.LongNamesInImage = longNames;
544 command.MediaRows = assignedMediaRows;
545 command.ResolveMedia = this.ResolveMedia;
546 command.DatabasePath = tempDatabaseFile;
547 command.WixMediaTable = this.Output.Tables["WixMedia"];
548 command.Execute();
549
550 fileTransfers.AddRange(command.FileTransfers);
551 }
552
553 this.FileTransfers = fileTransfers;
554 this.ContentFilePaths = fileFacades.Select(r => r.WixFile.Source).ToList();
555 }
556
557 /// <summary>
558 /// Localize dialogs and controls.
559 /// </summary>
560 /// <param name="tables">The tables to localize.</param>
561 private void LocalizeUI(TableIndexedCollection tables)
562 {
563 Table dialogTable = tables["Dialog"];
564 if (null != dialogTable)
565 {
566 foreach (Row row in dialogTable.Rows)
567 {
568 string dialog = (string)row[0];
569
570 if (this.WixVariableResolver.TryGetLocalizedControl(dialog, null, out LocalizedControl localizedControl))
571 {
572 if (CompilerConstants.IntegerNotSet != localizedControl.X)
573 {
574 row[1] = localizedControl.X;
575 }
576
577 if (CompilerConstants.IntegerNotSet != localizedControl.Y)
578 {
579 row[2] = localizedControl.Y;
580 }
581
582 if (CompilerConstants.IntegerNotSet != localizedControl.Width)
583 {
584 row[3] = localizedControl.Width;
585 }
586
587 if (CompilerConstants.IntegerNotSet != localizedControl.Height)
588 {
589 row[4] = localizedControl.Height;
590 }
591
592 row[5] = (int)row[5] | localizedControl.Attributes;
593
594 if (!String.IsNullOrEmpty(localizedControl.Text))
595 {
596 row[6] = localizedControl.Text;
597 }
598 }
599 }
600 }
601
602 Table controlTable = tables["Control"];
603 if (null != controlTable)
604 {
605 foreach (Row row in controlTable.Rows)
606 {
607 string dialog = (string)row[0];
608 string control = (string)row[1];
609
610 if (this.WixVariableResolver.TryGetLocalizedControl(dialog, control, out LocalizedControl localizedControl))
611 {
612 if (CompilerConstants.IntegerNotSet != localizedControl.X)
613 {
614 row[3] = localizedControl.X.ToString();
615 }
616
617 if (CompilerConstants.IntegerNotSet != localizedControl.Y)
618 {
619 row[4] = localizedControl.Y.ToString();
620 }
621
622 if (CompilerConstants.IntegerNotSet != localizedControl.Width)
623 {
624 row[5] = localizedControl.Width.ToString();
625 }
626
627 if (CompilerConstants.IntegerNotSet != localizedControl.Height)
628 {
629 row[6] = localizedControl.Height.ToString();
630 }
631
632 row[7] = (int)row[7] | localizedControl.Attributes;
633
634 if (!String.IsNullOrEmpty(localizedControl.Text))
635 {
636 row[9] = localizedControl.Text;
637 }
638 }
639 }
640 }
641 }
642
643 /// <summary>
644 /// Copy file data between transform substorages and the patch output object
645 /// </summary>
646 /// <param name="output">The output to bind.</param>
647 /// <param name="allFileRows">True if copying from transform to patch, false the other way.</param>
648 private IEnumerable<FileFacade> CopyFromTransformData(Output output)
649 {
650 var command = new CopyTransformDataCommand();
651 command.CopyOutFileRows = true;
652 command.Output = output;
653 command.TableDefinitions = this.TableDefinitions;
654 command.Execute();
655
656 return command.FileFacades;
657 }
658
659 /// <summary>
660 /// Copy file data between transform substorages and the patch output object
661 /// </summary>
662 /// <param name="output">The output to bind.</param>
663 /// <param name="allFileRows">True if copying from transform to patch, false the other way.</param>
664 private void CopyToTransformData(Output output)
665 {
666 var command = new CopyTransformDataCommand();
667 command.CopyOutFileRows = false;
668 command.Output = output;
669 command.TableDefinitions = this.TableDefinitions;
670 command.Execute();
671 }
672
673 private void UpdateMediaSequences(OutputType outputType, IEnumerable<FileFacade> fileFacades, RowDictionary<MediaRow> mediaRows)
674 {
675 // Calculate sequence numbers and media disk id layout for all file media information objects.
676 if (OutputType.Module == outputType)
677 {
678 int lastSequence = 0;
679 foreach (FileFacade facade in fileFacades) // TODO: Sort these rows directory path and component id and maybe file size or file extension and other creative ideas to get optimal install speed out of MSI.
680 {
681 facade.File.Sequence = ++lastSequence;
682 }
683 }
684 else
685 {
686 int lastSequence = 0;
687 MediaRow mediaRow = null;
688 Dictionary<int, List<FileFacade>> patchGroups = new Dictionary<int, List<FileFacade>>();
689
690 // sequence the non-patch-added files
691 foreach (FileFacade facade in fileFacades) // TODO: Sort these rows directory path and component id and maybe file size or file extension and other creative ideas to get optimal install speed out of MSI.
692 {
693 if (null == mediaRow)
694 {
695 mediaRow = mediaRows.Get(facade.WixFile.DiskId);
696 if (OutputType.Patch == outputType)
697 {
698 // patch Media cannot start at zero
699 lastSequence = mediaRow.LastSequence;
700 }
701 }
702 else if (mediaRow.DiskId != facade.WixFile.DiskId)
703 {
704 mediaRow.LastSequence = lastSequence;
705 mediaRow = mediaRows.Get(facade.WixFile.DiskId);
706 }
707
708 if (0 < facade.WixFile.PatchGroup)
709 {
710 List<FileFacade> patchGroup = patchGroups[facade.WixFile.PatchGroup];
711
712 if (null == patchGroup)
713 {
714 patchGroup = new List<FileFacade>();
715 patchGroups.Add(facade.WixFile.PatchGroup, patchGroup);
716 }
717
718 patchGroup.Add(facade);
719 }
720 else
721 {
722 facade.File.Sequence = ++lastSequence;
723 }
724 }
725
726 if (null != mediaRow)
727 {
728 mediaRow.LastSequence = lastSequence;
729 mediaRow = null;
730 }
731
732 // sequence the patch-added files
733 foreach (List<FileFacade> patchGroup in patchGroups.Values)
734 {
735 foreach (FileFacade facade in patchGroup)
736 {
737 if (null == mediaRow)
738 {
739 mediaRow = mediaRows.Get(facade.WixFile.DiskId);
740 }
741 else if (mediaRow.DiskId != facade.WixFile.DiskId)
742 {
743 mediaRow.LastSequence = lastSequence;
744 mediaRow = mediaRows.Get(facade.WixFile.DiskId);
745 }
746
747 facade.File.Sequence = ++lastSequence;
748 }
749 }
750
751 if (null != mediaRow)
752 {
753 mediaRow.LastSequence = lastSequence;
754 }
755 }
756 }
757
758 /// <summary>
759 /// Set the guids for components with generatable guids.
760 /// </summary>
761 /// <param name="output">Internal representation of the database to operate on.</param>
762 private void SetComponentGuids(Output output)
763 {
764 Table componentTable = output.Tables["Component"];
765 if (null != componentTable)
766 {
767 Hashtable registryKeyRows = null;
768 Hashtable directories = null;
769 Hashtable componentIdGenSeeds = null;
770 Dictionary<string, List<FileRow>> fileRows = null;
771
772 // find components with generatable guids
773 foreach (ComponentRow componentRow in componentTable.Rows)
774 {
775 // component guid will be generated
776 if ("*" == componentRow.Guid)
777 {
778 if (null == componentRow.KeyPath || componentRow.IsOdbcDataSourceKeyPath)
779 {
780 Messaging.Instance.OnMessage(WixErrors.IllegalComponentWithAutoGeneratedGuid(componentRow.SourceLineNumbers));
781 }
782 else if (componentRow.IsRegistryKeyPath)
783 {
784 if (null == registryKeyRows)
785 {
786 Table registryTable = output.Tables["Registry"];
787
788 registryKeyRows = new Hashtable(registryTable.Rows.Count);
789
790 foreach (Row registryRow in registryTable.Rows)
791 {
792 registryKeyRows.Add((string)registryRow[0], registryRow);
793 }
794 }
795
796 Row foundRow = registryKeyRows[componentRow.KeyPath] as Row;
797
798 string bitness = componentRow.Is64Bit ? "64" : String.Empty;
799 if (null != foundRow)
800 {
801 string regkey = String.Concat(bitness, foundRow[1], "\\", foundRow[2], "\\", foundRow[3]);
802 componentRow.Guid = Uuid.NewUuid(BindDatabaseCommand.WixComponentGuidNamespace, regkey.ToLowerInvariant()).ToString("B").ToUpperInvariant();
803 }
804 }
805 else // must be a File KeyPath
806 {
807 // if the directory table hasn't been loaded into an indexed hash
808 // of directory ids to target names do that now.
809 if (null == directories)
810 {
811 Table directoryTable = output.Tables["Directory"];
812
813 int numDirectoryTableRows = (null != directoryTable) ? directoryTable.Rows.Count : 0;
814
815 directories = new Hashtable(numDirectoryTableRows);
816
817 // get the target paths for all directories
818 if (null != directoryTable)
819 {
820 foreach (Row row in directoryTable.Rows)
821 {
822 // if the directory Id already exists, we will skip it here since
823 // checking for duplicate primary keys is done later when importing tables
824 // into database
825 if (directories.ContainsKey(row[0]))
826 {
827 continue;
828 }
829
830 string targetName = Common.GetName((string)row[2], false, true);
831 directories.Add(row[0], new ResolvedDirectory((string)row[1], targetName));
832 }
833 }
834 }
835
836 // if the component id generation seeds have not been indexed
837 // from the WixDirectory table do that now.
838 if (null == componentIdGenSeeds)
839 {
840 Table wixDirectoryTable = output.Tables["WixDirectory"];
841
842 int numWixDirectoryRows = (null != wixDirectoryTable) ? wixDirectoryTable.Rows.Count : 0;
843
844 componentIdGenSeeds = new Hashtable(numWixDirectoryRows);
845
846 // if there are any WixDirectory rows, build up the Component Guid
847 // generation seeds indexed by Directory/@Id.
848 if (null != wixDirectoryTable)
849 {
850 foreach (Row row in wixDirectoryTable.Rows)
851 {
852 componentIdGenSeeds.Add(row[0], (string)row[1]);
853 }
854 }
855 }
856
857 // if the file rows have not been indexed by File.Component yet
858 // then do that now
859 if (null == fileRows)
860 {
861 Table fileTable = output.Tables["File"];
862
863 int numFileRows = (null != fileTable) ? fileTable.Rows.Count : 0;
864
865 fileRows = new Dictionary<string, List<FileRow>>(numFileRows);
866
867 if (null != fileTable)
868 {
869 foreach (FileRow file in fileTable.Rows)
870 {
871 List<FileRow> files;
872 if (!fileRows.TryGetValue(file.Component, out files))
873 {
874 files = new List<FileRow>();
875 fileRows.Add(file.Component, files);
876 }
877
878 files.Add(file);
879 }
880 }
881 }
882
883 // validate component meets all the conditions to have a generated guid
884 List<FileRow> currentComponentFiles = fileRows[componentRow.Component];
885 int numFilesInComponent = currentComponentFiles.Count;
886 string path = null;
887
888 foreach (FileRow fileRow in currentComponentFiles)
889 {
890 if (fileRow.File == componentRow.KeyPath)
891 {
892 // calculate the key file's canonical target path
893 string directoryPath = Binder.GetDirectoryPath(directories, componentIdGenSeeds, componentRow.Directory, true);
894 string fileName = Common.GetName(fileRow.FileName, false, true).ToLower(CultureInfo.InvariantCulture);
895 path = Path.Combine(directoryPath, fileName);
896
897 // find paths that are not canonicalized
898 if (path.StartsWith(@"PersonalFolder\my pictures", StringComparison.Ordinal) ||
899 path.StartsWith(@"ProgramFilesFolder\common files", StringComparison.Ordinal) ||
900 path.StartsWith(@"ProgramMenuFolder\startup", StringComparison.Ordinal) ||
901 path.StartsWith("TARGETDIR", StringComparison.Ordinal) ||
902 path.StartsWith(@"StartMenuFolder\programs", StringComparison.Ordinal) ||
903 path.StartsWith(@"WindowsFolder\fonts", StringComparison.Ordinal))
904 {
905 Messaging.Instance.OnMessage(WixErrors.IllegalPathForGeneratedComponentGuid(componentRow.SourceLineNumbers, fileRow.Component, path));
906 }
907
908 // if component has more than one file, the key path must be versioned
909 if (1 < numFilesInComponent && String.IsNullOrEmpty(fileRow.Version))
910 {
911 Messaging.Instance.OnMessage(WixErrors.IllegalGeneratedGuidComponentUnversionedKeypath(componentRow.SourceLineNumbers));
912 }
913 }
914 else
915 {
916 // not a key path, so it must be an unversioned file if component has more than one file
917 if (1 < numFilesInComponent && !String.IsNullOrEmpty(fileRow.Version))
918 {
919 Messaging.Instance.OnMessage(WixErrors.IllegalGeneratedGuidComponentVersionedNonkeypath(componentRow.SourceLineNumbers));
920 }
921 }
922 }
923
924 // if the rules were followed, reward with a generated guid
925 if (!Messaging.Instance.EncounteredError)
926 {
927 componentRow.Guid = Uuid.NewUuid(BindDatabaseCommand.WixComponentGuidNamespace, path).ToString("B").ToUpperInvariant();
928 }
929 }
930 }
931 }
932 }
933 }
934
935 /// <summary>
936 /// Creates instance transform substorages in the output.
937 /// </summary>
938 /// <param name="output">Output containing instance transform definitions.</param>
939 private void CreateInstanceTransforms(Output output)
940 {
941 // Create and add substorages for instance transforms.
942 Table wixInstanceTransformsTable = output.Tables["WixInstanceTransforms"];
943 if (null != wixInstanceTransformsTable && 0 <= wixInstanceTransformsTable.Rows.Count)
944 {
945 string targetProductCode = null;
946 string targetUpgradeCode = null;
947 string targetProductVersion = null;
948
949 Table targetSummaryInformationTable = output.Tables["_SummaryInformation"];
950 Table targetPropertyTable = output.Tables["Property"];
951
952 // Get the data from target database
953 foreach (Row propertyRow in targetPropertyTable.Rows)
954 {
955 if ("ProductCode" == (string)propertyRow[0])
956 {
957 targetProductCode = (string)propertyRow[1];
958 }
959 else if ("ProductVersion" == (string)propertyRow[0])
960 {
961 targetProductVersion = (string)propertyRow[1];
962 }
963 else if ("UpgradeCode" == (string)propertyRow[0])
964 {
965 targetUpgradeCode = (string)propertyRow[1];
966 }
967 }
968
969 // Index the Instance Component Rows.
970 Dictionary<string, ComponentRow> instanceComponentGuids = new Dictionary<string, ComponentRow>();
971 Table targetInstanceComponentTable = output.Tables["WixInstanceComponent"];
972 if (null != targetInstanceComponentTable && 0 < targetInstanceComponentTable.Rows.Count)
973 {
974 foreach (Row row in targetInstanceComponentTable.Rows)
975 {
976 // Build up all the instances, we'll get the Components rows from the real Component table.
977 instanceComponentGuids.Add((string)row[0], null);
978 }
979
980 Table targetComponentTable = output.Tables["Component"];
981 foreach (ComponentRow componentRow in targetComponentTable.Rows)
982 {
983 string component = (string)componentRow[0];
984 if (instanceComponentGuids.ContainsKey(component))
985 {
986 instanceComponentGuids[component] = componentRow;
987 }
988 }
989 }
990
991 // Generate the instance transforms
992 foreach (Row instanceRow in wixInstanceTransformsTable.Rows)
993 {
994 string instanceId = (string)instanceRow[0];
995
996 Output instanceTransform = new Output(instanceRow.SourceLineNumbers);
997 instanceTransform.Type = OutputType.Transform;
998 instanceTransform.Codepage = output.Codepage;
999
1000 Table instanceSummaryInformationTable = instanceTransform.EnsureTable(this.TableDefinitions["_SummaryInformation"]);
1001 string targetPlatformAndLanguage = null;
1002
1003 foreach (Row summaryInformationRow in targetSummaryInformationTable.Rows)
1004 {
1005 if (7 == (int)summaryInformationRow[0]) // PID_TEMPLATE
1006 {
1007 targetPlatformAndLanguage = (string)summaryInformationRow[1];
1008 }
1009
1010 // Copy the row's data to the transform.
1011 Row copyOfSummaryRow = instanceSummaryInformationTable.CreateRow(null);
1012 copyOfSummaryRow[0] = summaryInformationRow[0];
1013 copyOfSummaryRow[1] = summaryInformationRow[1];
1014 }
1015
1016 // Modify the appropriate properties.
1017 Table propertyTable = instanceTransform.EnsureTable(this.TableDefinitions["Property"]);
1018
1019 // Change the ProductCode property
1020 string productCode = (string)instanceRow[2];
1021 if ("*" == productCode)
1022 {
1023 productCode = Common.GenerateGuid();
1024 }
1025
1026 Row productCodeRow = propertyTable.CreateRow(instanceRow.SourceLineNumbers);
1027 productCodeRow.Operation = RowOperation.Modify;
1028 productCodeRow.Fields[1].Modified = true;
1029 productCodeRow[0] = "ProductCode";
1030 productCodeRow[1] = productCode;
1031
1032 // Change the instance property
1033 Row instanceIdRow = propertyTable.CreateRow(instanceRow.SourceLineNumbers);
1034 instanceIdRow.Operation = RowOperation.Modify;
1035 instanceIdRow.Fields[1].Modified = true;
1036 instanceIdRow[0] = (string)instanceRow[1];
1037 instanceIdRow[1] = instanceId;
1038
1039 if (null != instanceRow[3])
1040 {
1041 // Change the ProductName property
1042 Row productNameRow = propertyTable.CreateRow(instanceRow.SourceLineNumbers);
1043 productNameRow.Operation = RowOperation.Modify;
1044 productNameRow.Fields[1].Modified = true;
1045 productNameRow[0] = "ProductName";
1046 productNameRow[1] = (string)instanceRow[3];
1047 }
1048
1049 if (null != instanceRow[4])
1050 {
1051 // Change the UpgradeCode property
1052 Row upgradeCodeRow = propertyTable.CreateRow(instanceRow.SourceLineNumbers);
1053 upgradeCodeRow.Operation = RowOperation.Modify;
1054 upgradeCodeRow.Fields[1].Modified = true;
1055 upgradeCodeRow[0] = "UpgradeCode";
1056 upgradeCodeRow[1] = instanceRow[4];
1057
1058 // Change the Upgrade table
1059 Table targetUpgradeTable = output.Tables["Upgrade"];
1060 if (null != targetUpgradeTable && 0 <= targetUpgradeTable.Rows.Count)
1061 {
1062 string upgradeId = (string)instanceRow[4];
1063 Table upgradeTable = instanceTransform.EnsureTable(this.TableDefinitions["Upgrade"]);
1064 foreach (Row row in targetUpgradeTable.Rows)
1065 {
1066 // In case they are upgrading other codes to this new product, leave the ones that don't match the
1067 // Product.UpgradeCode intact.
1068 if (targetUpgradeCode == (string)row[0])
1069 {
1070 Row upgradeRow = upgradeTable.CreateRow(null);
1071 upgradeRow.Operation = RowOperation.Add;
1072 upgradeRow.Fields[0].Modified = true;
1073 // I was hoping to be able to RowOperation.Modify, but that didn't appear to function.
1074 // upgradeRow.Fields[0].PreviousData = (string)row[0];
1075
1076 // Inserting a new Upgrade record with the updated UpgradeCode
1077 upgradeRow[0] = upgradeId;
1078 upgradeRow[1] = row[1];
1079 upgradeRow[2] = row[2];
1080 upgradeRow[3] = row[3];
1081 upgradeRow[4] = row[4];
1082 upgradeRow[5] = row[5];
1083 upgradeRow[6] = row[6];
1084
1085 // Delete the old row
1086 Row upgradeRemoveRow = upgradeTable.CreateRow(null);
1087 upgradeRemoveRow.Operation = RowOperation.Delete;
1088 upgradeRemoveRow[0] = row[0];
1089 upgradeRemoveRow[1] = row[1];
1090 upgradeRemoveRow[2] = row[2];
1091 upgradeRemoveRow[3] = row[3];
1092 upgradeRemoveRow[4] = row[4];
1093 upgradeRemoveRow[5] = row[5];
1094 upgradeRemoveRow[6] = row[6];
1095 }
1096 }
1097 }
1098 }
1099
1100 // If there are instance Components generate new GUIDs for them.
1101 if (0 < instanceComponentGuids.Count)
1102 {
1103 Table componentTable = instanceTransform.EnsureTable(this.TableDefinitions["Component"]);
1104 foreach (ComponentRow targetComponentRow in instanceComponentGuids.Values)
1105 {
1106 string guid = targetComponentRow.Guid;
1107 if (!String.IsNullOrEmpty(guid))
1108 {
1109 Row instanceComponentRow = componentTable.CreateRow(targetComponentRow.SourceLineNumbers);
1110 instanceComponentRow.Operation = RowOperation.Modify;
1111 instanceComponentRow.Fields[1].Modified = true;
1112 instanceComponentRow[0] = targetComponentRow[0];
1113 instanceComponentRow[1] = Uuid.NewUuid(BindDatabaseCommand.WixComponentGuidNamespace, String.Concat(guid, instanceId)).ToString("B").ToUpper(CultureInfo.InvariantCulture);
1114 instanceComponentRow[2] = targetComponentRow[2];
1115 instanceComponentRow[3] = targetComponentRow[3];
1116 instanceComponentRow[4] = targetComponentRow[4];
1117 instanceComponentRow[5] = targetComponentRow[5];
1118 }
1119 }
1120 }
1121
1122 // Update the summary information
1123 Hashtable summaryRows = new Hashtable(instanceSummaryInformationTable.Rows.Count);
1124 foreach (Row row in instanceSummaryInformationTable.Rows)
1125 {
1126 summaryRows[row[0]] = row;
1127
1128 if ((int)SummaryInformation.Transform.UpdatedPlatformAndLanguage == (int)row[0])
1129 {
1130 row[1] = targetPlatformAndLanguage;
1131 }
1132 else if ((int)SummaryInformation.Transform.ProductCodes == (int)row[0])
1133 {
1134 row[1] = String.Concat(targetProductCode, targetProductVersion, ';', productCode, targetProductVersion, ';', targetUpgradeCode);
1135 }
1136 else if ((int)SummaryInformation.Transform.ValidationFlags == (int)row[0])
1137 {
1138 row[1] = 0;
1139 }
1140 else if ((int)SummaryInformation.Transform.Security == (int)row[0])
1141 {
1142 row[1] = "4";
1143 }
1144 }
1145
1146 if (!summaryRows.Contains((int)SummaryInformation.Transform.UpdatedPlatformAndLanguage))
1147 {
1148 Row summaryRow = instanceSummaryInformationTable.CreateRow(null);
1149 summaryRow[0] = (int)SummaryInformation.Transform.UpdatedPlatformAndLanguage;
1150 summaryRow[1] = targetPlatformAndLanguage;
1151 }
1152 else if (!summaryRows.Contains((int)SummaryInformation.Transform.ValidationFlags))
1153 {
1154 Row summaryRow = instanceSummaryInformationTable.CreateRow(null);
1155 summaryRow[0] = (int)SummaryInformation.Transform.ValidationFlags;
1156 summaryRow[1] = "0";
1157 }
1158 else if (!summaryRows.Contains((int)SummaryInformation.Transform.Security))
1159 {
1160 Row summaryRow = instanceSummaryInformationTable.CreateRow(null);
1161 summaryRow[0] = (int)SummaryInformation.Transform.Security;
1162 summaryRow[1] = "4";
1163 }
1164
1165 output.SubStorages.Add(new SubStorage(instanceId, instanceTransform));
1166 }
1167 }
1168 }
1169
1170 /// <summary>
1171 /// Validate that there are no duplicate GUIDs in the output.
1172 /// </summary>
1173 /// <remarks>
1174 /// Duplicate GUIDs without conditions are an error condition; with conditions, it's a
1175 /// warning, as the conditions might be mutually exclusive.
1176 /// </remarks>
1177 private void ValidateComponentGuids(Output output)
1178 {
1179 Table componentTable = output.Tables["Component"];
1180 if (null != componentTable)
1181 {
1182 Dictionary<string, bool> componentGuidConditions = new Dictionary<string, bool>(componentTable.Rows.Count);
1183
1184 foreach (ComponentRow row in componentTable.Rows)
1185 {
1186 // we don't care about unmanaged components and if there's a * GUID remaining,
1187 // there's already an error that prevented it from being replaced with a real GUID.
1188 if (!String.IsNullOrEmpty(row.Guid) && "*" != row.Guid)
1189 {
1190 bool thisComponentHasCondition = !String.IsNullOrEmpty(row.Condition);
1191 bool allComponentsHaveConditions = thisComponentHasCondition;
1192
1193 if (componentGuidConditions.ContainsKey(row.Guid))
1194 {
1195 allComponentsHaveConditions = componentGuidConditions[row.Guid] && thisComponentHasCondition;
1196
1197 if (allComponentsHaveConditions)
1198 {
1199 Messaging.Instance.OnMessage(WixWarnings.DuplicateComponentGuidsMustHaveMutuallyExclusiveConditions(row.SourceLineNumbers, row.Component, row.Guid));
1200 }
1201 else
1202 {
1203 Messaging.Instance.OnMessage(WixErrors.DuplicateComponentGuids(row.SourceLineNumbers, row.Component, row.Guid));
1204 }
1205 }
1206
1207 componentGuidConditions[row.Guid] = allComponentsHaveConditions;
1208 }
1209 }
1210 }
1211 }
1212
1213 /// <summary>
1214 /// Update Control and BBControl text by reading from files when necessary.
1215 /// </summary>
1216 /// <param name="output">Internal representation of the msi database to operate upon.</param>
1217 private void UpdateControlText(Output output)
1218 {
1219 UpdateControlTextCommand command = new UpdateControlTextCommand();
1220 command.BBControlTable = output.Tables["BBControl"];
1221 command.WixBBControlTable = output.Tables["WixBBControl"];
1222 command.ControlTable = output.Tables["Control"];
1223 command.WixControlTable = output.Tables["WixControl"];
1224 command.Execute();
1225 }
1226
1227 private string ResolveMedia(MediaRow mediaRow, string mediaLayoutDirectory, string layoutDirectory)
1228 {
1229 string layout = null;
1230
1231 foreach (var extension in this.BackendExtensions)
1232 {
1233 layout = extension.ResolveMedia(mediaRow, mediaLayoutDirectory, layoutDirectory);
1234 if (!String.IsNullOrEmpty(layout))
1235 {
1236 break;
1237 }
1238 }
1239
1240 // If no binder file manager resolved the layout, do the default behavior.
1241 if (String.IsNullOrEmpty(layout))
1242 {
1243 if (String.IsNullOrEmpty(mediaLayoutDirectory))
1244 {
1245 layout = layoutDirectory;
1246 }
1247 else if (Path.IsPathRooted(mediaLayoutDirectory))
1248 {
1249 layout = mediaLayoutDirectory;
1250 }
1251 else
1252 {
1253 layout = Path.Combine(layoutDirectory, mediaLayoutDirectory);
1254 }
1255 }
1256
1257 return layout;
1258 }
1259
1260 /// <summary>
1261 /// Creates the MSI/MSM/PCP database.
1262 /// </summary>
1263 /// <param name="output">Output to create database for.</param>
1264 /// <param name="databaseFile">The database file to create.</param>
1265 /// <param name="keepAddedColumns">Whether to keep columns added in a transform.</param>
1266 /// <param name="useSubdirectory">Whether to use a subdirectory based on the <paramref name="databaseFile"/> file name for intermediate files.</param>
1267 private void GenerateDatabase(Output output, string databaseFile, bool keepAddedColumns, bool useSubdirectory)
1268 {
1269 var command = new GenerateDatabaseCommand();
1270 command.Extensions = this.Extensions;
1271 command.Output = output;
1272 command.OutputPath = databaseFile;
1273 command.KeepAddedColumns = keepAddedColumns;
1274 command.UseSubDirectory = useSubdirectory;
1275 command.SuppressAddingValidationRows = this.SuppressAddingValidationRows;
1276 command.TableDefinitions = this.TableDefinitions;
1277 command.TempFilesLocation = this.IntermediateFolder;
1278 command.Codepage = this.Codepage;
1279 command.Execute();
1280 }
1281 }
1282}
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/BindSummaryInfoCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/BindSummaryInfoCommand.cs
new file mode 100644
index 00000000..5471792d
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/BindSummaryInfoCommand.cs
@@ -0,0 +1,135 @@
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.Globalization;
7 using WixToolset.Data;
8
9 /// <summary>
10 /// Binds the summary information table of a database.
11 /// </summary>
12 internal class BindSummaryInfoCommand
13 {
14 /// <summary>
15 /// The output to bind.
16 /// </summary>
17 public Output Output { private get; set; }
18
19 /// <summary>
20 /// Returns a flag indicating if files are compressed by default.
21 /// </summary>
22 public bool Compressed { get; private set; }
23
24 /// <summary>
25 /// Returns a flag indicating if uncompressed files use long filenames.
26 /// </summary>
27 public bool LongNames { get; private set; }
28
29 public int InstallerVersion { get; private set; }
30
31 /// <summary>
32 /// Modularization guid, or null if the output is not a module.
33 /// </summary>
34 public string ModularizationGuid { get; private set; }
35
36 public void Execute()
37 {
38 this.Compressed = false;
39 this.LongNames = false;
40 this.InstallerVersion = 0;
41 this.ModularizationGuid = null;
42
43 Table summaryInformationTable = this.Output.Tables["_SummaryInformation"];
44
45 if (null != summaryInformationTable)
46 {
47 bool foundCreateDataTime = false;
48 bool foundLastSaveDataTime = false;
49 bool foundCreatingApplication = false;
50 string now = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss", CultureInfo.InvariantCulture);
51
52 foreach (Row summaryInformationRow in summaryInformationTable.Rows)
53 {
54 switch (summaryInformationRow.FieldAsInteger(0))
55 {
56 case 1: // PID_CODEPAGE
57 // make sure the code page is an int and not a web name or null
58 string codepage = summaryInformationRow.FieldAsString(1);
59
60 if (null == codepage)
61 {
62 codepage = "0";
63 }
64 else
65 {
66 summaryInformationRow[1] = Common.GetValidCodePage(codepage, false, false, summaryInformationRow.SourceLineNumbers).ToString(CultureInfo.InvariantCulture);
67 }
68 break;
69 case 9: // PID_REVNUMBER
70 string packageCode = (string)summaryInformationRow[1];
71
72 if (OutputType.Module == this.Output.Type)
73 {
74 this.ModularizationGuid = packageCode.Substring(1, 36).Replace('-', '_');
75 }
76 else if ("*" == packageCode)
77 {
78 // set the revision number (package/patch code) if it should be automatically generated
79 summaryInformationRow[1] = Common.GenerateGuid();
80 }
81 break;
82 case 12: // PID_CREATE_DTM
83 foundCreateDataTime = true;
84 break;
85 case 13: // PID_LASTSAVE_DTM
86 foundLastSaveDataTime = true;
87 break;
88 case 14:
89 this.InstallerVersion = summaryInformationRow.FieldAsInteger(1);
90 break;
91 case 15: // PID_WORDCOUNT
92 if (OutputType.Patch == this.Output.Type)
93 {
94 this.LongNames = true;
95 this.Compressed = true;
96 }
97 else
98 {
99 this.LongNames = (0 == (summaryInformationRow.FieldAsInteger(1) & 1));
100 this.Compressed = (2 == (summaryInformationRow.FieldAsInteger(1) & 2));
101 }
102 break;
103 case 18: // PID_APPNAME
104 foundCreatingApplication = true;
105 break;
106 }
107 }
108
109 // add a summary information row for the create time/date property if its not already set
110 if (!foundCreateDataTime)
111 {
112 Row createTimeDateRow = summaryInformationTable.CreateRow(null);
113 createTimeDateRow[0] = 12;
114 createTimeDateRow[1] = now;
115 }
116
117 // add a summary information row for the last save time/date property if its not already set
118 if (!foundLastSaveDataTime)
119 {
120 Row lastSaveTimeDateRow = summaryInformationTable.CreateRow(null);
121 lastSaveTimeDateRow[0] = 13;
122 lastSaveTimeDateRow[1] = now;
123 }
124
125 // add a summary information row for the creating application property if its not already set
126 if (!foundCreatingApplication)
127 {
128 Row creatingApplicationRow = summaryInformationTable.CreateRow(null);
129 creatingApplicationRow[0] = 18;
130 creatingApplicationRow[1] = String.Format(CultureInfo.InvariantCulture, AppCommon.GetCreatingApplicationString());
131 }
132 }
133 }
134 }
135}
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/BindTransformCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/BindTransformCommand.cs
new file mode 100644
index 00000000..425d1f9c
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/BindTransformCommand.cs
@@ -0,0 +1,470 @@
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.Globalization;
8 using System.IO;
9 using WixToolset.Data;
10 using WixToolset.Extensibility;
11 using WixToolset.Msi;
12 using WixToolset.Core.Native;
13
14 internal class BindTransformCommand
15 {
16 public IEnumerable<IBinderExtension> Extensions { private get; set; }
17
18 public TableDefinitionCollection TableDefinitions { private get; set; }
19
20 public string TempFilesLocation { private get; set; }
21
22 public Output Transform { private get; set; }
23
24 public string OutputPath { private get; set; }
25
26 public void Execute()
27 {
28 int transformFlags = 0;
29
30 Output targetOutput = new Output(null);
31 Output updatedOutput = new Output(null);
32
33 // TODO: handle added columns
34
35 // to generate a localized transform, both the target and updated
36 // databases need to have the same code page. the only reason to
37 // set different code pages is to support localized primary key
38 // columns, but that would only support deleting rows. if this
39 // becomes necessary, define a PreviousCodepage property on the
40 // Output class and persist this throughout transform generation.
41 targetOutput.Codepage = this.Transform.Codepage;
42 updatedOutput.Codepage = this.Transform.Codepage;
43
44 // remove certain Property rows which will be populated from summary information values
45 string targetUpgradeCode = null;
46 string updatedUpgradeCode = null;
47
48 Table propertyTable = this.Transform.Tables["Property"];
49 if (null != propertyTable)
50 {
51 for (int i = propertyTable.Rows.Count - 1; i >= 0; i--)
52 {
53 Row row = propertyTable.Rows[i];
54
55 if ("ProductCode" == (string)row[0] || "ProductLanguage" == (string)row[0] || "ProductVersion" == (string)row[0] || "UpgradeCode" == (string)row[0])
56 {
57 propertyTable.Rows.RemoveAt(i);
58
59 if ("UpgradeCode" == (string)row[0])
60 {
61 updatedUpgradeCode = (string)row[1];
62 }
63 }
64 }
65 }
66
67 Table targetSummaryInfo = targetOutput.EnsureTable(this.TableDefinitions["_SummaryInformation"]);
68 Table updatedSummaryInfo = updatedOutput.EnsureTable(this.TableDefinitions["_SummaryInformation"]);
69 Table targetPropertyTable = targetOutput.EnsureTable(this.TableDefinitions["Property"]);
70 Table updatedPropertyTable = updatedOutput.EnsureTable(this.TableDefinitions["Property"]);
71
72 // process special summary information values
73 foreach (Row row in this.Transform.Tables["_SummaryInformation"].Rows)
74 {
75 if ((int)SummaryInformation.Transform.CodePage == (int)row[0])
76 {
77 // convert from a web name if provided
78 string codePage = (string)row.Fields[1].Data;
79 if (null == codePage)
80 {
81 codePage = "0";
82 }
83 else
84 {
85 codePage = Common.GetValidCodePage(codePage).ToString(CultureInfo.InvariantCulture);
86 }
87
88 string previousCodePage = (string)row.Fields[1].PreviousData;
89 if (null == previousCodePage)
90 {
91 previousCodePage = "0";
92 }
93 else
94 {
95 previousCodePage = Common.GetValidCodePage(previousCodePage).ToString(CultureInfo.InvariantCulture);
96 }
97
98 Row targetCodePageRow = targetSummaryInfo.CreateRow(null);
99 targetCodePageRow[0] = 1; // PID_CODEPAGE
100 targetCodePageRow[1] = previousCodePage;
101
102 Row updatedCodePageRow = updatedSummaryInfo.CreateRow(null);
103 updatedCodePageRow[0] = 1; // PID_CODEPAGE
104 updatedCodePageRow[1] = codePage;
105 }
106 else if ((int)SummaryInformation.Transform.TargetPlatformAndLanguage == (int)row[0] ||
107 (int)SummaryInformation.Transform.UpdatedPlatformAndLanguage == (int)row[0])
108 {
109 // the target language
110 string[] propertyData = ((string)row[1]).Split(';');
111 string lang = 2 == propertyData.Length ? propertyData[1] : "0";
112
113 Table tempSummaryInfo = (int)SummaryInformation.Transform.TargetPlatformAndLanguage == (int)row[0] ? targetSummaryInfo : updatedSummaryInfo;
114 Table tempPropertyTable = (int)SummaryInformation.Transform.TargetPlatformAndLanguage == (int)row[0] ? targetPropertyTable : updatedPropertyTable;
115
116 Row productLanguageRow = tempPropertyTable.CreateRow(null);
117 productLanguageRow[0] = "ProductLanguage";
118 productLanguageRow[1] = lang;
119
120 // set the platform;language on the MSI to be generated
121 Row templateRow = tempSummaryInfo.CreateRow(null);
122 templateRow[0] = 7; // PID_TEMPLATE
123 templateRow[1] = (string)row[1];
124 }
125 else if ((int)SummaryInformation.Transform.ProductCodes == (int)row[0])
126 {
127 string[] propertyData = ((string)row[1]).Split(';');
128
129 Row targetProductCodeRow = targetPropertyTable.CreateRow(null);
130 targetProductCodeRow[0] = "ProductCode";
131 targetProductCodeRow[1] = propertyData[0].Substring(0, 38);
132
133 Row targetProductVersionRow = targetPropertyTable.CreateRow(null);
134 targetProductVersionRow[0] = "ProductVersion";
135 targetProductVersionRow[1] = propertyData[0].Substring(38);
136
137 Row updatedProductCodeRow = updatedPropertyTable.CreateRow(null);
138 updatedProductCodeRow[0] = "ProductCode";
139 updatedProductCodeRow[1] = propertyData[1].Substring(0, 38);
140
141 Row updatedProductVersionRow = updatedPropertyTable.CreateRow(null);
142 updatedProductVersionRow[0] = "ProductVersion";
143 updatedProductVersionRow[1] = propertyData[1].Substring(38);
144
145 // UpgradeCode is optional and may not exists in the target
146 // or upgraded databases, so do not include a null-valued
147 // UpgradeCode property.
148
149 targetUpgradeCode = propertyData[2];
150 if (!String.IsNullOrEmpty(targetUpgradeCode))
151 {
152 Row targetUpgradeCodeRow = targetPropertyTable.CreateRow(null);
153 targetUpgradeCodeRow[0] = "UpgradeCode";
154 targetUpgradeCodeRow[1] = targetUpgradeCode;
155
156 // If the target UpgradeCode is specified, an updated
157 // UpgradeCode is required.
158 if (String.IsNullOrEmpty(updatedUpgradeCode))
159 {
160 updatedUpgradeCode = targetUpgradeCode;
161 }
162 }
163
164 if (!String.IsNullOrEmpty(updatedUpgradeCode))
165 {
166 Row updatedUpgradeCodeRow = updatedPropertyTable.CreateRow(null);
167 updatedUpgradeCodeRow[0] = "UpgradeCode";
168 updatedUpgradeCodeRow[1] = updatedUpgradeCode;
169 }
170 }
171 else if ((int)SummaryInformation.Transform.ValidationFlags == (int)row[0])
172 {
173 transformFlags = Convert.ToInt32(row[1], CultureInfo.InvariantCulture);
174 }
175 else if ((int)SummaryInformation.Transform.Reserved11 == (int)row[0])
176 {
177 // PID_LASTPRINTED should be null for transforms
178 row.Operation = RowOperation.None;
179 }
180 else
181 {
182 // add everything else as is
183 Row targetRow = targetSummaryInfo.CreateRow(null);
184 targetRow[0] = row[0];
185 targetRow[1] = row[1];
186
187 Row updatedRow = updatedSummaryInfo.CreateRow(null);
188 updatedRow[0] = row[0];
189 updatedRow[1] = row[1];
190 }
191 }
192
193 // Validate that both databases have an UpgradeCode if the
194 // authoring transform will validate the UpgradeCode; otherwise,
195 // MsiCreateTransformSummaryinfo() will fail with 1620.
196 if (((int)TransformFlags.ValidateUpgradeCode & transformFlags) != 0 &&
197 (String.IsNullOrEmpty(targetUpgradeCode) || String.IsNullOrEmpty(updatedUpgradeCode)))
198 {
199 Messaging.Instance.OnMessage(WixErrors.BothUpgradeCodesRequired());
200 }
201
202 string emptyFile = null;
203
204 foreach (Table table in this.Transform.Tables)
205 {
206 // Ignore unreal tables when building transforms except the _Stream table.
207 // These tables are ignored when generating the database so there is no reason
208 // to process them here.
209 if (table.Definition.Unreal && "_Streams" != table.Name)
210 {
211 continue;
212 }
213
214 // process table operations
215 switch (table.Operation)
216 {
217 case TableOperation.Add:
218 updatedOutput.EnsureTable(table.Definition);
219 break;
220 case TableOperation.Drop:
221 targetOutput.EnsureTable(table.Definition);
222 continue;
223 default:
224 targetOutput.EnsureTable(table.Definition);
225 updatedOutput.EnsureTable(table.Definition);
226 break;
227 }
228
229 // process row operations
230 foreach (Row row in table.Rows)
231 {
232 switch (row.Operation)
233 {
234 case RowOperation.Add:
235 Table updatedTable = updatedOutput.EnsureTable(table.Definition);
236 updatedTable.Rows.Add(row);
237 continue;
238 case RowOperation.Delete:
239 Table targetTable = targetOutput.EnsureTable(table.Definition);
240 targetTable.Rows.Add(row);
241
242 // fill-in non-primary key values
243 foreach (Field field in row.Fields)
244 {
245 if (!field.Column.PrimaryKey)
246 {
247 if (ColumnType.Number == field.Column.Type && !field.Column.IsLocalizable)
248 {
249 field.Data = field.Column.MinValue;
250 }
251 else if (ColumnType.Object == field.Column.Type)
252 {
253 if (null == emptyFile)
254 {
255 emptyFile = Path.Combine(this.TempFilesLocation, "empty");
256 }
257
258 field.Data = emptyFile;
259 }
260 else
261 {
262 field.Data = "0";
263 }
264 }
265 }
266 continue;
267 }
268
269 // Assure that the file table's sequence is populated
270 if ("File" == table.Name)
271 {
272 foreach (Row fileRow in table.Rows)
273 {
274 if (null == fileRow[7])
275 {
276 if (RowOperation.Add == fileRow.Operation)
277 {
278 Messaging.Instance.OnMessage(WixErrors.InvalidAddedFileRowWithoutSequence(fileRow.SourceLineNumbers, (string)fileRow[0]));
279 break;
280 }
281
282 // Set to 1 to prevent invalid IDT file from being generated
283 fileRow[7] = 1;
284 }
285 }
286 }
287
288 // process modified and unmodified rows
289 bool modifiedRow = false;
290 Row targetRow = new Row(null, table.Definition);
291 Row updatedRow = row;
292 for (int i = 0; i < row.Fields.Length; i++)
293 {
294 Field updatedField = row.Fields[i];
295
296 if (updatedField.Modified)
297 {
298 // set a different value in the target row to ensure this value will be modified during transform generation
299 if (ColumnType.Number == updatedField.Column.Type && !updatedField.Column.IsLocalizable)
300 {
301 if (null == updatedField.Data || 1 != (int)updatedField.Data)
302 {
303 targetRow[i] = 1;
304 }
305 else
306 {
307 targetRow[i] = 2;
308 }
309 }
310 else if (ColumnType.Object == updatedField.Column.Type)
311 {
312 if (null == emptyFile)
313 {
314 emptyFile = Path.Combine(this.TempFilesLocation, "empty");
315 }
316
317 targetRow[i] = emptyFile;
318 }
319 else
320 {
321 if ("0" != (string)updatedField.Data)
322 {
323 targetRow[i] = "0";
324 }
325 else
326 {
327 targetRow[i] = "1";
328 }
329 }
330
331 modifiedRow = true;
332 }
333 else if (ColumnType.Object == updatedField.Column.Type)
334 {
335 ObjectField objectField = (ObjectField)updatedField;
336
337 // create an empty file for comparing against
338 if (null == objectField.PreviousData)
339 {
340 if (null == emptyFile)
341 {
342 emptyFile = Path.Combine(this.TempFilesLocation, "empty");
343 }
344
345 targetRow[i] = emptyFile;
346 modifiedRow = true;
347 }
348 else if (!this.CompareFiles(objectField.PreviousData, (string)objectField.Data))
349 {
350 targetRow[i] = objectField.PreviousData;
351 modifiedRow = true;
352 }
353 }
354 else // unmodified
355 {
356 if (null != updatedField.Data)
357 {
358 targetRow[i] = updatedField.Data;
359 }
360 }
361 }
362
363 // modified rows and certain special rows go in the target and updated msi databases
364 if (modifiedRow ||
365 ("Property" == table.Name &&
366 ("ProductCode" == (string)row[0] ||
367 "ProductLanguage" == (string)row[0] ||
368 "ProductVersion" == (string)row[0] ||
369 "UpgradeCode" == (string)row[0])))
370 {
371 Table targetTable = targetOutput.EnsureTable(table.Definition);
372 targetTable.Rows.Add(targetRow);
373
374 Table updatedTable = updatedOutput.EnsureTable(table.Definition);
375 updatedTable.Rows.Add(updatedRow);
376 }
377 }
378 }
379
380 //foreach (BinderExtension extension in this.Extensions)
381 //{
382 // extension.PostBind(this.Context);
383 //}
384
385 // Any errors encountered up to this point can cause errors during generation.
386 if (Messaging.Instance.EncounteredError)
387 {
388 return;
389 }
390
391 string transformFileName = Path.GetFileNameWithoutExtension(this.OutputPath);
392 string targetDatabaseFile = Path.Combine(this.TempFilesLocation, String.Concat(transformFileName, "_target.msi"));
393 string updatedDatabaseFile = Path.Combine(this.TempFilesLocation, String.Concat(transformFileName, "_updated.msi"));
394
395 try
396 {
397 if (!String.IsNullOrEmpty(emptyFile))
398 {
399 using (FileStream fileStream = File.Create(emptyFile))
400 {
401 }
402 }
403
404 this.GenerateDatabase(targetOutput, targetDatabaseFile, false);
405 this.GenerateDatabase(updatedOutput, updatedDatabaseFile, true);
406
407 // make sure the directory exists
408 Directory.CreateDirectory(Path.GetDirectoryName(this.OutputPath));
409
410 // create the transform file
411 using (Database targetDatabase = new Database(targetDatabaseFile, OpenDatabase.ReadOnly))
412 {
413 using (Database updatedDatabase = new Database(updatedDatabaseFile, OpenDatabase.ReadOnly))
414 {
415 if (updatedDatabase.GenerateTransform(targetDatabase, this.OutputPath))
416 {
417 updatedDatabase.CreateTransformSummaryInfo(targetDatabase, this.OutputPath, (TransformErrorConditions)(transformFlags & 0xFFFF), (TransformValidations)((transformFlags >> 16) & 0xFFFF));
418 }
419 else
420 {
421 Messaging.Instance.OnMessage(WixErrors.NoDifferencesInTransform(this.Transform.SourceLineNumbers));
422 }
423 }
424 }
425 }
426 finally
427 {
428 if (!String.IsNullOrEmpty(emptyFile))
429 {
430 File.Delete(emptyFile);
431 }
432 }
433 }
434
435 private bool CompareFiles(string targetFile, string updatedFile)
436 {
437 bool? compared = null;
438 foreach (var extension in this.Extensions)
439 {
440 compared = extension.CompareFiles(targetFile, updatedFile);
441 if (compared.HasValue)
442 {
443 break;
444 }
445 }
446
447 if (!compared.HasValue)
448 {
449 throw new InvalidOperationException(); // TODO: something needs to be said here that none of the binder file managers returned a result.
450 }
451
452 return compared.Value;
453 }
454
455 private void GenerateDatabase(Output output, string outputPath, bool keepAddedColumns)
456 {
457 var command = new GenerateDatabaseCommand();
458 command.Codepage = output.Codepage;
459 command.Extensions = this.Extensions;
460 command.KeepAddedColumns = keepAddedColumns;
461 command.Output = output;
462 command.OutputPath = outputPath;
463 command.TableDefinitions = this.TableDefinitions;
464 command.TempFilesLocation = this.TempFilesLocation;
465 command.SuppressAddingValidationRows = true;
466 command.UseSubDirectory = true;
467 command.Execute();
468 }
469 }
470}
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/CabinetBuilder.cs b/src/WixToolset.Core.WindowsInstaller/Bind/CabinetBuilder.cs
new file mode 100644
index 00000000..b2cc76fc
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/CabinetBuilder.cs
@@ -0,0 +1,177 @@
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;
7 using System.IO;
8 using System.Linq;
9 using System.Threading;
10 using WixToolset.Core.Bind;
11 using WixToolset.Core.Cab;
12 using WixToolset.Data;
13
14 /// <summary>
15 /// Builds cabinets using multiple threads. This implements a thread pool that generates cabinets with multiple
16 /// threads. Unlike System.Threading.ThreadPool, it waits until all threads are finished.
17 /// </summary>
18 internal sealed class CabinetBuilder
19 {
20 private Queue cabinetWorkItems;
21 private object lockObject;
22 private int threadCount;
23
24 // Address of Binder's callback function for Cabinet Splitting
25 private IntPtr newCabNamesCallBackAddress;
26
27 public int MaximumCabinetSizeForLargeFileSplitting { get; set; }
28
29 public int MaximumUncompressedMediaSize { get; set; }
30
31 /// <summary>
32 /// Instantiate a new CabinetBuilder.
33 /// </summary>
34 /// <param name="threadCount">number of threads to use</param>
35 /// <param name="newCabNamesCallBackAddress">Address of Binder's callback function for Cabinet Splitting</param>
36 public CabinetBuilder(int threadCount, IntPtr newCabNamesCallBackAddress)
37 {
38 if (0 >= threadCount)
39 {
40 throw new ArgumentOutOfRangeException("threadCount");
41 }
42
43 this.cabinetWorkItems = new Queue();
44 this.lockObject = new object();
45
46 this.threadCount = threadCount;
47
48 // Set Address of Binder's callback function for Cabinet Splitting
49 this.newCabNamesCallBackAddress = newCabNamesCallBackAddress;
50 }
51
52 /// <summary>
53 /// Enqueues a CabinetWorkItem to the queue.
54 /// </summary>
55 /// <param name="cabinetWorkItem">cabinet work item</param>
56 public void Enqueue(CabinetWorkItem cabinetWorkItem)
57 {
58 this.cabinetWorkItems.Enqueue(cabinetWorkItem);
59 }
60
61 /// <summary>
62 /// Create the queued cabinets.
63 /// </summary>
64 /// <returns>error message number (zero if no error)</returns>
65 public void CreateQueuedCabinets()
66 {
67 // don't create more threads than the number of cabinets to build
68 if (this.cabinetWorkItems.Count < this.threadCount)
69 {
70 this.threadCount = this.cabinetWorkItems.Count;
71 }
72
73 if (0 < this.threadCount)
74 {
75 Thread[] threads = new Thread[this.threadCount];
76
77 for (int i = 0; i < threads.Length; i++)
78 {
79 threads[i] = new Thread(new ThreadStart(this.ProcessWorkItems));
80 threads[i].Start();
81 }
82
83 // wait for all threads to finish
84 foreach (Thread thread in threads)
85 {
86 thread.Join();
87 }
88 }
89 }
90
91 /// <summary>
92 /// This function gets called by multiple threads to do actual work.
93 /// It takes one work item at a time and calls this.CreateCabinet().
94 /// It does not return until cabinetWorkItems queue is empty
95 /// </summary>
96 private void ProcessWorkItems()
97 {
98 try
99 {
100 while (true)
101 {
102 CabinetWorkItem cabinetWorkItem;
103
104 lock (this.cabinetWorkItems)
105 {
106 // check if there are any more cabinets to create
107 if (0 == this.cabinetWorkItems.Count)
108 {
109 break;
110 }
111
112 cabinetWorkItem = (CabinetWorkItem)this.cabinetWorkItems.Dequeue();
113 }
114
115 // create a cabinet
116 this.CreateCabinet(cabinetWorkItem);
117 }
118 }
119 catch (WixException we)
120 {
121 Messaging.Instance.OnMessage(we.Error);
122 }
123 catch (Exception e)
124 {
125 Messaging.Instance.OnMessage(WixErrors.UnexpectedException(e.Message, e.GetType().ToString(), e.StackTrace));
126 }
127 }
128
129 /// <summary>
130 /// Creates a cabinet using the wixcab.dll interop layer.
131 /// </summary>
132 /// <param name="cabinetWorkItem">CabinetWorkItem containing information about the cabinet to create.</param>
133 private void CreateCabinet(CabinetWorkItem cabinetWorkItem)
134 {
135 Messaging.Instance.OnMessage(WixVerboses.CreateCabinet(cabinetWorkItem.CabinetFile));
136
137 int maxCabinetSize = 0; // The value of 0 corresponds to default of 2GB which means no cabinet splitting
138 ulong maxPreCompressedSizeInBytes = 0;
139
140 if (MaximumCabinetSizeForLargeFileSplitting != 0)
141 {
142 // User Specified Max Cab Size for File Splitting, So Check if this cabinet has a single file larger than MaximumUncompressedFileSize
143 // If a file is larger than MaximumUncompressedFileSize, then the cabinet containing it will have only this file
144 if (1 == cabinetWorkItem.FileFacades.Count())
145 {
146 // Cabinet has Single File, Check if this is Large File than needs Splitting into Multiple cabs
147 // Get the Value for Max Uncompressed Media Size
148 maxPreCompressedSizeInBytes = (ulong)MaximumUncompressedMediaSize * 1024 * 1024;
149
150 foreach (FileFacade facade in cabinetWorkItem.FileFacades) // No other easy way than looping to get the only row
151 {
152 if ((ulong)facade.File.FileSize >= maxPreCompressedSizeInBytes)
153 {
154 // If file is larger than MaximumUncompressedFileSize set Maximum Cabinet Size for Cabinet Splitting
155 maxCabinetSize = MaximumCabinetSizeForLargeFileSplitting;
156 }
157 }
158 }
159 }
160
161 // create the cabinet file
162 string cabinetFileName = Path.GetFileName(cabinetWorkItem.CabinetFile);
163 string cabinetDirectory = Path.GetDirectoryName(cabinetWorkItem.CabinetFile);
164
165 using (WixCreateCab cab = new WixCreateCab(cabinetFileName, cabinetDirectory, cabinetWorkItem.FileFacades.Count(), maxCabinetSize, cabinetWorkItem.MaxThreshold, cabinetWorkItem.CompressionLevel))
166 {
167 foreach (FileFacade facade in cabinetWorkItem.FileFacades)
168 {
169 cab.AddFile(facade);
170 }
171
172 cab.Complete(newCabNamesCallBackAddress);
173 }
174 }
175 }
176}
177
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/CabinetResolver.cs b/src/WixToolset.Core.WindowsInstaller/Bind/CabinetResolver.cs
new file mode 100644
index 00000000..df1ccecf
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/CabinetResolver.cs
@@ -0,0 +1,122 @@
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.Bind
4{
5 using System;
6 using System.Collections.Generic;
7 using System.IO;
8 using System.Linq;
9 using WixToolset.Core.Cab;
10 using WixToolset.Core.Bind;
11 using WixToolset.Data;
12 using WixToolset.Extensibility;
13
14 public class CabinetResolver
15 {
16 public CabinetResolver(string cabCachePath, IEnumerable<IWindowsInstallerBackendExtension> backendExtensions)
17 {
18 this.CabCachePath = cabCachePath;
19
20 this.BackendExtensions = backendExtensions;
21 }
22
23 private string CabCachePath { get; }
24
25 private IEnumerable<IWindowsInstallerBackendExtension> BackendExtensions { get; }
26
27 public ResolvedCabinet ResolveCabinet(string cabinetPath, IEnumerable<FileFacade> fileFacades)
28 {
29 var filesWithPath = fileFacades.Select(f => new BindFileWithPath() { Id = f.File.File, Path = f.WixFile.Source }).ToList();
30
31 ResolvedCabinet resolved = null;
32
33 foreach (var extension in this.BackendExtensions)
34 {
35 resolved = extension.ResolveCabinet(cabinetPath, filesWithPath);
36
37 if (null != resolved)
38 {
39 return resolved;
40 }
41 }
42
43 // By default cabinet should be built and moved to the suggested location.
44 resolved = new ResolvedCabinet() { BuildOption = CabinetBuildOption.BuildAndMove, Path = cabinetPath };
45
46 // If a cabinet cache path was provided, change the location for the cabinet
47 // to be built to and check if there is a cabinet that can be reused.
48 if (!String.IsNullOrEmpty(this.CabCachePath))
49 {
50 string cabinetName = Path.GetFileName(cabinetPath);
51 resolved.Path = Path.Combine(this.CabCachePath, cabinetName);
52
53 if (CheckFileExists(resolved.Path))
54 {
55 // Assume that none of the following are true:
56 // 1. any files are added or removed
57 // 2. order of files changed or names changed
58 // 3. modified time changed
59 bool cabinetValid = true;
60
61 // Need to force garbage collection of WixEnumerateCab to ensure the handle
62 // associated with it is closed before it is reused.
63 using (var wixEnumerateCab = new WixEnumerateCab())
64 {
65 List<CabinetFileInfo> fileList = wixEnumerateCab.Enumerate(resolved.Path);
66
67 if (filesWithPath.Count() != fileList.Count)
68 {
69 cabinetValid = false;
70 }
71 else
72 {
73 int i = 0;
74 foreach (BindFileWithPath file in filesWithPath)
75 {
76 // First check that the file identifiers match because that is quick and easy.
77 CabinetFileInfo cabFileInfo = fileList[i];
78 cabinetValid = (cabFileInfo.FileId == file.Id);
79 if (cabinetValid)
80 {
81 // Still valid so ensure the file sizes are the same.
82 FileInfo fileInfo = new FileInfo(file.Path);
83 cabinetValid = (cabFileInfo.Size == fileInfo.Length);
84 if (cabinetValid)
85 {
86 // Still valid so ensure the source time stamp hasn't changed. Thus we need
87 // to convert the source file time stamp into a cabinet compatible data/time.
88 Native.CabInterop.DateTimeToCabDateAndTime(fileInfo.LastWriteTime, out var sourceCabDate, out var sourceCabTime);
89 cabinetValid = (cabFileInfo.Date == sourceCabDate && cabFileInfo.Time == sourceCabTime);
90 }
91 }
92
93 if (!cabinetValid)
94 {
95 break;
96 }
97
98 i++;
99 }
100 }
101 }
102
103 resolved.BuildOption = cabinetValid ? CabinetBuildOption.Copy : CabinetBuildOption.BuildAndCopy;
104 }
105 }
106
107 return resolved;
108 }
109
110 private static bool CheckFileExists(string path)
111 {
112 try
113 {
114 return File.Exists(path);
115 }
116 catch (ArgumentException)
117 {
118 throw new WixException(WixErrors.IllegalCharactersInPath(path));
119 }
120 }
121 }
122}
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/CabinetWorkItem.cs b/src/WixToolset.Core.WindowsInstaller/Bind/CabinetWorkItem.cs
new file mode 100644
index 00000000..dcafcd36
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/CabinetWorkItem.cs
@@ -0,0 +1,79 @@
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.Collections.Generic;
6 using WixToolset.Core.Bind;
7 using WixToolset.Data;
8 using WixToolset.Data.Rows;
9
10 /// <summary>
11 /// A cabinet builder work item.
12 /// </summary>
13 internal sealed class CabinetWorkItem
14 {
15 private string cabinetFile;
16 private CompressionLevel compressionLevel;
17 //private BinderFileManager binderFileManager;
18 private int maxThreshold;
19
20 /// <summary>
21 /// Instantiate a new CabinetWorkItem.
22 /// </summary>
23 /// <param name="fileFacades">The collection of files in this cabinet.</param>
24 /// <param name="cabinetFile">The cabinet file.</param>
25 /// <param name="maxThreshold">Maximum threshold for each cabinet.</param>
26 /// <param name="compressionLevel">The compression level of the cabinet.</param>
27 /// <param name="binderFileManager">The binder file manager.</param>
28 public CabinetWorkItem(IEnumerable<FileFacade> fileFacades, string cabinetFile, int maxThreshold, CompressionLevel compressionLevel /*, BinderFileManager binderFileManager*/)
29 {
30 this.cabinetFile = cabinetFile;
31 this.compressionLevel = compressionLevel;
32 this.FileFacades = fileFacades;
33 //this.binderFileManager = binderFileManager;
34 this.maxThreshold = maxThreshold;
35 }
36
37 /// <summary>
38 /// Gets the cabinet file.
39 /// </summary>
40 /// <value>The cabinet file.</value>
41 public string CabinetFile
42 {
43 get { return this.cabinetFile; }
44 }
45
46 /// <summary>
47 /// Gets the compression level of the cabinet.
48 /// </summary>
49 /// <value>The compression level of the cabinet.</value>
50 public CompressionLevel CompressionLevel
51 {
52 get { return this.compressionLevel; }
53 }
54
55 /// <summary>
56 /// Gets the collection of files in this cabinet.
57 /// </summary>
58 /// <value>The collection of files in this cabinet.</value>
59 public IEnumerable<FileFacade> FileFacades { get; private set; }
60
61 /// <summary>
62 /// Gets the binder file manager.
63 /// </summary>
64 /// <value>The binder file manager.</value>
65 //public BinderFileManager BinderFileManager
66 //{
67 // get { return this.binderFileManager; }
68 //}
69
70 /// <summary>
71 /// Gets the max threshold.
72 /// </summary>
73 /// <value>The maximum threshold for a folder in a cabinet.</value>
74 public int MaxThreshold
75 {
76 get { return this.maxThreshold; }
77 }
78 }
79}
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/ConfigurationCallback.cs b/src/WixToolset.Core.WindowsInstaller/Bind/ConfigurationCallback.cs
new file mode 100644
index 00000000..d4d3799f
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/ConfigurationCallback.cs
@@ -0,0 +1,91 @@
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;
7 using System.Globalization;
8 using WixToolset.MergeMod;
9
10 /// <summary>
11 /// Callback object for configurable merge modules.
12 /// </summary>
13 internal sealed class ConfigurationCallback : IMsmConfigureModule
14 {
15 private const int SOk = 0x0;
16 private const int SFalse = 0x1;
17 private Hashtable configurationData;
18
19 /// <summary>
20 /// Creates a ConfigurationCallback object.
21 /// </summary>
22 /// <param name="configData">String to break up into name/value pairs.</param>
23 public ConfigurationCallback(string configData)
24 {
25 if (String.IsNullOrEmpty(configData))
26 {
27 throw new ArgumentNullException("configData");
28 }
29
30 string[] pairs = configData.Split(',');
31 this.configurationData = new Hashtable(pairs.Length);
32 for (int i = 0; i < pairs.Length; ++i)
33 {
34 string[] nameVal = pairs[i].Split('=');
35 string name = nameVal[0];
36 string value = nameVal[1];
37
38 name = name.Replace("%2C", ",");
39 name = name.Replace("%3D", "=");
40 name = name.Replace("%25", "%");
41
42 value = value.Replace("%2C", ",");
43 value = value.Replace("%3D", "=");
44 value = value.Replace("%25", "%");
45
46 this.configurationData[name] = value;
47 }
48 }
49
50 /// <summary>
51 /// Returns text data based on name.
52 /// </summary>
53 /// <param name="name">Name of value to return.</param>
54 /// <param name="configData">Out param to put configuration data into.</param>
55 /// <returns>S_OK if value provided, S_FALSE if not.</returns>
56 public int ProvideTextData(string name, out string configData)
57 {
58 if (this.configurationData.Contains(name))
59 {
60 configData = (string)this.configurationData[name];
61 return SOk;
62 }
63 else
64 {
65 configData = null;
66 return SFalse;
67 }
68 }
69
70 /// <summary>
71 /// Returns integer data based on name.
72 /// </summary>
73 /// <param name="name">Name of value to return.</param>
74 /// <param name="configData">Out param to put configuration data into.</param>
75 /// <returns>S_OK if value provided, S_FALSE if not.</returns>
76 public int ProvideIntegerData(string name, out int configData)
77 {
78 if (this.configurationData.Contains(name))
79 {
80 string val = (string)this.configurationData[name];
81 configData = Convert.ToInt32(val, CultureInfo.InvariantCulture);
82 return SOk;
83 }
84 else
85 {
86 configData = 0;
87 return SFalse;
88 }
89 }
90 }
91}
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/CopyTransformDataCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/CopyTransformDataCommand.cs
new file mode 100644
index 00000000..6388a352
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/CopyTransformDataCommand.cs
@@ -0,0 +1,606 @@
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.Diagnostics;
8 using WixToolset.Data;
9 using WixToolset.Data.Rows;
10 using WixToolset.Extensibility;
11 using WixToolset.Core.Native;
12 using WixToolset.Core.Bind;
13
14 internal class CopyTransformDataCommand
15 {
16 public bool CopyOutFileRows { private get; set; }
17
18 public IEnumerable<IBinderExtension> Extensions { private get; set; }
19
20 public Output Output { private get; set; }
21
22 public TableDefinitionCollection TableDefinitions { private get; set; }
23
24 public IEnumerable<FileFacade> FileFacades { get; private set; }
25
26 public void Execute()
27 {
28 Debug.Assert(OutputType.Patch != this.Output.Type);
29
30 List<FileFacade> allFileRows = this.CopyOutFileRows ? new List<FileFacade>() : null;
31
32#if false // TODO: Fix this patching related code to work correctly with FileFacades.
33 bool copyToPatch = (allFileRows != null);
34 bool copyFromPatch = !copyToPatch;
35
36 RowDictionary<MediaRow> patchMediaRows = new RowDictionary<MediaRow>();
37
38 Dictionary<int, RowDictionary<WixFileRow>> patchMediaFileRows = new Dictionary<int, RowDictionary<WixFileRow>>();
39
40 Table patchActualFileTable = this.Output.EnsureTable(this.TableDefinitions["File"]);
41 Table patchFileTable = this.Output.EnsureTable(this.TableDefinitions["WixFile"]);
42
43 if (copyFromPatch)
44 {
45 // index patch files by diskId+fileId
46 foreach (WixFileRow patchFileRow in patchFileTable.Rows)
47 {
48 int diskId = patchFileRow.DiskId;
49 RowDictionary<WixFileRow> mediaFileRows;
50 if (!patchMediaFileRows.TryGetValue(diskId, out mediaFileRows))
51 {
52 mediaFileRows = new RowDictionary<WixFileRow>();
53 patchMediaFileRows.Add(diskId, mediaFileRows);
54 }
55
56 mediaFileRows.Add(patchFileRow);
57 }
58
59 Table patchMediaTable = this.Output.EnsureTable(this.TableDefinitions["Media"]);
60 patchMediaRows = new RowDictionary<MediaRow>(patchMediaTable);
61 }
62
63 // index paired transforms
64 Dictionary<string, Output> pairedTransforms = new Dictionary<string, Output>();
65 foreach (SubStorage substorage in this.Output.SubStorages)
66 {
67 if (substorage.Name.StartsWith("#"))
68 {
69 pairedTransforms.Add(substorage.Name.Substring(1), substorage.Data);
70 }
71 }
72
73 try
74 {
75 // copy File bind data into substorages
76 foreach (SubStorage substorage in this.Output.SubStorages)
77 {
78 if (substorage.Name.StartsWith("#"))
79 {
80 // no changes necessary for paired transforms
81 continue;
82 }
83
84 Output mainTransform = substorage.Data;
85 Table mainWixFileTable = mainTransform.Tables["WixFile"];
86 Table mainMsiFileHashTable = mainTransform.Tables["MsiFileHash"];
87
88 this.FileManagerCore.ActiveSubStorage = substorage;
89
90 RowDictionary<WixFileRow> mainWixFiles = new RowDictionary<WixFileRow>(mainWixFileTable);
91 RowDictionary<Row> mainMsiFileHashIndex = new RowDictionary<Row>();
92
93 Table mainFileTable = mainTransform.Tables["File"];
94 Output pairedTransform = (Output)pairedTransforms[substorage.Name];
95
96 // copy Media.LastSequence and index the MsiFileHash table if it exists.
97 if (copyFromPatch)
98 {
99 Table pairedMediaTable = pairedTransform.Tables["Media"];
100 foreach (MediaRow pairedMediaRow in pairedMediaTable.Rows)
101 {
102 MediaRow patchMediaRow = patchMediaRows.Get(pairedMediaRow.DiskId);
103 pairedMediaRow.Fields[1] = patchMediaRow.Fields[1];
104 }
105
106 if (null != mainMsiFileHashTable)
107 {
108 mainMsiFileHashIndex = new RowDictionary<Row>(mainMsiFileHashTable);
109 }
110
111 // Validate file row changes for keypath-related issues
112 this.ValidateFileRowChanges(mainTransform);
113 }
114
115 // Index File table of pairedTransform
116 Table pairedFileTable = pairedTransform.Tables["File"];
117 RowDictionary<FileRow> pairedFileRows = new RowDictionary<FileRow>(pairedFileTable);
118
119 if (null != mainFileTable)
120 {
121 if (copyFromPatch)
122 {
123 // Remove the MsiFileHash table because it will be updated later with the final file hash for each file
124 mainTransform.Tables.Remove("MsiFileHash");
125 }
126
127 foreach (FileRow mainFileRow in mainFileTable.Rows)
128 {
129 if (RowOperation.Delete == mainFileRow.Operation)
130 {
131 continue;
132 }
133 else if (RowOperation.None == mainFileRow.Operation && !copyToPatch)
134 {
135 continue;
136 }
137
138 WixFileRow mainWixFileRow = mainWixFiles.Get(mainFileRow.File);
139
140 if (copyToPatch) // when copying to the patch, we need compare the underlying files and include all file changes.
141 {
142 ObjectField objectField = (ObjectField)mainWixFileRow.Fields[6];
143 FileRow pairedFileRow = pairedFileRows.Get(mainFileRow.File);
144
145 // If the file is new, we always need to add it to the patch.
146 if (mainFileRow.Operation != RowOperation.Add)
147 {
148 // If PreviousData doesn't exist, target and upgrade layout point to the same location. No need to compare.
149 if (null == objectField.PreviousData)
150 {
151 if (mainFileRow.Operation == RowOperation.None)
152 {
153 continue;
154 }
155 }
156 else
157 {
158 // TODO: should this entire condition be placed in the binder file manager?
159 if ((0 == (PatchAttributeType.Ignore & mainWixFileRow.PatchAttributes)) &&
160 !this.CompareFiles(objectField.PreviousData.ToString(), objectField.Data.ToString()))
161 {
162 // If the file is different, we need to mark the mainFileRow and pairedFileRow as modified.
163 mainFileRow.Operation = RowOperation.Modify;
164 if (null != pairedFileRow)
165 {
166 // Always patch-added, but never non-compressed.
167 pairedFileRow.Attributes |= MsiInterop.MsidbFileAttributesPatchAdded;
168 pairedFileRow.Attributes &= ~MsiInterop.MsidbFileAttributesNoncompressed;
169 pairedFileRow.Fields[6].Modified = true;
170 pairedFileRow.Operation = RowOperation.Modify;
171 }
172 }
173 else
174 {
175 // The File is same. We need mark all the attributes as unchanged.
176 mainFileRow.Operation = RowOperation.None;
177 foreach (Field field in mainFileRow.Fields)
178 {
179 field.Modified = false;
180 }
181
182 if (null != pairedFileRow)
183 {
184 pairedFileRow.Attributes &= ~MsiInterop.MsidbFileAttributesPatchAdded;
185 pairedFileRow.Fields[6].Modified = false;
186 pairedFileRow.Operation = RowOperation.None;
187 }
188 continue;
189 }
190 }
191 }
192 else if (null != pairedFileRow) // RowOperation.Add
193 {
194 // Always patch-added, but never non-compressed.
195 pairedFileRow.Attributes |= MsiInterop.MsidbFileAttributesPatchAdded;
196 pairedFileRow.Attributes &= ~MsiInterop.MsidbFileAttributesNoncompressed;
197 pairedFileRow.Fields[6].Modified = true;
198 pairedFileRow.Operation = RowOperation.Add;
199 }
200 }
201
202 // index patch files by diskId+fileId
203 int diskId = mainWixFileRow.DiskId;
204
205 RowDictionary<WixFileRow> mediaFileRows;
206 if (!patchMediaFileRows.TryGetValue(diskId, out mediaFileRows))
207 {
208 mediaFileRows = new RowDictionary<WixFileRow>();
209 patchMediaFileRows.Add(diskId, mediaFileRows);
210 }
211
212 string fileId = mainFileRow.File;
213 WixFileRow patchFileRow = mediaFileRows.Get(fileId);
214 if (copyToPatch)
215 {
216 if (null == patchFileRow)
217 {
218 FileRow patchActualFileRow = (FileRow)patchFileTable.CreateRow(mainFileRow.SourceLineNumbers);
219 patchActualFileRow.CopyFrom(mainFileRow);
220
221 patchFileRow = (WixFileRow)patchFileTable.CreateRow(mainFileRow.SourceLineNumbers);
222 patchFileRow.CopyFrom(mainWixFileRow);
223
224 mediaFileRows.Add(patchFileRow);
225
226 allFileRows.Add(new FileFacade(patchActualFileRow, patchFileRow, null)); // TODO: should we be passing along delta information? Probably, right?
227 }
228 else
229 {
230 // TODO: confirm the rest of data is identical?
231
232 // make sure Source is same. Otherwise we are silently ignoring a file.
233 if (0 != String.Compare(patchFileRow.Source, mainWixFileRow.Source, StringComparison.OrdinalIgnoreCase))
234 {
235 Messaging.Instance.OnMessage(WixErrors.SameFileIdDifferentSource(mainFileRow.SourceLineNumbers, fileId, patchFileRow.Source, mainWixFileRow.Source));
236 }
237
238 // capture the previous file versions (and associated data) from this targeted instance of the baseline into the current filerow.
239 patchFileRow.AppendPreviousDataFrom(mainWixFileRow);
240 }
241 }
242 else
243 {
244 // copy data from the patch back to the transform
245 if (null != patchFileRow)
246 {
247 FileRow pairedFileRow = (FileRow)pairedFileRows.Get(fileId);
248 for (int i = 0; i < patchFileRow.Fields.Length; i++)
249 {
250 string patchValue = patchFileRow[i] == null ? "" : patchFileRow[i].ToString();
251 string mainValue = mainFileRow[i] == null ? "" : mainFileRow[i].ToString();
252
253 if (1 == i)
254 {
255 // File.Component_ changes should not come from the shared file rows
256 // that contain the file information as each individual transform might
257 // have different changes (or no changes at all).
258 }
259 // File.Attributes should not changed for binary deltas
260 else if (6 == i)
261 {
262 if (null != patchFileRow.Patch)
263 {
264 // File.Attribute should not change for binary deltas
265 pairedFileRow.Attributes = mainFileRow.Attributes;
266 mainFileRow.Fields[i].Modified = false;
267 }
268 }
269 // File.Sequence is updated in pairedTransform, not mainTransform
270 else if (7 == i)
271 {
272 // file sequence is updated in Patch table instead of File table for delta patches
273 if (null != patchFileRow.Patch)
274 {
275 pairedFileRow.Fields[i].Modified = false;
276 }
277 else
278 {
279 pairedFileRow[i] = patchFileRow[i];
280 pairedFileRow.Fields[i].Modified = true;
281 }
282 mainFileRow.Fields[i].Modified = false;
283 }
284 else if (patchValue != mainValue)
285 {
286 mainFileRow[i] = patchFileRow[i];
287 mainFileRow.Fields[i].Modified = true;
288 if (mainFileRow.Operation == RowOperation.None)
289 {
290 mainFileRow.Operation = RowOperation.Modify;
291 }
292 }
293 }
294
295 // copy MsiFileHash row for this File
296 Row patchHashRow;
297 if (!mainMsiFileHashIndex.TryGetValue(patchFileRow.File, out patchHashRow))
298 {
299 patchHashRow = patchFileRow.Hash;
300 }
301
302 if (null != patchHashRow)
303 {
304 Table mainHashTable = mainTransform.EnsureTable(this.TableDefinitions["MsiFileHash"]);
305 Row mainHashRow = mainHashTable.CreateRow(mainFileRow.SourceLineNumbers);
306 for (int i = 0; i < patchHashRow.Fields.Length; i++)
307 {
308 mainHashRow[i] = patchHashRow[i];
309 if (i > 1)
310 {
311 // assume all hash fields have been modified
312 mainHashRow.Fields[i].Modified = true;
313 }
314 }
315
316 // assume the MsiFileHash operation follows the File one
317 mainHashRow.Operation = mainFileRow.Operation;
318 }
319
320 // copy MsiAssemblyName rows for this File
321 List<Row> patchAssemblyNameRows = patchFileRow.AssemblyNames;
322 if (null != patchAssemblyNameRows)
323 {
324 Table mainAssemblyNameTable = mainTransform.EnsureTable(this.TableDefinitions["MsiAssemblyName"]);
325 foreach (Row patchAssemblyNameRow in patchAssemblyNameRows)
326 {
327 // Copy if there isn't an identical modified/added row already in the transform.
328 bool foundMatchingModifiedRow = false;
329 foreach (Row mainAssemblyNameRow in mainAssemblyNameTable.Rows)
330 {
331 if (RowOperation.None != mainAssemblyNameRow.Operation && mainAssemblyNameRow.GetPrimaryKey('/').Equals(patchAssemblyNameRow.GetPrimaryKey('/')))
332 {
333 foundMatchingModifiedRow = true;
334 break;
335 }
336 }
337
338 if (!foundMatchingModifiedRow)
339 {
340 Row mainAssemblyNameRow = mainAssemblyNameTable.CreateRow(mainFileRow.SourceLineNumbers);
341 for (int i = 0; i < patchAssemblyNameRow.Fields.Length; i++)
342 {
343 mainAssemblyNameRow[i] = patchAssemblyNameRow[i];
344 }
345
346 // assume value field has been modified
347 mainAssemblyNameRow.Fields[2].Modified = true;
348 mainAssemblyNameRow.Operation = mainFileRow.Operation;
349 }
350 }
351 }
352
353 // Add patch header for this file
354 if (null != patchFileRow.Patch)
355 {
356 // Add the PatchFiles action automatically to the AdminExecuteSequence and InstallExecuteSequence tables.
357 AddPatchFilesActionToSequenceTable(SequenceTable.AdminExecuteSequence, mainTransform, pairedTransform, mainFileRow);
358 AddPatchFilesActionToSequenceTable(SequenceTable.InstallExecuteSequence, mainTransform, pairedTransform, mainFileRow);
359
360 // Add to Patch table
361 Table patchTable = pairedTransform.EnsureTable(this.TableDefinitions["Patch"]);
362 if (0 == patchTable.Rows.Count)
363 {
364 patchTable.Operation = TableOperation.Add;
365 }
366
367 Row patchRow = patchTable.CreateRow(mainFileRow.SourceLineNumbers);
368 patchRow[0] = patchFileRow.File;
369 patchRow[1] = patchFileRow.Sequence;
370
371 FileInfo patchFile = new FileInfo(patchFileRow.Source);
372 patchRow[2] = (int)patchFile.Length;
373 patchRow[3] = 0 == (PatchAttributeType.AllowIgnoreOnError & patchFileRow.PatchAttributes) ? 0 : 1;
374
375 string streamName = patchTable.Name + "." + patchRow[0] + "." + patchRow[1];
376 if (MsiInterop.MsiMaxStreamNameLength < streamName.Length)
377 {
378 streamName = "_" + Guid.NewGuid().ToString("D").ToUpperInvariant().Replace('-', '_');
379 Table patchHeadersTable = pairedTransform.EnsureTable(this.TableDefinitions["MsiPatchHeaders"]);
380 if (0 == patchHeadersTable.Rows.Count)
381 {
382 patchHeadersTable.Operation = TableOperation.Add;
383 }
384 Row patchHeadersRow = patchHeadersTable.CreateRow(mainFileRow.SourceLineNumbers);
385 patchHeadersRow[0] = streamName;
386 patchHeadersRow[1] = patchFileRow.Patch;
387 patchRow[5] = streamName;
388 patchHeadersRow.Operation = RowOperation.Add;
389 }
390 else
391 {
392 patchRow[4] = patchFileRow.Patch;
393 }
394 patchRow.Operation = RowOperation.Add;
395 }
396 }
397 else
398 {
399 // TODO: throw because all transform rows should have made it into the patch
400 }
401 }
402 }
403 }
404
405 if (copyFromPatch)
406 {
407 this.Output.Tables.Remove("Media");
408 this.Output.Tables.Remove("File");
409 this.Output.Tables.Remove("MsiFileHash");
410 this.Output.Tables.Remove("MsiAssemblyName");
411 }
412 }
413 }
414 finally
415 {
416 this.FileManagerCore.ActiveSubStorage = null;
417 }
418#endif
419 this.FileFacades = allFileRows;
420 }
421
422 /// <summary>
423 /// Adds the PatchFiles action to the sequence table if it does not already exist.
424 /// </summary>
425 /// <param name="table">The sequence table to check or modify.</param>
426 /// <param name="mainTransform">The primary authoring transform.</param>
427 /// <param name="pairedTransform">The secondary patch transform.</param>
428 /// <param name="mainFileRow">The file row that contains information about the patched file.</param>
429 private void AddPatchFilesActionToSequenceTable(SequenceTable table, Output mainTransform, Output pairedTransform, Row mainFileRow)
430 {
431 // Find/add PatchFiles action (also determine sequence for it).
432 // Search mainTransform first, then pairedTransform (pairedTransform overrides).
433 bool hasPatchFilesAction = false;
434 int seqInstallFiles = 0;
435 int seqDuplicateFiles = 0;
436 string tableName = table.ToString();
437
438 TestSequenceTableForPatchFilesAction(
439 mainTransform.Tables[tableName],
440 ref hasPatchFilesAction,
441 ref seqInstallFiles,
442 ref seqDuplicateFiles);
443 TestSequenceTableForPatchFilesAction(
444 pairedTransform.Tables[tableName],
445 ref hasPatchFilesAction,
446 ref seqInstallFiles,
447 ref seqDuplicateFiles);
448 if (!hasPatchFilesAction)
449 {
450 Table iesTable = pairedTransform.EnsureTable(this.TableDefinitions[tableName]);
451 if (0 == iesTable.Rows.Count)
452 {
453 iesTable.Operation = TableOperation.Add;
454 }
455
456 Row patchAction = iesTable.CreateRow(null);
457 WixActionRow wixPatchAction = WindowsInstallerStandard.GetStandardActions()[table, "PatchFiles"];
458 int sequence = wixPatchAction.Sequence;
459 // Test for default sequence value's appropriateness
460 if (seqInstallFiles >= sequence || (0 != seqDuplicateFiles && seqDuplicateFiles <= sequence))
461 {
462 if (0 != seqDuplicateFiles)
463 {
464 if (seqDuplicateFiles < seqInstallFiles)
465 {
466 throw new WixException(WixErrors.InsertInvalidSequenceActionOrder(mainFileRow.SourceLineNumbers, iesTable.Name, "InstallFiles", "DuplicateFiles", wixPatchAction.Action));
467 }
468 else
469 {
470 sequence = (seqDuplicateFiles + seqInstallFiles) / 2;
471 if (seqInstallFiles == sequence || seqDuplicateFiles == sequence)
472 {
473 throw new WixException(WixErrors.InsertSequenceNoSpace(mainFileRow.SourceLineNumbers, iesTable.Name, "InstallFiles", "DuplicateFiles", wixPatchAction.Action));
474 }
475 }
476 }
477 else
478 {
479 sequence = seqInstallFiles + 1;
480 }
481 }
482 patchAction[0] = wixPatchAction.Action;
483 patchAction[1] = wixPatchAction.Condition;
484 patchAction[2] = sequence;
485 patchAction.Operation = RowOperation.Add;
486 }
487 }
488
489 /// <summary>
490 /// Tests sequence table for PatchFiles and associated actions
491 /// </summary>
492 /// <param name="iesTable">The table to test.</param>
493 /// <param name="hasPatchFilesAction">Set to true if PatchFiles action is found. Left unchanged otherwise.</param>
494 /// <param name="seqInstallFiles">Set to sequence value of InstallFiles action if found. Left unchanged otherwise.</param>
495 /// <param name="seqDuplicateFiles">Set to sequence value of DuplicateFiles action if found. Left unchanged otherwise.</param>
496 private static void TestSequenceTableForPatchFilesAction(Table iesTable, ref bool hasPatchFilesAction, ref int seqInstallFiles, ref int seqDuplicateFiles)
497 {
498 if (null != iesTable)
499 {
500 foreach (Row iesRow in iesTable.Rows)
501 {
502 if (String.Equals("PatchFiles", (string)iesRow[0], StringComparison.Ordinal))
503 {
504 hasPatchFilesAction = true;
505 }
506 if (String.Equals("InstallFiles", (string)iesRow[0], StringComparison.Ordinal))
507 {
508 seqInstallFiles = (int)iesRow.Fields[2].Data;
509 }
510 if (String.Equals("DuplicateFiles", (string)iesRow[0], StringComparison.Ordinal))
511 {
512 seqDuplicateFiles = (int)iesRow.Fields[2].Data;
513 }
514 }
515 }
516 }
517
518 /// <summary>
519 /// Signal a warning if a non-keypath file was changed in a patch without also changing the keypath file of the component.
520 /// </summary>
521 /// <param name="output">The output to validate.</param>
522 private void ValidateFileRowChanges(Output transform)
523 {
524 Table componentTable = transform.Tables["Component"];
525 Table fileTable = transform.Tables["File"];
526
527 // There's no sense validating keypaths if the transform has no component or file table
528 if (componentTable == null || fileTable == null)
529 {
530 return;
531 }
532
533 Dictionary<string, string> componentKeyPath = new Dictionary<string, string>(componentTable.Rows.Count);
534
535 // Index the Component table for non-directory & non-registry key paths.
536 foreach (Row row in componentTable.Rows)
537 {
538 if (null != row.Fields[5].Data &&
539 0 != ((int)row.Fields[3].Data & MsiInterop.MsidbComponentAttributesRegistryKeyPath))
540 {
541 componentKeyPath.Add(row.Fields[0].Data.ToString(), row.Fields[5].Data.ToString());
542 }
543 }
544
545 Dictionary<string, string> componentWithChangedKeyPath = new Dictionary<string, string>();
546 Dictionary<string, string> componentWithNonKeyPathChanged = new Dictionary<string, string>();
547 // Verify changes in the file table, now that file diffing has occurred
548 foreach (FileRow row in fileTable.Rows)
549 {
550 string fileId = row.Fields[0].Data.ToString();
551 string componentId = row.Fields[1].Data.ToString();
552
553 if (RowOperation.Modify != row.Operation)
554 {
555 continue;
556 }
557
558 // If this file is the keypath of a component
559 if (componentKeyPath.ContainsValue(fileId))
560 {
561 if (!componentWithChangedKeyPath.ContainsKey(componentId))
562 {
563 componentWithChangedKeyPath.Add(componentId, fileId);
564 }
565 }
566 else
567 {
568 if (!componentWithNonKeyPathChanged.ContainsKey(componentId))
569 {
570 componentWithNonKeyPathChanged.Add(componentId, fileId);
571 }
572 }
573 }
574
575 foreach (KeyValuePair<string, string> componentFile in componentWithNonKeyPathChanged)
576 {
577 // Make sure all changes to non keypath files also had a change in the keypath.
578 if (!componentWithChangedKeyPath.ContainsKey(componentFile.Key) && componentKeyPath.ContainsKey(componentFile.Key))
579 {
580 Messaging.Instance.OnMessage(WixWarnings.UpdateOfNonKeyPathFile((string)componentFile.Value, (string)componentFile.Key, (string)componentKeyPath[componentFile.Key]));
581 }
582 }
583 }
584
585 private bool CompareFiles(string targetFile, string updatedFile)
586 {
587 bool? compared = null;
588 foreach (var extension in this.Extensions)
589 {
590 compared = extension.CompareFiles(targetFile, updatedFile);
591
592 if (compared.HasValue)
593 {
594 break;
595 }
596 }
597
598 if (!compared.HasValue)
599 {
600 throw new InvalidOperationException(); // TODO: something needs to be said here that none of the binder file managers returned a result.
601 }
602
603 return compared.Value;
604 }
605 }
606}
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/CreateCabinetsCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/CreateCabinetsCommand.cs
new file mode 100644
index 00000000..02015744
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/CreateCabinetsCommand.cs
@@ -0,0 +1,499 @@
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.Globalization;
8 using System.IO;
9 using System.Linq;
10 using System.Runtime.InteropServices;
11 using System.Threading;
12 using WixToolset.Core.Bind;
13 using WixToolset.Core.WindowsInstaller.Bind;
14 using WixToolset.Data;
15 using WixToolset.Data.Bind;
16 using WixToolset.Data.Rows;
17 using WixToolset.Extensibility;
18
19 /// <summary>
20 /// Creates cabinet files.
21 /// </summary>
22 internal class CreateCabinetsCommand
23 {
24 public const int DefaultMaximumUncompressedMediaSize = 200; // Default value is 200 MB
25 public const int MaxValueOfMaxCabSizeForLargeFileSplitting = 2 * 1024; // 2048 MB (i.e. 2 GB)
26
27 private List<FileTransfer> fileTransfers;
28
29 private FileSplitCabNamesCallback newCabNamesCallBack;
30
31 private Dictionary<string, string> lastCabinetAddedToMediaTable; // Key is First Cabinet Name, Value is Last Cabinet Added in the Split Sequence
32
33 public CreateCabinetsCommand()
34 {
35 this.fileTransfers = new List<FileTransfer>();
36
37 this.newCabNamesCallBack = this.NewCabNamesCallBack;
38 }
39
40 /// <summary>
41 /// Sets the number of threads to use for cabinet creation.
42 /// </summary>
43 public int CabbingThreadCount { private get; set; }
44
45 public string CabCachePath { private get; set; }
46
47 public string TempFilesLocation { private get; set; }
48
49 /// <summary>
50 /// Sets the default compression level to use for cabinets
51 /// that don't have their compression level explicitly set.
52 /// </summary>
53 public CompressionLevel DefaultCompressionLevel { private get; set; }
54
55 public IEnumerable<IWindowsInstallerBackendExtension> BackendExtensions { private get; set; }
56
57 public Output Output { private get; set; }
58
59 public string LayoutDirectory { private get; set; }
60
61 public bool Compressed { private get; set; }
62
63 public Dictionary<MediaRow, IEnumerable<FileFacade>> FileRowsByCabinet { private get; set; }
64
65 public Func<MediaRow, string, string, string> ResolveMedia { private get; set; }
66
67 public TableDefinitionCollection TableDefinitions { private get; set; }
68
69 public Table WixMediaTable { private get; set; }
70
71 public IEnumerable<FileTransfer> FileTransfers => this.fileTransfers;
72
73 /// <param name="output">Output to generate image for.</param>
74 /// <param name="fileTransfers">Array of files to be transfered.</param>
75 /// <param name="layoutDirectory">The directory in which the image should be layed out.</param>
76 /// <param name="compressed">Flag if source image should be compressed.</param>
77 /// <returns>The uncompressed file rows.</returns>
78 public void Execute()
79 {
80 RowDictionary<WixMediaRow> wixMediaRows = new RowDictionary<WixMediaRow>(this.WixMediaTable);
81
82 this.lastCabinetAddedToMediaTable = new Dictionary<string, string>();
83
84 this.SetCabbingThreadCount();
85
86 // Send Binder object to Facilitate NewCabNamesCallBack Callback
87 CabinetBuilder cabinetBuilder = new CabinetBuilder(this.CabbingThreadCount, Marshal.GetFunctionPointerForDelegate(this.newCabNamesCallBack));
88
89 // Supply Compile MediaTemplate Attributes to Cabinet Builder
90 int MaximumCabinetSizeForLargeFileSplitting;
91 int MaximumUncompressedMediaSize;
92 this.GetMediaTemplateAttributes(out MaximumCabinetSizeForLargeFileSplitting, out MaximumUncompressedMediaSize);
93 cabinetBuilder.MaximumCabinetSizeForLargeFileSplitting = MaximumCabinetSizeForLargeFileSplitting;
94 cabinetBuilder.MaximumUncompressedMediaSize = MaximumUncompressedMediaSize;
95
96 foreach (var entry in this.FileRowsByCabinet)
97 {
98 MediaRow mediaRow = entry.Key;
99 IEnumerable<FileFacade> files = entry.Value;
100 CompressionLevel compressionLevel = this.DefaultCompressionLevel;
101
102 WixMediaRow wixMediaRow = null;
103 string mediaLayoutFolder = null;
104
105 if (wixMediaRows.TryGetValue(mediaRow.GetKey(), out wixMediaRow))
106 {
107 mediaLayoutFolder = wixMediaRow.Layout;
108
109 if (wixMediaRow.CompressionLevel.HasValue)
110 {
111 compressionLevel = wixMediaRow.CompressionLevel.Value;
112 }
113 }
114
115 string cabinetDir = this.ResolveMedia(mediaRow, mediaLayoutFolder, this.LayoutDirectory);
116
117 CabinetWorkItem cabinetWorkItem = this.CreateCabinetWorkItem(this.Output, cabinetDir, mediaRow, compressionLevel, files, this.fileTransfers);
118 if (null != cabinetWorkItem)
119 {
120 cabinetBuilder.Enqueue(cabinetWorkItem);
121 }
122 }
123
124 // stop processing if an error previously occurred
125 if (Messaging.Instance.EncounteredError)
126 {
127 return;
128 }
129
130 // create queued cabinets with multiple threads
131 cabinetBuilder.CreateQueuedCabinets();
132 if (Messaging.Instance.EncounteredError)
133 {
134 return;
135 }
136 }
137
138 /// <summary>
139 /// Sets the thead count to the number of processors if the current thread count is set to 0.
140 /// </summary>
141 /// <remarks>The thread count value must be greater than 0 otherwise and exception will be thrown.</remarks>
142 private void SetCabbingThreadCount()
143 {
144 // default the number of cabbing threads to the number of processors if it wasn't specified
145 if (0 == this.CabbingThreadCount)
146 {
147 string numberOfProcessors = System.Environment.GetEnvironmentVariable("NUMBER_OF_PROCESSORS");
148
149 try
150 {
151 if (null != numberOfProcessors)
152 {
153 this.CabbingThreadCount = Convert.ToInt32(numberOfProcessors, CultureInfo.InvariantCulture.NumberFormat);
154
155 if (0 >= this.CabbingThreadCount)
156 {
157 throw new WixException(WixErrors.IllegalEnvironmentVariable("NUMBER_OF_PROCESSORS", numberOfProcessors));
158 }
159 }
160 else // default to 1 if the environment variable is not set
161 {
162 this.CabbingThreadCount = 1;
163 }
164
165 Messaging.Instance.OnMessage(WixVerboses.SetCabbingThreadCount(this.CabbingThreadCount.ToString()));
166 }
167 catch (ArgumentException)
168 {
169 throw new WixException(WixErrors.IllegalEnvironmentVariable("NUMBER_OF_PROCESSORS", numberOfProcessors));
170 }
171 catch (FormatException)
172 {
173 throw new WixException(WixErrors.IllegalEnvironmentVariable("NUMBER_OF_PROCESSORS", numberOfProcessors));
174 }
175 }
176 }
177
178
179 /// <summary>
180 /// Creates a work item to create a cabinet.
181 /// </summary>
182 /// <param name="output">Output for the current database.</param>
183 /// <param name="cabinetDir">Directory to create cabinet in.</param>
184 /// <param name="mediaRow">MediaRow containing information about the cabinet.</param>
185 /// <param name="fileFacades">Collection of files in this cabinet.</param>
186 /// <param name="fileTransfers">Array of files to be transfered.</param>
187 /// <returns>created CabinetWorkItem object</returns>
188 private CabinetWorkItem CreateCabinetWorkItem(Output output, string cabinetDir, MediaRow mediaRow, CompressionLevel compressionLevel, IEnumerable<FileFacade> fileFacades, List<FileTransfer> fileTransfers)
189 {
190 CabinetWorkItem cabinetWorkItem = null;
191 string tempCabinetFileX = Path.Combine(this.TempFilesLocation, mediaRow.Cabinet);
192
193 // check for an empty cabinet
194 if (!fileFacades.Any())
195 {
196 string cabinetName = mediaRow.Cabinet;
197
198 // remove the leading '#' from the embedded cabinet name to make the warning easier to understand
199 if (cabinetName.StartsWith("#", StringComparison.Ordinal))
200 {
201 cabinetName = cabinetName.Substring(1);
202 }
203
204 // If building a patch, remind them to run -p for torch.
205 if (OutputType.Patch == output.Type)
206 {
207 Messaging.Instance.OnMessage(WixWarnings.EmptyCabinet(mediaRow.SourceLineNumbers, cabinetName, true));
208 }
209 else
210 {
211 Messaging.Instance.OnMessage(WixWarnings.EmptyCabinet(mediaRow.SourceLineNumbers, cabinetName));
212 }
213 }
214
215 var cabinetResolver = new CabinetResolver(this.CabCachePath, this.BackendExtensions);
216
217 ResolvedCabinet resolvedCabinet = cabinetResolver.ResolveCabinet(tempCabinetFileX, fileFacades);
218
219 // create a cabinet work item if it's not being skipped
220 if (CabinetBuildOption.BuildAndCopy == resolvedCabinet.BuildOption || CabinetBuildOption.BuildAndMove == resolvedCabinet.BuildOption)
221 {
222 int maxThreshold = 0; // default to the threshold for best smartcabbing (makes smallest cabinet).
223
224 cabinetWorkItem = new CabinetWorkItem(fileFacades, resolvedCabinet.Path, maxThreshold, compressionLevel/*, this.FileManager*/);
225 }
226 else // reuse the cabinet from the cabinet cache.
227 {
228 Messaging.Instance.OnMessage(WixVerboses.ReusingCabCache(mediaRow.SourceLineNumbers, mediaRow.Cabinet, resolvedCabinet.Path));
229
230 try
231 {
232 // Ensure the cached cabinet timestamp is current to prevent perpetual incremental builds. The
233 // problematic scenario goes like this. Imagine two cabinets in the cache. Update a file that
234 // goes into one of the cabinets. One cabinet will get rebuilt, the other will be copied from
235 // the cache. Now the file (an input) has a newer timestamp than the reused cabient (an output)
236 // causing the project to look like it perpetually needs a rebuild until all of the reused
237 // cabinets get newer timestamps.
238 File.SetLastWriteTime(resolvedCabinet.Path, DateTime.Now);
239 }
240 catch (Exception e)
241 {
242 Messaging.Instance.OnMessage(WixWarnings.CannotUpdateCabCache(mediaRow.SourceLineNumbers, resolvedCabinet.Path, e.Message));
243 }
244 }
245
246 if (mediaRow.Cabinet.StartsWith("#", StringComparison.Ordinal))
247 {
248 Table streamsTable = output.EnsureTable(this.TableDefinitions["_Streams"]);
249
250 Row streamRow = streamsTable.CreateRow(mediaRow.SourceLineNumbers);
251 streamRow[0] = mediaRow.Cabinet.Substring(1);
252 streamRow[1] = resolvedCabinet.Path;
253 }
254 else
255 {
256 string destinationPath = Path.Combine(cabinetDir, mediaRow.Cabinet);
257 FileTransfer transfer;
258 if (FileTransfer.TryCreate(resolvedCabinet.Path, destinationPath, CabinetBuildOption.BuildAndMove == resolvedCabinet.BuildOption, "Cabinet", mediaRow.SourceLineNumbers, out transfer))
259 {
260 transfer.Built = true;
261 fileTransfers.Add(transfer);
262 }
263 }
264
265 return cabinetWorkItem;
266 }
267
268 //private ResolvedCabinet ResolveCabinet(string cabinetPath, IEnumerable<FileFacade> fileFacades)
269 //{
270 // ResolvedCabinet resolved = null;
271
272 // List<BindFileWithPath> filesWithPath = fileFacades.Select(f => new BindFileWithPath() { Id = f.File.File, Path = f.WixFile.Source }).ToList();
273
274 // foreach (var extension in this.BackendExtensions)
275 // {
276 // resolved = extension.ResolveCabinet(cabinetPath, filesWithPath);
277 // if (null != resolved)
278 // {
279 // break;
280 // }
281 // }
282
283 // return resolved;
284 //}
285
286 /// <summary>
287 /// Delegate for Cabinet Split Callback
288 /// </summary>
289 [UnmanagedFunctionPointer(CallingConvention.StdCall)]
290 internal delegate void FileSplitCabNamesCallback([MarshalAs(UnmanagedType.LPWStr)]string firstCabName, [MarshalAs(UnmanagedType.LPWStr)]string newCabName, [MarshalAs(UnmanagedType.LPWStr)]string fileToken);
291
292 /// <summary>
293 /// Call back to Add File Transfer for new Cab and add new Cab to Media table
294 /// This callback can come from Multiple Cabinet Builder Threads and so should be thread safe
295 /// This callback will not be called in case there is no File splitting. i.e. MaximumCabinetSizeForLargeFileSplitting was not authored
296 /// </summary>
297 /// <param name="firstCabName">The name of splitting cabinet without extention e.g. "cab1".</param>
298 /// <param name="newCabName">The name of the new cabinet that would be formed by splitting e.g. "cab1b.cab"</param>
299 /// <param name="fileToken">The file token of the first file present in the splitting cabinet</param>
300 internal void NewCabNamesCallBack([MarshalAs(UnmanagedType.LPWStr)]string firstCabName, [MarshalAs(UnmanagedType.LPWStr)]string newCabName, [MarshalAs(UnmanagedType.LPWStr)]string fileToken)
301 {
302 // Locking Mutex here as this callback can come from Multiple Cabinet Builder Threads
303 Mutex mutex = new Mutex(false, "WixCabinetSplitBinderCallback");
304 try
305 {
306 if (!mutex.WaitOne(0, false)) // Check if you can get the lock
307 {
308 // Cound not get the Lock
309 Messaging.Instance.OnMessage(WixVerboses.CabinetsSplitInParallel());
310 mutex.WaitOne(); // Wait on other thread
311 }
312
313 string firstCabinetName = firstCabName + ".cab";
314 string newCabinetName = newCabName;
315 bool transferAdded = false; // Used for Error Handling
316
317 // Create File Transfer for new Cabinet using transfer of Base Cabinet
318 foreach (FileTransfer transfer in this.FileTransfers)
319 {
320 if (firstCabinetName.Equals(Path.GetFileName(transfer.Source), StringComparison.InvariantCultureIgnoreCase))
321 {
322 string newCabSourcePath = Path.Combine(Path.GetDirectoryName(transfer.Source), newCabinetName);
323 string newCabTargetPath = Path.Combine(Path.GetDirectoryName(transfer.Destination), newCabinetName);
324
325 FileTransfer newTransfer;
326 if (FileTransfer.TryCreate(newCabSourcePath, newCabTargetPath, transfer.Move, "Cabinet", transfer.SourceLineNumbers, out newTransfer))
327 {
328 newTransfer.Built = true;
329 this.fileTransfers.Add(newTransfer);
330 transferAdded = true;
331 break;
332 }
333 }
334 }
335
336 // Check if File Transfer was added
337 if (!transferAdded)
338 {
339 throw new WixException(WixErrors.SplitCabinetCopyRegistrationFailed(newCabinetName, firstCabinetName));
340 }
341
342 // Add the new Cabinets to media table using LastSequence of Base Cabinet
343 Table mediaTable = this.Output.Tables["Media"];
344 Table wixFileTable = this.Output.Tables["WixFile"];
345 int diskIDForLastSplitCabAdded = 0; // The DiskID value for the first cab in this cabinet split chain
346 int lastSequenceForLastSplitCabAdded = 0; // The LastSequence value for the first cab in this cabinet split chain
347 bool lastSplitCabinetFound = false; // Used for Error Handling
348
349 string lastCabinetOfThisSequence = String.Empty;
350 // Get the Value of Last Cabinet Added in this split Sequence from Dictionary
351 if (!this.lastCabinetAddedToMediaTable.TryGetValue(firstCabinetName, out lastCabinetOfThisSequence))
352 {
353 // If there is no value for this sequence, then use first Cabinet is the last one of this split sequence
354 lastCabinetOfThisSequence = firstCabinetName;
355 }
356
357 foreach (MediaRow mediaRow in mediaTable.Rows)
358 {
359 // Get details for the Last Cabinet Added in this Split Sequence
360 if ((lastSequenceForLastSplitCabAdded == 0) && lastCabinetOfThisSequence.Equals(mediaRow.Cabinet, StringComparison.InvariantCultureIgnoreCase))
361 {
362 lastSequenceForLastSplitCabAdded = mediaRow.LastSequence;
363 diskIDForLastSplitCabAdded = mediaRow.DiskId;
364 lastSplitCabinetFound = true;
365 }
366
367 // Check for Name Collision for the new Cabinet added
368 if (newCabinetName.Equals(mediaRow.Cabinet, StringComparison.InvariantCultureIgnoreCase))
369 {
370 // Name Collision of generated Split Cabinet Name and user Specified Cab name for current row
371 throw new WixException(WixErrors.SplitCabinetNameCollision(newCabinetName, firstCabinetName));
372 }
373 }
374
375 // Check if the last Split Cabinet was found in the Media Table
376 if (!lastSplitCabinetFound)
377 {
378 throw new WixException(WixErrors.SplitCabinetInsertionFailed(newCabinetName, firstCabinetName, lastCabinetOfThisSequence));
379 }
380
381 // The new Row has to be inserted just after the last cab in this cabinet split chain according to DiskID Sort
382 // This is because the FDI Extract requires DiskID of Split Cabinets to be continuous. It Fails otherwise with
383 // Error 2350 (FDI Server Error) as next DiskID did not have the right split cabinet during extraction
384 MediaRow newMediaRow = (MediaRow)mediaTable.CreateRow(null);
385 newMediaRow.Cabinet = newCabinetName;
386 newMediaRow.DiskId = diskIDForLastSplitCabAdded + 1; // When Sorted with DiskID, this new Cabinet Row is an Insertion
387 newMediaRow.LastSequence = lastSequenceForLastSplitCabAdded;
388
389 // Now increment the DiskID for all rows that come after the newly inserted row to Ensure that DiskId is unique
390 foreach (MediaRow mediaRow in mediaTable.Rows)
391 {
392 // Check if this row comes after inserted row and it is not the new cabinet inserted row
393 if (mediaRow.DiskId >= newMediaRow.DiskId && !newCabinetName.Equals(mediaRow.Cabinet, StringComparison.InvariantCultureIgnoreCase))
394 {
395 mediaRow.DiskId++; // Increment DiskID
396 }
397 }
398
399 // Now Increment DiskID for All files Rows so that they refer to the right Media Row
400 foreach (WixFileRow wixFileRow in wixFileTable.Rows)
401 {
402 // Check if this row comes after inserted row and if this row is not the file that has to go into the current cabinet
403 // This check will work as we have only one large file in every splitting cabinet
404 // If we want to support splitting cabinet with more large files we need to update this code
405 if (wixFileRow.DiskId >= newMediaRow.DiskId && !wixFileRow.File.Equals(fileToken, StringComparison.InvariantCultureIgnoreCase))
406 {
407 wixFileRow.DiskId++; // Increment DiskID
408 }
409 }
410
411 // Update the Last Cabinet Added in the Split Sequence in Dictionary for future callback
412 this.lastCabinetAddedToMediaTable[firstCabinetName] = newCabinetName;
413
414 mediaTable.ValidateRows(); // Valdiates DiskDIs, throws Exception as Wix Error if validation fails
415 }
416 finally
417 {
418 // Releasing the Mutex here
419 mutex.ReleaseMutex();
420 }
421 }
422
423
424 /// <summary>
425 /// Gets Compiler Values of MediaTemplate Attributes governing Maximum Cabinet Size after applying Environment Variable Overrides
426 /// </summary>
427 /// <param name="output">Output to generate image for.</param>
428 /// <param name="fileRows">The indexed file rows.</param>
429 private void GetMediaTemplateAttributes(out int maxCabSizeForLargeFileSplitting, out int maxUncompressedMediaSize)
430 {
431 // Get Environment Variable Overrides for MediaTemplate Attributes governing Maximum Cabinet Size
432 string mcslfsString = Environment.GetEnvironmentVariable("WIX_MCSLFS");
433 string mumsString = Environment.GetEnvironmentVariable("WIX_MUMS");
434 int maxCabSizeForLargeFileInMB = 0;
435 int maxPreCompressedSizeInMB = 0;
436 ulong testOverFlow = 0;
437
438 // Supply Compile MediaTemplate Attributes to Cabinet Builder
439 Table mediaTemplateTable = this.Output.Tables["WixMediaTemplate"];
440 if (mediaTemplateTable != null)
441 {
442 WixMediaTemplateRow mediaTemplateRow = (WixMediaTemplateRow)mediaTemplateTable.Rows[0];
443
444 // Get the Value for Max Cab Size for File Splitting
445 try
446 {
447 // Override authored mcslfs value if environment variable is authored.
448 if (!String.IsNullOrEmpty(mcslfsString))
449 {
450 maxCabSizeForLargeFileInMB = Int32.Parse(mcslfsString);
451 }
452 else
453 {
454 maxCabSizeForLargeFileInMB = mediaTemplateRow.MaximumCabinetSizeForLargeFileSplitting;
455 }
456 testOverFlow = (ulong)maxCabSizeForLargeFileInMB * 1024 * 1024;
457 }
458 catch (FormatException)
459 {
460 throw new WixException(WixErrors.IllegalEnvironmentVariable("WIX_MCSLFS", mcslfsString));
461 }
462 catch (OverflowException)
463 {
464 throw new WixException(WixErrors.MaximumCabinetSizeForLargeFileSplittingTooLarge(null, maxCabSizeForLargeFileInMB, MaxValueOfMaxCabSizeForLargeFileSplitting));
465 }
466
467 try
468 {
469 // Override authored mums value if environment variable is authored.
470 if (!String.IsNullOrEmpty(mumsString))
471 {
472 maxPreCompressedSizeInMB = Int32.Parse(mumsString);
473 }
474 else
475 {
476 maxPreCompressedSizeInMB = mediaTemplateRow.MaximumUncompressedMediaSize;
477 }
478 testOverFlow = (ulong)maxPreCompressedSizeInMB * 1024 * 1024;
479 }
480 catch (FormatException)
481 {
482 throw new WixException(WixErrors.IllegalEnvironmentVariable("WIX_MUMS", mumsString));
483 }
484 catch (OverflowException)
485 {
486 throw new WixException(WixErrors.MaximumUncompressedMediaSizeTooLarge(null, maxPreCompressedSizeInMB));
487 }
488
489 maxCabSizeForLargeFileSplitting = maxCabSizeForLargeFileInMB;
490 maxUncompressedMediaSize = maxPreCompressedSizeInMB;
491 }
492 else
493 {
494 maxCabSizeForLargeFileSplitting = 0;
495 maxUncompressedMediaSize = DefaultMaximumUncompressedMediaSize;
496 }
497 }
498 }
499}
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/CreateDeltaPatchesCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/CreateDeltaPatchesCommand.cs
new file mode 100644
index 00000000..767671b8
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/CreateDeltaPatchesCommand.cs
@@ -0,0 +1,87 @@
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.Globalization;
8 using System.IO;
9 using WixToolset.Core.Bind;
10 using WixToolset.Data;
11 using WixToolset.Data.Rows;
12
13 /// <summary>
14 /// Creates delta patches and updates the appropriate rows to point to the newly generated patches.
15 /// </summary>
16 internal class CreateDeltaPatchesCommand
17 {
18 public IEnumerable<FileFacade> FileFacades { private get; set; }
19
20 public Table WixPatchIdTable { private get; set; }
21
22 public string TempFilesLocation { private get; set; }
23
24 public void Execute()
25 {
26 bool optimizePatchSizeForLargeFiles = false;
27 PatchSymbolFlagsType apiPatchingSymbolFlags = 0;
28
29 if (null != this.WixPatchIdTable)
30 {
31 Row row = this.WixPatchIdTable.Rows[0];
32 if (null != row)
33 {
34 if (null != row[2])
35 {
36 optimizePatchSizeForLargeFiles = (1 == Convert.ToUInt32(row[2], CultureInfo.InvariantCulture));
37 }
38
39 if (null != row[3])
40 {
41 apiPatchingSymbolFlags = (PatchSymbolFlagsType)Convert.ToUInt32(row[3], CultureInfo.InvariantCulture);
42 }
43 }
44 }
45
46 foreach (FileFacade facade in this.FileFacades)
47 {
48 if (RowOperation.Modify == facade.File.Operation &&
49 0 != (facade.WixFile.PatchAttributes & PatchAttributeType.IncludeWholeFile))
50 {
51 string deltaBase = String.Concat("delta_", facade.File.File);
52 string deltaFile = Path.Combine(this.TempFilesLocation, String.Concat(deltaBase, ".dpf"));
53 string headerFile = Path.Combine(this.TempFilesLocation, String.Concat(deltaBase, ".phd"));
54
55 bool retainRangeWarning = false;
56
57 if (PatchAPI.PatchInterop.CreateDelta(
58 deltaFile,
59 facade.WixFile.Source,
60 facade.DeltaPatchFile.Symbols,
61 facade.DeltaPatchFile.RetainOffsets,
62 new[] { facade.WixFile.PreviousSource },
63 facade.DeltaPatchFile.PreviousSymbols.Split(new[] { ';' }),
64 facade.DeltaPatchFile.PreviousIgnoreLengths.Split(new[] { ';' }),
65 facade.DeltaPatchFile.PreviousIgnoreOffsets.Split(new[] { ';' }),
66 facade.DeltaPatchFile.PreviousRetainLengths.Split(new[] { ';' }),
67 facade.DeltaPatchFile.PreviousRetainOffsets.Split(new[] { ';' }),
68 apiPatchingSymbolFlags,
69 optimizePatchSizeForLargeFiles,
70 out retainRangeWarning))
71 {
72 PatchAPI.PatchInterop.ExtractDeltaHeader(deltaFile, headerFile);
73
74 facade.WixFile.Source = deltaFile;
75 facade.WixFile.DeltaPatchHeaderSource = headerFile;
76 }
77
78 if (retainRangeWarning)
79 {
80 // TODO: get patch family to add to warning message for PatchWiz parity.
81 Messaging.Instance.OnMessage(WixWarnings.RetainRangeMismatch(facade.File.SourceLineNumbers, facade.File.File));
82 }
83 }
84 }
85 }
86 }
87}
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/CreateSpecialPropertiesCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/CreateSpecialPropertiesCommand.cs
new file mode 100644
index 00000000..aef130b0
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/CreateSpecialPropertiesCommand.cs
@@ -0,0 +1,68 @@
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 WixToolset.Data;
8 using WixToolset.Data.Rows;
9
10 internal class CreateSpecialPropertiesCommand
11 {
12 public Table PropertyTable { private get; set; }
13
14 public Table WixPropertyTable { private get; set; }
15
16 public void Execute()
17 {
18 // Create the special properties.
19 if (null != this.WixPropertyTable)
20 {
21 // Create lists of the properties that contribute to the special lists of properties.
22 SortedSet<string> adminProperties = new SortedSet<string>();
23 SortedSet<string> secureProperties = new SortedSet<string>();
24 SortedSet<string> hiddenProperties = new SortedSet<string>();
25
26 foreach (WixPropertyRow wixPropertyRow in this.WixPropertyTable.Rows)
27 {
28 if (wixPropertyRow.Admin)
29 {
30 adminProperties.Add(wixPropertyRow.Id);
31 }
32
33 if (wixPropertyRow.Hidden)
34 {
35 hiddenProperties.Add(wixPropertyRow.Id);
36 }
37
38 if (wixPropertyRow.Secure)
39 {
40 secureProperties.Add(wixPropertyRow.Id);
41 }
42 }
43
44 Table propertyTable = this.PropertyTable;
45 if (0 < adminProperties.Count)
46 {
47 PropertyRow row = (PropertyRow)propertyTable.CreateRow(null);
48 row.Property = "AdminProperties";
49 row.Value = String.Join(";", adminProperties);
50 }
51
52 if (0 < secureProperties.Count)
53 {
54 PropertyRow row = (PropertyRow)propertyTable.CreateRow(null);
55 row.Property = "SecureCustomProperties";
56 row.Value = String.Join(";", secureProperties);
57 }
58
59 if (0 < hiddenProperties.Count)
60 {
61 PropertyRow row = (PropertyRow)propertyTable.CreateRow(null);
62 row.Property = "MsiHiddenProperties";
63 row.Value = String.Join(";", hiddenProperties);
64 }
65 }
66 }
67 }
68}
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/ExtractMergeModuleFilesCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/ExtractMergeModuleFilesCommand.cs
new file mode 100644
index 00000000..ae76037d
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/ExtractMergeModuleFilesCommand.cs
@@ -0,0 +1,226 @@
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.ComponentModel;
8 using System.Globalization;
9 using System.IO;
10 using System.Linq;
11 using System.Runtime.InteropServices;
12 using WixToolset.Data;
13 using WixToolset.Data.Rows;
14 using WixToolset.MergeMod;
15 using WixToolset.Msi;
16 using WixToolset.Core.Native;
17 using WixToolset.Core.Bind;
18 using WixToolset.Core.Cab;
19
20 /// <summary>
21 /// Retrieve files information and extract them from merge modules.
22 /// </summary>
23 internal class ExtractMergeModuleFilesCommand
24 {
25 public IEnumerable<FileFacade> FileFacades { private get; set; }
26
27 public Table FileTable { private get; set; }
28
29 public Table WixFileTable { private get; set; }
30
31 public Table WixMergeTable { private get; set; }
32
33 public int OutputInstallerVersion { private get; set; }
34
35 public bool SuppressLayout { private get; set; }
36
37 public string TempFilesLocation { private get; set; }
38
39 public IEnumerable<FileFacade> MergeModulesFileFacades { get; private set; }
40
41 public void Execute()
42 {
43 List<FileFacade> mergeModulesFileFacades = new List<FileFacade>();
44
45 IMsmMerge2 merge = MsmInterop.GetMsmMerge();
46
47 // Index all of the file rows to be able to detect collisions with files in the Merge Modules.
48 // It may seem a bit expensive to build up this index solely for the purpose of checking collisions
49 // and you may be thinking, "Surely, we must need the file rows indexed elsewhere." It turns out
50 // there are other cases where we need all the file rows indexed, however they are not common cases.
51 // Now since Merge Modules are already slow and generally less desirable than .wixlibs we'll let
52 // this case be slightly more expensive because the cost of maintaining an indexed file row collection
53 // is a lot more costly for the common cases.
54 Dictionary<string, FileFacade> indexedFileFacades = this.FileFacades.ToDictionary(f => f.File.File, StringComparer.Ordinal);
55
56 foreach (WixMergeRow wixMergeRow in this.WixMergeTable.Rows)
57 {
58 bool containsFiles = this.CreateFacadesForMergeModuleFiles(wixMergeRow, mergeModulesFileFacades, indexedFileFacades);
59
60 // If the module has files and creating layout
61 if (containsFiles && !this.SuppressLayout)
62 {
63 this.ExtractFilesFromMergeModule(merge, wixMergeRow);
64 }
65 }
66
67 this.MergeModulesFileFacades = mergeModulesFileFacades;
68 }
69
70 private bool CreateFacadesForMergeModuleFiles(WixMergeRow wixMergeRow, List<FileFacade> mergeModulesFileFacades, Dictionary<string, FileFacade> indexedFileFacades)
71 {
72 bool containsFiles = false;
73
74 try
75 {
76 // read the module's File table to get its FileMediaInformation entries and gather any other information needed from the module.
77 using (Database db = new Database(wixMergeRow.SourceFile, OpenDatabase.ReadOnly))
78 {
79 if (db.TableExists("File") && db.TableExists("Component"))
80 {
81 Dictionary<string, FileFacade> uniqueModuleFileIdentifiers = new Dictionary<string, FileFacade>(StringComparer.OrdinalIgnoreCase);
82
83 using (View view = db.OpenExecuteView("SELECT `File`, `Directory_` FROM `File`, `Component` WHERE `Component_`=`Component`"))
84 {
85 // add each file row from the merge module into the file row collection (check for errors along the way)
86 while (true)
87 {
88 using (Record record = view.Fetch())
89 {
90 if (null == record)
91 {
92 break;
93 }
94
95 // NOTE: this is very tricky - the merge module file rows are not added to the
96 // file table because they should not be created via idt import. Instead, these
97 // rows are created by merging in the actual modules.
98 FileRow fileRow = (FileRow)this.FileTable.CreateRow(wixMergeRow.SourceLineNumbers, false);
99 fileRow.File = record[1];
100 fileRow.Compressed = wixMergeRow.FileCompression;
101
102 WixFileRow wixFileRow = (WixFileRow)this.WixFileTable.CreateRow(wixMergeRow.SourceLineNumbers, false);
103 wixFileRow.Directory = record[2];
104 wixFileRow.DiskId = wixMergeRow.DiskId;
105 wixFileRow.PatchGroup = -1;
106 wixFileRow.Source = String.Concat(this.TempFilesLocation, Path.DirectorySeparatorChar, "MergeId.", wixMergeRow.Number.ToString(CultureInfo.InvariantCulture), Path.DirectorySeparatorChar, record[1]);
107
108 FileFacade mergeModuleFileFacade = new FileFacade(true, fileRow, wixFileRow);
109
110 FileFacade collidingFacade;
111
112 // If case-sensitive collision with another merge module or a user-authored file identifier.
113 if (indexedFileFacades.TryGetValue(mergeModuleFileFacade.File.File, out collidingFacade))
114 {
115 Messaging.Instance.OnMessage(WixErrors.DuplicateModuleFileIdentifier(wixMergeRow.SourceLineNumbers, wixMergeRow.Id, collidingFacade.File.File));
116 }
117 else if (uniqueModuleFileIdentifiers.TryGetValue(mergeModuleFileFacade.File.File, out collidingFacade)) // case-insensitive collision with another file identifier in the same merge module
118 {
119 Messaging.Instance.OnMessage(WixErrors.DuplicateModuleCaseInsensitiveFileIdentifier(wixMergeRow.SourceLineNumbers, wixMergeRow.Id, mergeModuleFileFacade.File.File, collidingFacade.File.File));
120 }
121 else // no collision
122 {
123 mergeModulesFileFacades.Add(mergeModuleFileFacade);
124
125 // Keep updating the indexes as new rows are added.
126 indexedFileFacades.Add(mergeModuleFileFacade.File.File, mergeModuleFileFacade);
127 uniqueModuleFileIdentifiers.Add(mergeModuleFileFacade.File.File, mergeModuleFileFacade);
128 }
129
130 containsFiles = true;
131 }
132 }
133 }
134 }
135
136 // Get the summary information to detect the Schema
137 using (SummaryInformation summaryInformation = new SummaryInformation(db))
138 {
139 string moduleInstallerVersionString = summaryInformation.GetProperty(14);
140
141 try
142 {
143 int moduleInstallerVersion = Convert.ToInt32(moduleInstallerVersionString, CultureInfo.InvariantCulture);
144 if (moduleInstallerVersion > this.OutputInstallerVersion)
145 {
146 Messaging.Instance.OnMessage(WixWarnings.InvalidHigherInstallerVersionInModule(wixMergeRow.SourceLineNumbers, wixMergeRow.Id, moduleInstallerVersion, this.OutputInstallerVersion));
147 }
148 }
149 catch (FormatException)
150 {
151 throw new WixException(WixErrors.MissingOrInvalidModuleInstallerVersion(wixMergeRow.SourceLineNumbers, wixMergeRow.Id, wixMergeRow.SourceFile, moduleInstallerVersionString));
152 }
153 }
154 }
155 }
156 catch (FileNotFoundException)
157 {
158 throw new WixException(WixErrors.FileNotFound(wixMergeRow.SourceLineNumbers, wixMergeRow.SourceFile));
159 }
160 catch (Win32Exception)
161 {
162 throw new WixException(WixErrors.CannotOpenMergeModule(wixMergeRow.SourceLineNumbers, wixMergeRow.Id, wixMergeRow.SourceFile));
163 }
164
165 return containsFiles;
166 }
167
168 private void ExtractFilesFromMergeModule(IMsmMerge2 merge, WixMergeRow wixMergeRow)
169 {
170 bool moduleOpen = false;
171 short mergeLanguage;
172
173 try
174 {
175 mergeLanguage = Convert.ToInt16(wixMergeRow.Language, CultureInfo.InvariantCulture);
176 }
177 catch (System.FormatException)
178 {
179 Messaging.Instance.OnMessage(WixErrors.InvalidMergeLanguage(wixMergeRow.SourceLineNumbers, wixMergeRow.Id, wixMergeRow.Language));
180 return;
181 }
182
183 try
184 {
185 merge.OpenModule(wixMergeRow.SourceFile, mergeLanguage);
186 moduleOpen = true;
187
188 string safeMergeId = wixMergeRow.Number.ToString(CultureInfo.InvariantCulture.NumberFormat);
189
190 // extract the module cabinet, then explode all of the files to a temp directory
191 string moduleCabPath = String.Concat(this.TempFilesLocation, Path.DirectorySeparatorChar, safeMergeId, ".module.cab");
192 merge.ExtractCAB(moduleCabPath);
193
194 string mergeIdPath = String.Concat(this.TempFilesLocation, Path.DirectorySeparatorChar, "MergeId.", safeMergeId);
195 Directory.CreateDirectory(mergeIdPath);
196
197 using (var extractCab = new WixExtractCab())
198 {
199 try
200 {
201 extractCab.Extract(moduleCabPath, mergeIdPath);
202 }
203 catch (FileNotFoundException)
204 {
205 throw new WixException(WixErrors.CabFileDoesNotExist(moduleCabPath, wixMergeRow.SourceFile, mergeIdPath));
206 }
207 catch
208 {
209 throw new WixException(WixErrors.CabExtractionFailed(moduleCabPath, wixMergeRow.SourceFile, mergeIdPath));
210 }
211 }
212 }
213 catch (COMException ce)
214 {
215 throw new WixException(WixErrors.UnableToOpenModule(wixMergeRow.SourceLineNumbers, wixMergeRow.SourceFile, ce.Message));
216 }
217 finally
218 {
219 if (moduleOpen)
220 {
221 merge.CloseModule();
222 }
223 }
224 }
225 }
226}
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/GenerateDatabaseCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/GenerateDatabaseCommand.cs
new file mode 100644
index 00000000..26d254f2
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/GenerateDatabaseCommand.cs
@@ -0,0 +1,332 @@
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.ComponentModel;
8 using System.Globalization;
9 using System.IO;
10 using System.Text;
11 using WixToolset.Data;
12 using WixToolset.Extensibility;
13 using WixToolset.Msi;
14 using WixToolset.Core.Native;
15
16 internal class GenerateDatabaseCommand
17 {
18 public int Codepage { private get; set; }
19
20 public IEnumerable<IBinderExtension> Extensions { private get; set; }
21
22 /// <summary>
23 /// Whether to keep columns added in a transform.
24 /// </summary>
25 public bool KeepAddedColumns { private get; set; }
26
27 public Output Output { private get; set; }
28
29 public string OutputPath { private get; set; }
30
31 public TableDefinitionCollection TableDefinitions { private get; set; }
32
33 public string TempFilesLocation { private get; set; }
34
35 /// <summary>
36 /// Whether to use a subdirectory based on the <paramref name="databaseFile"/> file name for intermediate files.
37 /// </summary>
38 public bool SuppressAddingValidationRows { private get; set; }
39
40 public bool UseSubDirectory { private get; set; }
41
42 public void Execute()
43 {
44 // Add the _Validation rows.
45 if (!this.SuppressAddingValidationRows)
46 {
47 Table validationTable = this.Output.EnsureTable(this.TableDefinitions["_Validation"]);
48
49 foreach (Table table in this.Output.Tables)
50 {
51 if (!table.Definition.Unreal)
52 {
53 // Add the validation rows for this table.
54 table.Definition.AddValidationRows(validationTable);
55 }
56 }
57 }
58
59 // Set the base directory.
60 string baseDirectory = this.TempFilesLocation;
61
62 if (this.UseSubDirectory)
63 {
64 string filename = Path.GetFileNameWithoutExtension(this.OutputPath);
65 baseDirectory = Path.Combine(baseDirectory, filename);
66
67 // make sure the directory exists
68 Directory.CreateDirectory(baseDirectory);
69 }
70
71 try
72 {
73 OpenDatabase type = OpenDatabase.CreateDirect;
74
75 // set special flag for patch files
76 if (OutputType.Patch == this.Output.Type)
77 {
78 type |= OpenDatabase.OpenPatchFile;
79 }
80
81#if DEBUG
82 Console.WriteLine("Opening database at: {0}", this.OutputPath);
83#endif
84
85 using (Database db = new Database(this.OutputPath, type))
86 {
87 // Localize the codepage if a value was specified directly.
88 if (-1 != this.Codepage)
89 {
90 this.Output.Codepage = this.Codepage;
91 }
92
93 // if we're not using the default codepage, import a new one into our
94 // database before we add any tables (or the tables would be added
95 // with the wrong codepage).
96 if (0 != this.Output.Codepage)
97 {
98 this.SetDatabaseCodepage(db, this.Output.Codepage);
99 }
100
101 foreach (Table table in this.Output.Tables)
102 {
103 Table importTable = table;
104 bool hasBinaryColumn = false;
105
106 // Skip all unreal tables other than _Streams.
107 if (table.Definition.Unreal && "_Streams" != table.Name)
108 {
109 continue;
110 }
111
112 // Do not put the _Validation table in patches, it is not needed.
113 if (OutputType.Patch == this.Output.Type && "_Validation" == table.Name)
114 {
115 continue;
116 }
117
118 // The only way to import binary data is to copy it to a local subdirectory first.
119 // To avoid this extra copying and perf hit, import an empty table with the same
120 // definition and later import the binary data from source using records.
121 foreach (ColumnDefinition columnDefinition in table.Definition.Columns)
122 {
123 if (ColumnType.Object == columnDefinition.Type)
124 {
125 importTable = new Table(table.Section, table.Definition);
126 hasBinaryColumn = true;
127 break;
128 }
129 }
130
131 // Create the table via IDT import.
132 if ("_Streams" != importTable.Name)
133 {
134 try
135 {
136 db.ImportTable(this.Output.Codepage, importTable, baseDirectory, this.KeepAddedColumns);
137 }
138 catch (WixInvalidIdtException)
139 {
140 // If ValidateRows finds anything it doesn't like, it throws
141 importTable.ValidateRows();
142
143 // Otherwise we rethrow the InvalidIdt
144 throw;
145 }
146 }
147
148 // insert the rows via SQL query if this table contains object fields
149 if (hasBinaryColumn)
150 {
151 StringBuilder query = new StringBuilder("SELECT ");
152
153 // Build the query for the view.
154 bool firstColumn = true;
155 foreach (ColumnDefinition columnDefinition in table.Definition.Columns)
156 {
157 if (!firstColumn)
158 {
159 query.Append(",");
160 }
161
162 query.AppendFormat(" `{0}`", columnDefinition.Name);
163 firstColumn = false;
164 }
165 query.AppendFormat(" FROM `{0}`", table.Name);
166
167 using (View tableView = db.OpenExecuteView(query.ToString()))
168 {
169 // Import each row containing a stream
170 foreach (Row row in table.Rows)
171 {
172 using (Record record = new Record(table.Definition.Columns.Count))
173 {
174 StringBuilder streamName = new StringBuilder();
175 bool needStream = false;
176
177 // the _Streams table doesn't prepend the table name (or a period)
178 if ("_Streams" != table.Name)
179 {
180 streamName.Append(table.Name);
181 }
182
183 for (int i = 0; i < table.Definition.Columns.Count; i++)
184 {
185 ColumnDefinition columnDefinition = table.Definition.Columns[i];
186
187 switch (columnDefinition.Type)
188 {
189 case ColumnType.Localized:
190 case ColumnType.Preserved:
191 case ColumnType.String:
192 if (columnDefinition.PrimaryKey)
193 {
194 if (0 < streamName.Length)
195 {
196 streamName.Append(".");
197 }
198 streamName.Append((string)row[i]);
199 }
200
201 record.SetString(i + 1, (string)row[i]);
202 break;
203 case ColumnType.Number:
204 record.SetInteger(i + 1, Convert.ToInt32(row[i], CultureInfo.InvariantCulture));
205 break;
206 case ColumnType.Object:
207 if (null != row[i])
208 {
209 needStream = true;
210 try
211 {
212 record.SetStream(i + 1, (string)row[i]);
213 }
214 catch (Win32Exception e)
215 {
216 if (0xA1 == e.NativeErrorCode) // ERROR_BAD_PATHNAME
217 {
218 throw new WixException(WixErrors.FileNotFound(row.SourceLineNumbers, (string)row[i]));
219 }
220 else
221 {
222 throw new WixException(WixErrors.Win32Exception(e.NativeErrorCode, e.Message));
223 }
224 }
225 }
226 break;
227 }
228 }
229
230 // stream names are created by concatenating the name of the table with the values
231 // of the primary key (delimited by periods)
232 // check for a stream name that is more than 62 characters long (the maximum allowed length)
233 if (needStream && MsiInterop.MsiMaxStreamNameLength < streamName.Length)
234 {
235 Messaging.Instance.OnMessage(WixErrors.StreamNameTooLong(row.SourceLineNumbers, table.Name, streamName.ToString(), streamName.Length));
236 }
237 else // add the row to the database
238 {
239 tableView.Modify(ModifyView.Assign, record);
240 }
241 }
242 }
243 }
244
245 // Remove rows from the _Streams table for wixpdbs.
246 if ("_Streams" == table.Name)
247 {
248 table.Rows.Clear();
249 }
250 }
251 }
252
253 // Insert substorages (usually transforms inside a patch or instance transforms in a package).
254 if (0 < this.Output.SubStorages.Count)
255 {
256 using (View storagesView = new View(db, "SELECT `Name`, `Data` FROM `_Storages`"))
257 {
258 foreach (SubStorage subStorage in this.Output.SubStorages)
259 {
260 string transformFile = Path.Combine(this.TempFilesLocation, String.Concat(subStorage.Name, ".mst"));
261
262 // Bind the transform.
263 this.BindTransform(subStorage.Data, transformFile);
264
265 if (Messaging.Instance.EncounteredError)
266 {
267 continue;
268 }
269
270 // add the storage
271 using (Record record = new Record(2))
272 {
273 record.SetString(1, subStorage.Name);
274 record.SetStream(2, transformFile);
275 storagesView.Modify(ModifyView.Assign, record);
276 }
277 }
278 }
279 }
280
281 // We're good, commit the changes to the new database.
282 db.Commit();
283 }
284 }
285 catch (IOException)
286 {
287 // TODO: this error message doesn't seem specific enough
288 throw new WixFileNotFoundException(new SourceLineNumber(this.OutputPath), this.OutputPath);
289 }
290 }
291
292 private void BindTransform(Output transform, string outputPath)
293 {
294 BindTransformCommand command = new BindTransformCommand();
295 command.Extensions = this.Extensions;
296 command.TempFilesLocation = this.TempFilesLocation;
297 command.Transform = transform;
298 command.OutputPath = outputPath;
299 command.TableDefinitions = this.TableDefinitions;
300 command.Execute();
301 }
302
303 /// <summary>
304 /// Sets the codepage of a database.
305 /// </summary>
306 /// <param name="db">Database to set codepage into.</param>
307 /// <param name="output">Output with the codepage for the database.</param>
308 private void SetDatabaseCodepage(Database db, int codepage)
309 {
310 // write out the _ForceCodepage IDT file
311 string idtPath = Path.Combine(this.TempFilesLocation, "_ForceCodepage.idt");
312 using (StreamWriter idtFile = new StreamWriter(idtPath, false, Encoding.ASCII))
313 {
314 idtFile.WriteLine(); // dummy column name record
315 idtFile.WriteLine(); // dummy column definition record
316 idtFile.Write(codepage);
317 idtFile.WriteLine("\t_ForceCodepage");
318 }
319
320 // try to import the table into the MSI
321 try
322 {
323 db.Import(idtPath);
324 }
325 catch (WixInvalidIdtException)
326 {
327 // the IDT should be valid, so an invalid code page was given
328 throw new WixException(WixErrors.IllegalCodepage(codepage));
329 }
330 }
331 }
332}
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/GetFileFacadesCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/GetFileFacadesCommand.cs
new file mode 100644
index 00000000..caf8b7a7
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/GetFileFacadesCommand.cs
@@ -0,0 +1,149 @@
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.Globalization;
8 using System.Linq;
9 using WixToolset.Core.Bind;
10 using WixToolset.Data;
11 using WixToolset.Data.Rows;
12
13 internal class GetFileFacadesCommand
14 {
15 public Table FileTable { private get; set; }
16
17 public Table WixFileTable { private get; set; }
18
19 public Table WixDeltaPatchFileTable { private get; set; }
20
21 public Table WixDeltaPatchSymbolPathsTable { private get; set; }
22
23 public List<FileFacade> FileFacades { get; private set; }
24
25 public void Execute()
26 {
27 List<FileFacade> facades = new List<FileFacade>(this.FileTable.Rows.Count);
28
29 RowDictionary<WixFileRow> wixFiles = new RowDictionary<WixFileRow>(this.WixFileTable);
30 RowDictionary<WixDeltaPatchFileRow> deltaPatchFiles = new RowDictionary<WixDeltaPatchFileRow>(this.WixDeltaPatchFileTable);
31
32 foreach (FileRow file in this.FileTable.Rows)
33 {
34 WixDeltaPatchFileRow deltaPatchFile = null;
35
36 deltaPatchFiles.TryGetValue(file.File, out deltaPatchFile);
37
38 facades.Add(new FileFacade(file, wixFiles[file.File], deltaPatchFile));
39 }
40
41 if (null != this.WixDeltaPatchSymbolPathsTable)
42 {
43 this.ResolveDeltaPatchSymbolPaths(deltaPatchFiles, facades);
44 }
45
46 this.FileFacades = facades;
47 }
48
49 /// <summary>
50 /// Merge data from the WixPatchSymbolPaths rows into the WixDeltaPatchFile rows.
51 /// </summary>
52 public RowDictionary<WixDeltaPatchFileRow> ResolveDeltaPatchSymbolPaths(RowDictionary<WixDeltaPatchFileRow> deltaPatchFiles, IEnumerable<FileFacade> facades)
53 {
54 ILookup<string, FileFacade> filesByComponent = null;
55 ILookup<string, FileFacade> filesByDirectory = null;
56 ILookup<string, FileFacade> filesByDiskId = null;
57
58 foreach (WixDeltaPatchSymbolPathsRow row in this.WixDeltaPatchSymbolPathsTable.RowsAs<WixDeltaPatchSymbolPathsRow>().OrderBy(r => r.Type))
59 {
60 switch (row.Type)
61 {
62 case SymbolPathType.File:
63 this.MergeSymbolPaths(row, deltaPatchFiles[row.Id]);
64 break;
65
66 case SymbolPathType.Component:
67 if (null == filesByComponent)
68 {
69 filesByComponent = facades.ToLookup(f => f.File.Component);
70 }
71
72 foreach (FileFacade facade in filesByComponent[row.Id])
73 {
74 this.MergeSymbolPaths(row, deltaPatchFiles[facade.File.File]);
75 }
76 break;
77
78 case SymbolPathType.Directory:
79 if (null == filesByDirectory)
80 {
81 filesByDirectory = facades.ToLookup(f => f.WixFile.Directory);
82 }
83
84 foreach (FileFacade facade in filesByDirectory[row.Id])
85 {
86 this.MergeSymbolPaths(row, deltaPatchFiles[facade.File.File]);
87 }
88 break;
89
90 case SymbolPathType.Media:
91 if (null == filesByDiskId)
92 {
93 filesByDiskId = facades.ToLookup(f => f.WixFile.DiskId.ToString(CultureInfo.InvariantCulture));
94 }
95
96 foreach (FileFacade facade in filesByDiskId[row.Id])
97 {
98 this.MergeSymbolPaths(row, deltaPatchFiles[facade.File.File]);
99 }
100 break;
101
102 case SymbolPathType.Product:
103 foreach (WixDeltaPatchFileRow fileRow in deltaPatchFiles.Values)
104 {
105 this.MergeSymbolPaths(row, fileRow);
106 }
107 break;
108
109 default:
110 // error
111 break;
112 }
113 }
114
115 return deltaPatchFiles;
116 }
117
118 /// <summary>
119 /// Merge data from a row in the WixPatchSymbolsPaths table into an associated WixDeltaPatchFile row.
120 /// </summary>
121 /// <param name="row">Row from the WixPatchSymbolsPaths table.</param>
122 /// <param name="file">FileRow into which to set symbol information.</param>
123 /// <comment>This includes PreviousData as well.</comment>
124 private void MergeSymbolPaths(WixDeltaPatchSymbolPathsRow row, WixDeltaPatchFileRow file)
125 {
126 if (null == file.Symbols)
127 {
128 file.Symbols = row.SymbolPaths;
129 }
130 else
131 {
132 file.Symbols = String.Concat(file.Symbols, ";", row.SymbolPaths);
133 }
134
135 Field field = row.Fields[2];
136 if (null != field.PreviousData)
137 {
138 if (null == file.PreviousSymbols)
139 {
140 file.PreviousSymbols = field.PreviousData;
141 }
142 else
143 {
144 file.PreviousSymbols = String.Concat(file.PreviousSymbols, ";", field.PreviousData);
145 }
146 }
147 }
148 }
149}
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/MergeModulesCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/MergeModulesCommand.cs
new file mode 100644
index 00000000..624cbb43
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/MergeModulesCommand.cs
@@ -0,0 +1,351 @@
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.Diagnostics;
10 using System.Globalization;
11 using System.IO;
12 using System.Linq;
13 using System.Runtime.InteropServices;
14 using System.Text;
15 using System.Xml;
16 using System.Xml.XPath;
17 using WixToolset.Clr.Interop;
18 using WixToolset.Data;
19 using WixToolset.Data.Rows;
20 using WixToolset.MergeMod;
21 using WixToolset.Msi;
22 using WixToolset.Core.Native;
23 using WixToolset.Core.Bind;
24
25 /// <summary>
26 /// Update file information.
27 /// </summary>
28 internal class MergeModulesCommand
29 {
30 public IEnumerable<FileFacade> FileFacades { private get; set; }
31
32 public Output Output { private get; set; }
33
34 public string OutputPath { private get; set; }
35
36 public IEnumerable<string> SuppressedTableNames { private get; set; }
37
38 public string TempFilesLocation { private get; set; }
39
40 public void Execute()
41 {
42 Debug.Assert(OutputType.Product == this.Output.Type);
43
44 Table wixMergeTable = this.Output.Tables["WixMerge"];
45 Table wixFeatureModulesTable = this.Output.Tables["WixFeatureModules"];
46
47 // check for merge rows to see if there is any work to do
48 if (null == wixMergeTable || 0 == wixMergeTable.Rows.Count)
49 {
50 return;
51 }
52
53 IMsmMerge2 merge = null;
54 bool commit = true;
55 bool logOpen = false;
56 bool databaseOpen = false;
57 string logPath = null;
58 try
59 {
60 merge = MsmInterop.GetMsmMerge();
61
62 logPath = Path.Combine(this.TempFilesLocation, "merge.log");
63 merge.OpenLog(logPath);
64 logOpen = true;
65
66 merge.OpenDatabase(this.OutputPath);
67 databaseOpen = true;
68
69 // process all the merge rows
70 foreach (WixMergeRow wixMergeRow in wixMergeTable.Rows)
71 {
72 bool moduleOpen = false;
73
74 try
75 {
76 short mergeLanguage;
77
78 try
79 {
80 mergeLanguage = Convert.ToInt16(wixMergeRow.Language, CultureInfo.InvariantCulture);
81 }
82 catch (System.FormatException)
83 {
84 Messaging.Instance.OnMessage(WixErrors.InvalidMergeLanguage(wixMergeRow.SourceLineNumbers, wixMergeRow.Id, wixMergeRow.Language));
85 continue;
86 }
87
88 Messaging.Instance.OnMessage(WixVerboses.OpeningMergeModule(wixMergeRow.SourceFile, mergeLanguage));
89 merge.OpenModule(wixMergeRow.SourceFile, mergeLanguage);
90 moduleOpen = true;
91
92 // If there is merge configuration data, create a callback object to contain it all.
93 ConfigurationCallback callback = null;
94 if (!String.IsNullOrEmpty(wixMergeRow.ConfigurationData))
95 {
96 callback = new ConfigurationCallback(wixMergeRow.ConfigurationData);
97 }
98
99 // merge the module into the database that's being built
100 Messaging.Instance.OnMessage(WixVerboses.MergingMergeModule(wixMergeRow.SourceFile));
101 merge.MergeEx(wixMergeRow.Feature, wixMergeRow.Directory, callback);
102
103 // connect any non-primary features
104 if (null != wixFeatureModulesTable)
105 {
106 foreach (Row row in wixFeatureModulesTable.Rows)
107 {
108 if (wixMergeRow.Id == (string)row[1])
109 {
110 Messaging.Instance.OnMessage(WixVerboses.ConnectingMergeModule(wixMergeRow.SourceFile, (string)row[0]));
111 merge.Connect((string)row[0]);
112 }
113 }
114 }
115 }
116 catch (COMException)
117 {
118 commit = false;
119 }
120 finally
121 {
122 IMsmErrors mergeErrors = merge.Errors;
123
124 // display all the errors encountered during the merge operations for this module
125 for (int i = 1; i <= mergeErrors.Count; i++)
126 {
127 IMsmError mergeError = mergeErrors[i];
128 StringBuilder databaseKeys = new StringBuilder();
129 StringBuilder moduleKeys = new StringBuilder();
130
131 // build a string of the database keys
132 for (int j = 1; j <= mergeError.DatabaseKeys.Count; j++)
133 {
134 if (1 != j)
135 {
136 databaseKeys.Append(';');
137 }
138 databaseKeys.Append(mergeError.DatabaseKeys[j]);
139 }
140
141 // build a string of the module keys
142 for (int j = 1; j <= mergeError.ModuleKeys.Count; j++)
143 {
144 if (1 != j)
145 {
146 moduleKeys.Append(';');
147 }
148 moduleKeys.Append(mergeError.ModuleKeys[j]);
149 }
150
151 // display the merge error based on the msm error type
152 switch (mergeError.Type)
153 {
154 case MsmErrorType.msmErrorExclusion:
155 Messaging.Instance.OnMessage(WixErrors.MergeExcludedModule(wixMergeRow.SourceLineNumbers, wixMergeRow.Id, moduleKeys.ToString()));
156 break;
157 case MsmErrorType.msmErrorFeatureRequired:
158 Messaging.Instance.OnMessage(WixErrors.MergeFeatureRequired(wixMergeRow.SourceLineNumbers, mergeError.ModuleTable, moduleKeys.ToString(), wixMergeRow.SourceFile, wixMergeRow.Id));
159 break;
160 case MsmErrorType.msmErrorLanguageFailed:
161 Messaging.Instance.OnMessage(WixErrors.MergeLanguageFailed(wixMergeRow.SourceLineNumbers, mergeError.Language, wixMergeRow.SourceFile));
162 break;
163 case MsmErrorType.msmErrorLanguageUnsupported:
164 Messaging.Instance.OnMessage(WixErrors.MergeLanguageUnsupported(wixMergeRow.SourceLineNumbers, mergeError.Language, wixMergeRow.SourceFile));
165 break;
166 case MsmErrorType.msmErrorResequenceMerge:
167 Messaging.Instance.OnMessage(WixWarnings.MergeRescheduledAction(wixMergeRow.SourceLineNumbers, mergeError.DatabaseTable, databaseKeys.ToString(), wixMergeRow.SourceFile));
168 break;
169 case MsmErrorType.msmErrorTableMerge:
170 if ("_Validation" != mergeError.DatabaseTable) // ignore merge errors in the _Validation table
171 {
172 Messaging.Instance.OnMessage(WixWarnings.MergeTableFailed(wixMergeRow.SourceLineNumbers, mergeError.DatabaseTable, databaseKeys.ToString(), wixMergeRow.SourceFile));
173 }
174 break;
175 case MsmErrorType.msmErrorPlatformMismatch:
176 Messaging.Instance.OnMessage(WixErrors.MergePlatformMismatch(wixMergeRow.SourceLineNumbers, wixMergeRow.SourceFile));
177 break;
178 default:
179 Messaging.Instance.OnMessage(WixErrors.UnexpectedException(String.Format(CultureInfo.CurrentUICulture, WixStrings.EXP_UnexpectedMergerErrorWithType, Enum.GetName(typeof(MsmErrorType), mergeError.Type), logPath), "InvalidOperationException", Environment.StackTrace));
180 break;
181 }
182 }
183
184 if (0 >= mergeErrors.Count && !commit)
185 {
186 Messaging.Instance.OnMessage(WixErrors.UnexpectedException(String.Format(CultureInfo.CurrentUICulture, WixStrings.EXP_UnexpectedMergerErrorInSourceFile, wixMergeRow.SourceFile, logPath), "InvalidOperationException", Environment.StackTrace));
187 }
188
189 if (moduleOpen)
190 {
191 merge.CloseModule();
192 }
193 }
194 }
195 }
196 finally
197 {
198 if (databaseOpen)
199 {
200 merge.CloseDatabase(commit);
201 }
202
203 if (logOpen)
204 {
205 merge.CloseLog();
206 }
207 }
208
209 // stop processing if an error previously occurred
210 if (Messaging.Instance.EncounteredError)
211 {
212 return;
213 }
214
215 using (Database db = new Database(this.OutputPath, OpenDatabase.Direct))
216 {
217 Table suppressActionTable = this.Output.Tables["WixSuppressAction"];
218
219 // suppress individual actions
220 if (null != suppressActionTable)
221 {
222 foreach (Row row in suppressActionTable.Rows)
223 {
224 if (db.TableExists((string)row[0]))
225 {
226 string query = String.Format(CultureInfo.InvariantCulture, "SELECT * FROM {0} WHERE `Action` = '{1}'", row[0].ToString(), (string)row[1]);
227
228 using (View view = db.OpenExecuteView(query))
229 {
230 using (Record record = view.Fetch())
231 {
232 if (null != record)
233 {
234 Messaging.Instance.OnMessage(WixWarnings.SuppressMergedAction((string)row[1], row[0].ToString()));
235 view.Modify(ModifyView.Delete, record);
236 }
237 }
238 }
239 }
240 }
241 }
242
243 // query for merge module actions in suppressed sequences and drop them
244 foreach (string tableName in this.SuppressedTableNames)
245 {
246 if (!db.TableExists(tableName))
247 {
248 continue;
249 }
250
251 using (View view = db.OpenExecuteView(String.Concat("SELECT `Action` FROM ", tableName)))
252 {
253 while (true)
254 {
255 using (Record resultRecord = view.Fetch())
256 {
257 if (null == resultRecord)
258 {
259 break;
260 }
261
262 Messaging.Instance.OnMessage(WixWarnings.SuppressMergedAction(resultRecord.GetString(1), tableName));
263 }
264 }
265 }
266
267 // drop suppressed sequences
268 using (View view = db.OpenExecuteView(String.Concat("DROP TABLE ", tableName)))
269 {
270 }
271
272 // delete the validation rows
273 using (View view = db.OpenView(String.Concat("DELETE FROM _Validation WHERE `Table` = ?")))
274 {
275 using (Record record = new Record(1))
276 {
277 record.SetString(1, tableName);
278 view.Execute(record);
279 }
280 }
281 }
282
283 // now update the Attributes column for the files from the Merge Modules
284 Messaging.Instance.OnMessage(WixVerboses.ResequencingMergeModuleFiles());
285 using (View view = db.OpenView("SELECT `Sequence`, `Attributes` FROM `File` WHERE `File`=?"))
286 {
287 foreach (FileFacade file in this.FileFacades)
288 {
289 if (!file.FromModule)
290 {
291 continue;
292 }
293
294 using (Record record = new Record(1))
295 {
296 record.SetString(1, file.File.File);
297 view.Execute(record);
298 }
299
300 using (Record recordUpdate = view.Fetch())
301 {
302 if (null == recordUpdate)
303 {
304 throw new InvalidOperationException("Failed to fetch a File row from the database that was merged in from a module.");
305 }
306
307 recordUpdate.SetInteger(1, file.File.Sequence);
308
309 // update the file attributes to match the compression specified
310 // on the Merge element or on the Package element
311 int attributes = 0;
312
313 // get the current value if its not null
314 if (!recordUpdate.IsNull(2))
315 {
316 attributes = recordUpdate.GetInteger(2);
317 }
318
319 if (YesNoType.Yes == file.File.Compressed)
320 {
321 // these are mutually exclusive
322 attributes |= MsiInterop.MsidbFileAttributesCompressed;
323 attributes &= ~MsiInterop.MsidbFileAttributesNoncompressed;
324 }
325 else if (YesNoType.No == file.File.Compressed)
326 {
327 // these are mutually exclusive
328 attributes |= MsiInterop.MsidbFileAttributesNoncompressed;
329 attributes &= ~MsiInterop.MsidbFileAttributesCompressed;
330 }
331 else // not specified
332 {
333 Debug.Assert(YesNoType.NotSet == file.File.Compressed);
334
335 // clear any compression bits
336 attributes &= ~MsiInterop.MsidbFileAttributesCompressed;
337 attributes &= ~MsiInterop.MsidbFileAttributesNoncompressed;
338 }
339
340 recordUpdate.SetInteger(2, attributes);
341
342 view.Modify(ModifyView.Update, recordUpdate);
343 }
344 }
345 }
346
347 db.Commit();
348 }
349 }
350 }
351}
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/ProcessUncompressedFilesCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/ProcessUncompressedFilesCommand.cs
new file mode 100644
index 00000000..b3c09b9e
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/ProcessUncompressedFilesCommand.cs
@@ -0,0 +1,118 @@
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;
7 using System.Collections.Generic;
8 using System.IO;
9 using WixToolset.Data;
10 using WixToolset.Data.Rows;
11 using WixToolset.Msi;
12 using WixToolset.Core.Native;
13 using WixToolset.Bind;
14 using WixToolset.Core.Bind;
15 using WixToolset.Data.Bind;
16
17 /// <summary>
18 /// Defines the file transfers necessary to layout the uncompressed files.
19 /// </summary>
20 internal class ProcessUncompressedFilesCommand
21 {
22 public string DatabasePath { private get; set; }
23
24 public IEnumerable<FileFacade> FileFacades { private get; set; }
25
26 public RowDictionary<MediaRow> MediaRows { private get; set; }
27
28 public string LayoutDirectory { private get; set; }
29
30 public bool Compressed { private get; set; }
31
32 public bool LongNamesInImage { private get; set; }
33
34 public Func<MediaRow, string, string, string> ResolveMedia { private get; set; }
35
36 public Table WixMediaTable { private get; set; }
37
38 public IEnumerable<FileTransfer> FileTransfers { get; private set; }
39
40 public void Execute()
41 {
42 List<FileTransfer> fileTransfers = new List<FileTransfer>();
43
44 Hashtable directories = new Hashtable();
45
46 RowDictionary<WixMediaRow> wixMediaRows = new RowDictionary<WixMediaRow>(this.WixMediaTable);
47
48 using (Database db = new Database(this.DatabasePath, OpenDatabase.ReadOnly))
49 {
50 using (View directoryView = db.OpenExecuteView("SELECT `Directory`, `Directory_Parent`, `DefaultDir` FROM `Directory`"))
51 {
52 while (true)
53 {
54 using (Record directoryRecord = directoryView.Fetch())
55 {
56 if (null == directoryRecord)
57 {
58 break;
59 }
60
61 string sourceName = Common.GetName(directoryRecord.GetString(3), true, this.LongNamesInImage);
62
63 directories.Add(directoryRecord.GetString(1), new ResolvedDirectory(directoryRecord.GetString(2), sourceName));
64 }
65 }
66 }
67
68 using (View fileView = db.OpenView("SELECT `Directory_`, `FileName` FROM `Component`, `File` WHERE `Component`.`Component`=`File`.`Component_` AND `File`.`File`=?"))
69 {
70 using (Record fileQueryRecord = new Record(1))
71 {
72 // for each file in the array of uncompressed files
73 foreach (FileFacade facade in this.FileFacades)
74 {
75 MediaRow mediaRow = this.MediaRows.Get(facade.WixFile.DiskId);
76 string relativeFileLayoutPath = null;
77
78 WixMediaRow wixMediaRow = null;
79 string mediaLayoutFolder = null;
80
81 if (wixMediaRows.TryGetValue(mediaRow.GetKey(), out wixMediaRow))
82 {
83 mediaLayoutFolder = wixMediaRow.Layout;
84 }
85
86 string mediaLayoutDirectory = this.ResolveMedia(mediaRow, mediaLayoutFolder, this.LayoutDirectory);
87
88 // setup up the query record and find the appropriate file in the
89 // previously executed file view
90 fileQueryRecord[1] = facade.File.File;
91 fileView.Execute(fileQueryRecord);
92
93 using (Record fileRecord = fileView.Fetch())
94 {
95 if (null == fileRecord)
96 {
97 throw new WixException(WixErrors.FileIdentifierNotFound(facade.File.SourceLineNumbers, facade.File.File));
98 }
99
100 relativeFileLayoutPath = Binder.GetFileSourcePath(directories, fileRecord[1], fileRecord[2], this.Compressed, this.LongNamesInImage);
101 }
102
103 // finally put together the base media layout path and the relative file layout path
104 string fileLayoutPath = Path.Combine(mediaLayoutDirectory, relativeFileLayoutPath);
105 FileTransfer transfer;
106 if (FileTransfer.TryCreate(facade.WixFile.Source, fileLayoutPath, false, "File", facade.File.SourceLineNumbers, out transfer))
107 {
108 fileTransfers.Add(transfer);
109 }
110 }
111 }
112 }
113 }
114
115 this.FileTransfers = fileTransfers;
116 }
117 }
118}
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/UpdateControlTextCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/UpdateControlTextCommand.cs
new file mode 100644
index 00000000..7da32206
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/UpdateControlTextCommand.cs
@@ -0,0 +1,80 @@
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.IO;
7 using WixToolset.Data;
8 using WixToolset.Data.Rows;
9
10 internal class UpdateControlTextCommand
11 {
12 public Table BBControlTable { private get; set; }
13
14 public Table WixBBControlTable { private get; set; }
15
16 public Table ControlTable { private get; set; }
17
18 public Table WixControlTable { private get; set; }
19
20 public void Execute()
21 {
22 if (null != this.WixBBControlTable)
23 {
24 RowDictionary<BBControlRow> bbControlRows = new RowDictionary<BBControlRow>(this.BBControlTable);
25 foreach (Row wixRow in this.WixBBControlTable.Rows)
26 {
27 BBControlRow bbControlRow = bbControlRows.Get(wixRow.GetPrimaryKey());
28 bbControlRow.Text = this.ReadTextFile(bbControlRow.SourceLineNumbers, wixRow.FieldAsString(2));
29 }
30 }
31
32 if (null != this.WixControlTable)
33 {
34 RowDictionary<ControlRow> controlRows = new RowDictionary<ControlRow>(this.ControlTable);
35 foreach (Row wixRow in this.WixControlTable.Rows)
36 {
37 ControlRow controlRow = controlRows.Get(wixRow.GetPrimaryKey());
38 controlRow.Text = this.ReadTextFile(controlRow.SourceLineNumbers, wixRow.FieldAsString(2));
39 }
40 }
41 }
42
43 /// <summary>
44 /// Reads a text file and returns the contents.
45 /// </summary>
46 /// <param name="sourceLineNumbers">Source line numbers for row from source.</param>
47 /// <param name="source">Source path to file to read.</param>
48 /// <returns>Text string read from file.</returns>
49 private string ReadTextFile(SourceLineNumber sourceLineNumbers, string source)
50 {
51 string text = null;
52
53 try
54 {
55 using (StreamReader reader = new StreamReader(source))
56 {
57 text = reader.ReadToEnd();
58 }
59 }
60 catch (DirectoryNotFoundException e)
61 {
62 Messaging.Instance.OnMessage(WixErrors.BinderFileManagerMissingFile(sourceLineNumbers, e.Message));
63 }
64 catch (FileNotFoundException e)
65 {
66 Messaging.Instance.OnMessage(WixErrors.BinderFileManagerMissingFile(sourceLineNumbers, e.Message));
67 }
68 catch (IOException e)
69 {
70 Messaging.Instance.OnMessage(WixErrors.BinderFileManagerMissingFile(sourceLineNumbers, e.Message));
71 }
72 catch (NotSupportedException)
73 {
74 Messaging.Instance.OnMessage(WixErrors.FileNotFound(sourceLineNumbers, source));
75 }
76
77 return text;
78 }
79 }
80}
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}
diff --git a/src/WixToolset.Core.WindowsInstaller/CLR/Interop/CLRInterop.cs b/src/WixToolset.Core.WindowsInstaller/CLR/Interop/CLRInterop.cs
new file mode 100644
index 00000000..4157f23a
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/CLR/Interop/CLRInterop.cs
@@ -0,0 +1,147 @@
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.Clr.Interop
4{
5 using System;
6 using System.Runtime.InteropServices;
7
8 /// <summary>
9 /// Interop class for mscorwks.dll assembly name APIs.
10 /// </summary>
11 internal sealed class ClrInterop
12 {
13 private static readonly Guid referenceIdentityGuid = new Guid("6eaf5ace-7917-4f3c-b129-e046a9704766");
14
15 /// <summary>
16 /// Protect the constructor.
17 /// </summary>
18 private ClrInterop()
19 {
20 }
21
22 /// <summary>
23 /// Represents a reference to the unique signature of a code object.
24 /// </summary>
25 [ComImport]
26 [Guid("6eaf5ace-7917-4f3c-b129-e046a9704766")]
27 [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
28 internal interface IReferenceIdentity
29 {
30 /// <summary>
31 /// Get an assembly attribute.
32 /// </summary>
33 /// <param name="attributeNamespace">Attribute namespace.</param>
34 /// <param name="attributeName">Attribute name.</param>
35 /// <returns>The assembly attribute.</returns>
36 [return: MarshalAs(UnmanagedType.LPWStr)]
37 string GetAttribute(
38 [In, MarshalAs(UnmanagedType.LPWStr)] string attributeNamespace,
39 [In, MarshalAs(UnmanagedType.LPWStr)] string attributeName);
40
41 /// <summary>
42 /// Set an assembly attribute.
43 /// </summary>
44 /// <param name="attributeNamespace">Attribute namespace.</param>
45 /// <param name="attributeName">Attribute name.</param>
46 /// <param name="attributeValue">Attribute value.</param>
47 void SetAttribute(
48 [In, MarshalAs(UnmanagedType.LPWStr)] string attributeNamespace,
49 [In, MarshalAs(UnmanagedType.LPWStr)] string attributeName,
50 [In, MarshalAs(UnmanagedType.LPWStr)] string attributeValue);
51
52 /// <summary>
53 /// Get an iterator for the assembly's attributes.
54 /// </summary>
55 /// <returns>Assembly attribute enumerator.</returns>
56 IEnumIDENTITY_ATTRIBUTE EnumAttributes();
57
58 /// <summary>
59 /// Clone an IReferenceIdentity.
60 /// </summary>
61 /// <param name="countOfDeltas">Count of deltas.</param>
62 /// <param name="deltas">The deltas.</param>
63 /// <returns>Cloned IReferenceIdentity.</returns>
64 IReferenceIdentity Clone(
65 [In] IntPtr /*SIZE_T*/ countOfDeltas,
66 [In, MarshalAs(UnmanagedType.LPArray)] IDENTITY_ATTRIBUTE[] deltas);
67 }
68
69 /// <summary>
70 /// IEnumIDENTITY_ATTRIBUTE interface.
71 /// </summary>
72 [ComImport]
73 [Guid("9cdaae75-246e-4b00-a26d-b9aec137a3eb")]
74 [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
75 internal interface IEnumIDENTITY_ATTRIBUTE
76 {
77 /// <summary>
78 /// Gets the next attributes.
79 /// </summary>
80 /// <param name="celt">Count of elements.</param>
81 /// <param name="attributes">Array of attributes being returned.</param>
82 /// <returns>The next attribute.</returns>
83 uint Next(
84 [In] uint celt,
85 [Out, MarshalAs(UnmanagedType.LPArray)] IDENTITY_ATTRIBUTE[] attributes);
86
87 /// <summary>
88 /// Copy the current attribute into a buffer.
89 /// </summary>
90 /// <param name="available">Number of available bytes.</param>
91 /// <param name="data">Buffer into which attribute should be written.</param>
92 /// <returns>Pointer to buffer containing the attribute.</returns>
93 IntPtr CurrentIntoBuffer(
94 [In] IntPtr /*SIZE_T*/ available,
95 [Out, MarshalAs(UnmanagedType.LPArray)] byte[] data);
96
97 /// <summary>
98 /// Skip past a number of elements.
99 /// </summary>
100 /// <param name="celt">Count of elements to skip.</param>
101 void Skip([In] uint celt);
102
103 /// <summary>
104 /// Reset the enumeration to the beginning.
105 /// </summary>
106 void Reset();
107
108 /// <summary>
109 /// Clone this attribute enumeration.
110 /// </summary>
111 /// <returns>Clone of a IEnumIDENTITY_ATTRIBUTE.</returns>
112 IEnumIDENTITY_ATTRIBUTE Clone();
113 }
114
115 /// <summary>
116 /// Gets the guid.
117 /// </summary>
118 public static Guid ReferenceIdentityGuid
119 {
120 get { return referenceIdentityGuid; }
121 }
122
123 /// <summary>
124 /// Gets an interface pointer to an object with the specified IID, in the assembly at the specified file path.
125 /// </summary>
126 /// <param name="wszAssemblyPath">A valid path to the requested assembly.</param>
127 /// <param name="riid">The IID of the interface to return.</param>
128 /// <param name="i">The returned interface pointer.</param>
129 /// <returns>The error code.</returns>
130 [DllImport("mscorwks.dll", CharSet = CharSet.Unicode, EntryPoint = "GetAssemblyIdentityFromFile")]
131 internal static extern uint GetAssemblyIdentityFromFile(System.String wszAssemblyPath, ref Guid riid, out IReferenceIdentity i);
132
133 /// <summary>
134 /// Assembly attributes. Contains data about an IReferenceIdentity.
135 /// </summary>
136 [StructLayout(LayoutKind.Sequential)]
137 internal struct IDENTITY_ATTRIBUTE
138 {
139 [MarshalAs(UnmanagedType.LPWStr)]
140 public string AttributeNamespace;
141 [MarshalAs(UnmanagedType.LPWStr)]
142 public string AttributeName;
143 [MarshalAs(UnmanagedType.LPWStr)]
144 public string AttributeValue;
145 }
146 }
147}
diff --git a/src/WixToolset.Core.WindowsInstaller/Differ.cs b/src/WixToolset.Core.WindowsInstaller/Differ.cs
new file mode 100644
index 00000000..bdd06d32
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Differ.cs
@@ -0,0 +1,611 @@
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
4{
5 using System;
6 using System.Collections;
7 using System.Collections.Generic;
8 using System.Globalization;
9 using WixToolset.Core;
10 using WixToolset.Data;
11 using WixToolset.Data.Rows;
12 using WixToolset.Extensibility;
13 using WixToolset.Msi;
14
15 /// <summary>
16 /// Creates a transform by diffing two outputs.
17 /// </summary>
18 public sealed class Differ : IMessageHandler
19 {
20 private List<IInspectorExtension> inspectorExtensions;
21 private bool showPedanticMessages;
22 private bool suppressKeepingSpecialRows;
23 private bool preserveUnchangedRows;
24 private const char sectionDelimiter = '/';
25 private SummaryInformationStreams transformSummaryInfo;
26
27 /// <summary>
28 /// Instantiates a new Differ class.
29 /// </summary>
30 public Differ()
31 {
32 this.inspectorExtensions = new List<IInspectorExtension>();
33 }
34
35 /// <summary>
36 /// Gets or sets the option to show pedantic messages.
37 /// </summary>
38 /// <value>The option to show pedantic messages.</value>
39 public bool ShowPedanticMessages
40 {
41 get { return this.showPedanticMessages; }
42 set { this.showPedanticMessages = value; }
43 }
44
45 /// <summary>
46 /// Gets or sets the option to suppress keeping special rows.
47 /// </summary>
48 /// <value>The option to suppress keeping special rows.</value>
49 public bool SuppressKeepingSpecialRows
50 {
51 get { return this.suppressKeepingSpecialRows; }
52 set { this.suppressKeepingSpecialRows = value; }
53 }
54
55 /// <summary>
56 /// Gets or sets the flag to determine if all rows, even unchanged ones will be persisted in the output.
57 /// </summary>
58 /// <value>The option to keep all rows including unchanged rows.</value>
59 public bool PreserveUnchangedRows
60 {
61 get { return this.preserveUnchangedRows; }
62 set { this.preserveUnchangedRows = value; }
63 }
64
65 /// <summary>
66 /// Adds an extension.
67 /// </summary>
68 /// <param name="extension">The extension to add.</param>
69 public void AddExtension(IInspectorExtension extension)
70 {
71 this.inspectorExtensions.Add(extension);
72 }
73
74 /// <summary>
75 /// Creates a transform by diffing two outputs.
76 /// </summary>
77 /// <param name="targetOutput">The target output.</param>
78 /// <param name="updatedOutput">The updated output.</param>
79 /// <returns>The transform.</returns>
80 public Output Diff(Output targetOutput, Output updatedOutput)
81 {
82 return Diff(targetOutput, updatedOutput, 0);
83 }
84
85 /// <summary>
86 /// Creates a transform by diffing two outputs.
87 /// </summary>
88 /// <param name="targetOutput">The target output.</param>
89 /// <param name="updatedOutput">The updated output.</param>
90 /// <param name="validationFlags"></param>
91 /// <returns>The transform.</returns>
92 public Output Diff(Output targetOutput, Output updatedOutput, TransformFlags validationFlags)
93 {
94 Output transform = new Output(null);
95 transform.Type = OutputType.Transform;
96 transform.Codepage = updatedOutput.Codepage;
97 this.transformSummaryInfo = new SummaryInformationStreams();
98
99 // compare the codepages
100 if (targetOutput.Codepage != updatedOutput.Codepage && 0 == (TransformFlags.ErrorChangeCodePage & validationFlags))
101 {
102 this.OnMessage(WixErrors.OutputCodepageMismatch(targetOutput.SourceLineNumbers, targetOutput.Codepage, updatedOutput.Codepage));
103 if (null != updatedOutput.SourceLineNumbers)
104 {
105 this.OnMessage(WixErrors.OutputCodepageMismatch2(updatedOutput.SourceLineNumbers));
106 }
107 }
108
109 // compare the output types
110 if (targetOutput.Type != updatedOutput.Type)
111 {
112 throw new WixException(WixErrors.OutputTypeMismatch(targetOutput.SourceLineNumbers, targetOutput.Type.ToString(), updatedOutput.Type.ToString()));
113 }
114
115 // compare the contents of the tables
116 foreach (Table targetTable in targetOutput.Tables)
117 {
118 Table updatedTable = updatedOutput.Tables[targetTable.Name];
119 TableOperation operation = TableOperation.None;
120
121 List<Row> rows = this.CompareTables(targetOutput, targetTable, updatedTable, out operation);
122
123 if (TableOperation.Drop == operation)
124 {
125 Table droppedTable = transform.EnsureTable(targetTable.Definition);
126 droppedTable.Operation = TableOperation.Drop;
127 }
128 else if (TableOperation.None == operation)
129 {
130 Table modified = transform.EnsureTable(updatedTable.Definition);
131 rows.ForEach(r => modified.Rows.Add(r));
132 }
133 }
134
135 // added tables
136 foreach (Table updatedTable in updatedOutput.Tables)
137 {
138 if (null == targetOutput.Tables[updatedTable.Name])
139 {
140 Table addedTable = transform.EnsureTable(updatedTable.Definition);
141 addedTable.Operation = TableOperation.Add;
142
143 foreach (Row updatedRow in updatedTable.Rows)
144 {
145 updatedRow.Operation = RowOperation.Add;
146 updatedRow.SectionId = sectionDelimiter + updatedRow.SectionId;
147 addedTable.Rows.Add(updatedRow);
148 }
149 }
150 }
151
152 // set summary information properties
153 if (!this.suppressKeepingSpecialRows)
154 {
155 Table summaryInfoTable = transform.Tables["_SummaryInformation"];
156 this.UpdateTransformSummaryInformationTable(summaryInfoTable, validationFlags);
157 }
158
159 return transform;
160 }
161
162 /// <summary>
163 /// Sends a message to the message delegate if there is one.
164 /// </summary>
165 /// <param name="mea">Message event arguments.</param>
166 public void OnMessage(MessageEventArgs e)
167 {
168 Messaging.Instance.OnMessage(e);
169 }
170
171 /// <summary>
172 /// Add a row to the <paramref name="index"/> using the primary key.
173 /// </summary>
174 /// <param name="index">The indexed rows.</param>
175 /// <param name="row">The row to index.</param>
176 private void AddIndexedRow(IDictionary index, Row row)
177 {
178 string primaryKey = row.GetPrimaryKey('/');
179 if (null != primaryKey)
180 {
181 // Overriding WixActionRows have a primary key defined and take precedence in the index.
182 if (row is WixActionRow)
183 {
184 WixActionRow currentRow = (WixActionRow)row;
185 if (index.Contains(primaryKey))
186 {
187 // If the current row is not overridable, see if the indexed row is.
188 if (!currentRow.Overridable)
189 {
190 WixActionRow indexedRow = index[primaryKey] as WixActionRow;
191 if (null != indexedRow && indexedRow.Overridable)
192 {
193 // The indexed key is overridable and should be replaced
194 // (not removed and re-added which results in two Array.Copy
195 // operations for SortedList, or may be re-hashing in other
196 // implementations of IDictionary).
197 index[primaryKey] = currentRow;
198 }
199 }
200
201 // If we got this far, the row does not need to be indexed.
202 return;
203 }
204 }
205
206 // Nothing else should be added more than once.
207 if (!index.Contains(primaryKey))
208 {
209 index.Add(primaryKey, row);
210 }
211 else if (this.showPedanticMessages)
212 {
213 this.OnMessage(WixWarnings.DuplicatePrimaryKey(row.SourceLineNumbers, primaryKey, row.Table.Name));
214 }
215 }
216 else // use the string representation of the row as its primary key (it may not be unique)
217 {
218 // this is provided for compatibility with unreal tables with no primary key
219 // all real tables must specify at least one column as the primary key
220 primaryKey = row.ToString();
221 index[primaryKey] = row;
222 }
223 }
224
225 private Row CompareRows(Table targetTable, Row targetRow, Row updatedRow, out RowOperation operation, out bool keepRow)
226 {
227 Row comparedRow = null;
228 keepRow = false;
229 operation = RowOperation.None;
230
231 if (null == targetRow ^ null == updatedRow)
232 {
233 if (null == targetRow)
234 {
235 operation = updatedRow.Operation = RowOperation.Add;
236 comparedRow = updatedRow;
237 }
238 else if (null == updatedRow)
239 {
240 operation = targetRow.Operation = RowOperation.Delete;
241 targetRow.SectionId = targetRow.SectionId + sectionDelimiter;
242 comparedRow = targetRow;
243 keepRow = true;
244 }
245 }
246 else // possibly modified
247 {
248 updatedRow.Operation = RowOperation.None;
249 if (!this.suppressKeepingSpecialRows && "_SummaryInformation" == targetTable.Name)
250 {
251 // ignore rows that shouldn't be in a transform
252 if (Enum.IsDefined(typeof(SummaryInformation.Transform), (int)updatedRow[0]))
253 {
254 updatedRow.SectionId = targetRow.SectionId + sectionDelimiter + updatedRow.SectionId;
255 comparedRow = updatedRow;
256 keepRow = true;
257 operation = RowOperation.Modify;
258 }
259 }
260 else
261 {
262 if (this.preserveUnchangedRows)
263 {
264 keepRow = true;
265 }
266
267 for (int i = 0; i < updatedRow.Fields.Length; i++)
268 {
269 ColumnDefinition columnDefinition = updatedRow.Fields[i].Column;
270
271 if (!columnDefinition.PrimaryKey)
272 {
273 bool modified = false;
274
275 if (i >= targetRow.Fields.Length)
276 {
277 columnDefinition.Added = true;
278 modified = true;
279 }
280 else if (ColumnType.Number == columnDefinition.Type && !columnDefinition.IsLocalizable)
281 {
282 if (null == targetRow[i] ^ null == updatedRow[i])
283 {
284 modified = true;
285 }
286 else if (null != targetRow[i] && null != updatedRow[i])
287 {
288 modified = ((int)targetRow[i] != (int)updatedRow[i]);
289 }
290 }
291 else if (ColumnType.Preserved == columnDefinition.Type)
292 {
293 updatedRow.Fields[i].PreviousData = (string)targetRow.Fields[i].Data;
294
295 // keep rows containing preserved fields so the historical data is available to the binder
296 keepRow = !this.suppressKeepingSpecialRows;
297 }
298 else if (ColumnType.Object == columnDefinition.Type)
299 {
300 ObjectField targetObjectField = (ObjectField)targetRow.Fields[i];
301 ObjectField updatedObjectField = (ObjectField)updatedRow.Fields[i];
302
303 updatedObjectField.PreviousEmbeddedFileIndex = targetObjectField.EmbeddedFileIndex;
304 updatedObjectField.PreviousBaseUri = targetObjectField.BaseUri;
305
306 // always keep a copy of the previous data even if they are identical
307 // This makes diff.wixmst clean and easier to control patch logic
308 updatedObjectField.PreviousData = (string)targetObjectField.Data;
309
310 // always remember the unresolved data for target build
311 updatedObjectField.UnresolvedPreviousData = (string)targetObjectField.UnresolvedData;
312
313 // keep rows containing object fields so the files can be compared in the binder
314 keepRow = !this.suppressKeepingSpecialRows;
315 }
316 else
317 {
318 modified = ((string)targetRow[i] != (string)updatedRow[i]);
319 }
320
321 if (modified)
322 {
323 if (null != updatedRow.Fields[i].PreviousData)
324 {
325 updatedRow.Fields[i].PreviousData = targetRow.Fields[i].Data.ToString();
326 }
327
328 updatedRow.Fields[i].Modified = true;
329 operation = updatedRow.Operation = RowOperation.Modify;
330 keepRow = true;
331 }
332 }
333 }
334
335 if (keepRow)
336 {
337 comparedRow = updatedRow;
338 comparedRow.SectionId = targetRow.SectionId + sectionDelimiter + updatedRow.SectionId;
339 }
340 }
341 }
342
343 return comparedRow;
344 }
345
346 private List<Row> CompareTables(Output targetOutput, Table targetTable, Table updatedTable, out TableOperation operation)
347 {
348 List<Row> rows = new List<Row>();
349 operation = TableOperation.None;
350
351 // dropped tables
352 if (null == updatedTable ^ null == targetTable)
353 {
354 if (null == targetTable)
355 {
356 operation = TableOperation.Add;
357 rows.AddRange(updatedTable.Rows);
358 }
359 else if (null == updatedTable)
360 {
361 operation = TableOperation.Drop;
362 }
363 }
364 else // possibly modified tables
365 {
366 SortedList updatedPrimaryKeys = new SortedList();
367 SortedList targetPrimaryKeys = new SortedList();
368
369 // compare the table definitions
370 if (0 != targetTable.Definition.CompareTo(updatedTable.Definition))
371 {
372 // continue to the next table; may be more mismatches
373 this.OnMessage(WixErrors.DatabaseSchemaMismatch(targetOutput.SourceLineNumbers, targetTable.Name));
374 }
375 else
376 {
377 this.IndexPrimaryKeys(targetTable, targetPrimaryKeys, updatedTable, updatedPrimaryKeys);
378
379 // diff the target and updated rows
380 foreach (DictionaryEntry targetPrimaryKeyEntry in targetPrimaryKeys)
381 {
382 string targetPrimaryKey = (string)targetPrimaryKeyEntry.Key;
383 bool keepRow = false;
384 RowOperation rowOperation = RowOperation.None;
385
386 Row compared = this.CompareRows(targetTable, targetPrimaryKeyEntry.Value as Row, updatedPrimaryKeys[targetPrimaryKey] as Row, out rowOperation, out keepRow);
387
388 if (keepRow)
389 {
390 rows.Add(compared);
391 }
392 }
393
394 // find the inserted rows
395 foreach (DictionaryEntry updatedPrimaryKeyEntry in updatedPrimaryKeys)
396 {
397 string updatedPrimaryKey = (string)updatedPrimaryKeyEntry.Key;
398
399 if (!targetPrimaryKeys.Contains(updatedPrimaryKey))
400 {
401 Row updatedRow = (Row)updatedPrimaryKeyEntry.Value;
402
403 updatedRow.Operation = RowOperation.Add;
404 updatedRow.SectionId = sectionDelimiter + updatedRow.SectionId;
405 rows.Add(updatedRow);
406 }
407 }
408 }
409 }
410
411 return rows;
412 }
413
414 private void IndexPrimaryKeys(Table targetTable, SortedList targetPrimaryKeys, Table updatedTable, SortedList updatedPrimaryKeys)
415 {
416 // index the target rows
417 foreach (Row row in targetTable.Rows)
418 {
419 this.AddIndexedRow(targetPrimaryKeys, row);
420
421 if ("Property" == targetTable.Name)
422 {
423 if ("ProductCode" == (string)row[0])
424 {
425 this.transformSummaryInfo.TargetProductCode = (string)row[1];
426 if ("*" == this.transformSummaryInfo.TargetProductCode)
427 {
428 this.OnMessage(WixErrors.ProductCodeInvalidForTransform(row.SourceLineNumbers));
429 }
430 }
431 else if ("ProductVersion" == (string)row[0])
432 {
433 this.transformSummaryInfo.TargetProductVersion = (string)row[1];
434 }
435 else if ("UpgradeCode" == (string)row[0])
436 {
437 this.transformSummaryInfo.TargetUpgradeCode = (string)row[1];
438 }
439 }
440 else if ("_SummaryInformation" == targetTable.Name)
441 {
442 if (1 == (int)row[0]) // PID_CODEPAGE
443 {
444 this.transformSummaryInfo.TargetSummaryInfoCodepage = (string)row[1];
445 }
446 else if (7 == (int)row[0]) // PID_TEMPLATE
447 {
448 this.transformSummaryInfo.TargetPlatformAndLanguage = (string)row[1];
449 }
450 else if (14 == (int)row[0]) // PID_PAGECOUNT
451 {
452 this.transformSummaryInfo.TargetMinimumVersion = (string)row[1];
453 }
454 }
455 }
456
457 // index the updated rows
458 foreach (Row row in updatedTable.Rows)
459 {
460 this.AddIndexedRow(updatedPrimaryKeys, row);
461
462 if ("Property" == updatedTable.Name)
463 {
464 if ("ProductCode" == (string)row[0])
465 {
466 this.transformSummaryInfo.UpdatedProductCode = (string)row[1];
467 if ("*" == this.transformSummaryInfo.UpdatedProductCode)
468 {
469 this.OnMessage(WixErrors.ProductCodeInvalidForTransform(row.SourceLineNumbers));
470 }
471 }
472 else if ("ProductVersion" == (string)row[0])
473 {
474 this.transformSummaryInfo.UpdatedProductVersion = (string)row[1];
475 }
476 }
477 else if ("_SummaryInformation" == updatedTable.Name)
478 {
479 if (1 == (int)row[0]) // PID_CODEPAGE
480 {
481 this.transformSummaryInfo.UpdatedSummaryInfoCodepage = (string)row[1];
482 }
483 else if (7 == (int)row[0]) // PID_TEMPLATE
484 {
485 this.transformSummaryInfo.UpdatedPlatformAndLanguage = (string)row[1];
486 }
487 else if (14 == (int)row[0]) // PID_PAGECOUNT
488 {
489 this.transformSummaryInfo.UpdatedMinimumVersion = (string)row[1];
490 }
491 }
492 }
493 }
494
495 private void UpdateTransformSummaryInformationTable(Table summaryInfoTable, TransformFlags validationFlags)
496 {
497 // calculate the minimum version of MSI required to process the transform
498 int targetMin;
499 int updatedMin;
500 int minimumVersion = 100;
501
502 if (Int32.TryParse(this.transformSummaryInfo.TargetMinimumVersion, out targetMin) && Int32.TryParse(this.transformSummaryInfo.UpdatedMinimumVersion, out updatedMin))
503 {
504 minimumVersion = Math.Max(targetMin, updatedMin);
505 }
506
507 Hashtable summaryRows = new Hashtable(summaryInfoTable.Rows.Count);
508 foreach (Row row in summaryInfoTable.Rows)
509 {
510 summaryRows[row[0]] = row;
511
512 if ((int)SummaryInformation.Transform.CodePage == (int)row[0])
513 {
514 row.Fields[1].Data = this.transformSummaryInfo.UpdatedSummaryInfoCodepage;
515 row.Fields[1].PreviousData = this.transformSummaryInfo.TargetSummaryInfoCodepage;
516 }
517 else if ((int)SummaryInformation.Transform.TargetPlatformAndLanguage == (int)row[0])
518 {
519 row[1] = this.transformSummaryInfo.TargetPlatformAndLanguage;
520 }
521 else if ((int)SummaryInformation.Transform.UpdatedPlatformAndLanguage == (int)row[0])
522 {
523 row[1] = this.transformSummaryInfo.UpdatedPlatformAndLanguage;
524 }
525 else if ((int)SummaryInformation.Transform.ProductCodes == (int)row[0])
526 {
527 row[1] = String.Concat(this.transformSummaryInfo.TargetProductCode, this.transformSummaryInfo.TargetProductVersion, ';', this.transformSummaryInfo.UpdatedProductCode, this.transformSummaryInfo.UpdatedProductVersion, ';', this.transformSummaryInfo.TargetUpgradeCode);
528 }
529 else if ((int)SummaryInformation.Transform.InstallerRequirement == (int)row[0])
530 {
531 row[1] = minimumVersion.ToString(CultureInfo.InvariantCulture);
532 }
533 else if ((int)SummaryInformation.Transform.Security == (int)row[0])
534 {
535 row[1] = "4";
536 }
537 }
538
539 if (!summaryRows.Contains((int)SummaryInformation.Transform.TargetPlatformAndLanguage))
540 {
541 Row summaryRow = summaryInfoTable.CreateRow(null);
542 summaryRow[0] = (int)SummaryInformation.Transform.TargetPlatformAndLanguage;
543 summaryRow[1] = this.transformSummaryInfo.TargetPlatformAndLanguage;
544 }
545
546 if (!summaryRows.Contains((int)SummaryInformation.Transform.UpdatedPlatformAndLanguage))
547 {
548 Row summaryRow = summaryInfoTable.CreateRow(null);
549 summaryRow[0] = (int)SummaryInformation.Transform.UpdatedPlatformAndLanguage;
550 summaryRow[1] = this.transformSummaryInfo.UpdatedPlatformAndLanguage;
551 }
552
553 if (!summaryRows.Contains((int)SummaryInformation.Transform.ValidationFlags))
554 {
555 Row summaryRow = summaryInfoTable.CreateRow(null);
556 summaryRow[0] = (int)SummaryInformation.Transform.ValidationFlags;
557 summaryRow[1] = ((int)validationFlags).ToString(CultureInfo.InvariantCulture);
558 }
559
560 if (!summaryRows.Contains((int)SummaryInformation.Transform.InstallerRequirement))
561 {
562 Row summaryRow = summaryInfoTable.CreateRow(null);
563 summaryRow[0] = (int)SummaryInformation.Transform.InstallerRequirement;
564 summaryRow[1] = minimumVersion.ToString(CultureInfo.InvariantCulture);
565 }
566
567 if (!summaryRows.Contains((int)SummaryInformation.Transform.Security))
568 {
569 Row summaryRow = summaryInfoTable.CreateRow(null);
570 summaryRow[0] = (int)SummaryInformation.Transform.Security;
571 summaryRow[1] = "4";
572 }
573 }
574
575 private class SummaryInformationStreams
576 {
577 public string TargetSummaryInfoCodepage
578 { get; set; }
579
580 public string TargetPlatformAndLanguage
581 { get; set; }
582
583 public string TargetProductCode
584 { get; set; }
585
586 public string TargetProductVersion
587 { get; set; }
588
589 public string TargetUpgradeCode
590 { get; set; }
591
592 public string TargetMinimumVersion
593 { get; set; }
594
595 public string UpdatedSummaryInfoCodepage
596 { get; set; }
597
598 public string UpdatedPlatformAndLanguage
599 { get; set; }
600
601 public string UpdatedProductCode
602 { get; set; }
603
604 public string UpdatedProductVersion
605 { get; set; }
606
607 public string UpdatedMinimumVersion
608 { get; set; }
609 }
610 }
611}
diff --git a/src/WixToolset.Core.WindowsInstaller/Inscribe/InscribeMsiPackageCommand.cs b/src/WixToolset.Core.WindowsInstaller/Inscribe/InscribeMsiPackageCommand.cs
new file mode 100644
index 00000000..40901d7c
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Inscribe/InscribeMsiPackageCommand.cs
@@ -0,0 +1,282 @@
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.Inscribe
4{
5 using System;
6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.IO;
9 using System.Runtime.InteropServices;
10 using System.Security.Cryptography.X509Certificates;
11 using WixToolset.Core.Native;
12 using WixToolset.Data;
13 using WixToolset.Extensibility;
14 using WixToolset.Msi;
15
16 internal class InscribeMsiPackageCommand
17 {
18 public InscribeMsiPackageCommand(IInscribeContext context)
19 {
20 this.Context = context;
21 this.TableDefinitions = WindowsInstallerStandard.GetTableDefinitions();
22 }
23
24 private IInscribeContext Context { get; }
25
26 private TableDefinitionCollection TableDefinitions { get; }
27
28 public bool Execute()
29 {
30 // Keeps track of whether we've encountered at least one signed cab or not - we'll throw a warning if no signed cabs were encountered
31 bool foundUnsignedExternals = false;
32 bool shouldCommit = false;
33
34 FileAttributes attributes = File.GetAttributes(this.Context.InputFilePath);
35 if (FileAttributes.ReadOnly == (attributes & FileAttributes.ReadOnly))
36 {
37 this.Context.Messaging.OnMessage(WixErrors.ReadOnlyOutputFile(this.Context.InputFilePath));
38 return shouldCommit;
39 }
40
41 using (Database database = new Database(this.Context.InputFilePath, OpenDatabase.Transact))
42 {
43 // Just use the English codepage, because the tables we're importing only have binary streams / MSI identifiers / other non-localizable content
44 int codepage = 1252;
45
46 // list of certificates for this database (hash/identifier)
47 Dictionary<string, string> certificates = new Dictionary<string, string>();
48
49 // Reset the in-memory tables for this new database
50 Table digitalSignatureTable = new Table(null, this.TableDefinitions["MsiDigitalSignature"]);
51 Table digitalCertificateTable = new Table(null, this.TableDefinitions["MsiDigitalCertificate"]);
52
53 // If any digital signature records exist that are not of the media type, preserve them
54 if (database.TableExists("MsiDigitalSignature"))
55 {
56 using (View digitalSignatureView = database.OpenExecuteView("SELECT `Table`, `SignObject`, `DigitalCertificate_`, `Hash` FROM `MsiDigitalSignature` WHERE `Table` <> 'Media'"))
57 {
58 while (true)
59 {
60 using (Record digitalSignatureRecord = digitalSignatureView.Fetch())
61 {
62 if (null == digitalSignatureRecord)
63 {
64 break;
65 }
66
67 Row digitalSignatureRow = null;
68 digitalSignatureRow = digitalSignatureTable.CreateRow(null);
69
70 string table = digitalSignatureRecord.GetString(0);
71 string signObject = digitalSignatureRecord.GetString(1);
72
73 digitalSignatureRow[0] = table;
74 digitalSignatureRow[1] = signObject;
75 digitalSignatureRow[2] = digitalSignatureRecord.GetString(2);
76
77 if (false == digitalSignatureRecord.IsNull(3))
78 {
79 // Export to a file, because the MSI API's require us to provide a file path on disk
80 string hashPath = Path.Combine(this.Context.IntermediateFolder, "MsiDigitalSignature");
81 string hashFileName = string.Concat(table, ".", signObject, ".bin");
82
83 Directory.CreateDirectory(hashPath);
84 hashPath = Path.Combine(hashPath, hashFileName);
85
86 using (FileStream fs = File.Create(hashPath))
87 {
88 int bytesRead;
89 byte[] buffer = new byte[1024 * 4];
90
91 while (0 != (bytesRead = digitalSignatureRecord.GetStream(3, buffer, buffer.Length)))
92 {
93 fs.Write(buffer, 0, bytesRead);
94 }
95 }
96
97 digitalSignatureRow[3] = hashFileName;
98 }
99 }
100 }
101 }
102 }
103
104 // If any digital certificates exist, extract and preserve them
105 if (database.TableExists("MsiDigitalCertificate"))
106 {
107 using (View digitalCertificateView = database.OpenExecuteView("SELECT * FROM `MsiDigitalCertificate`"))
108 {
109 while (true)
110 {
111 using (Record digitalCertificateRecord = digitalCertificateView.Fetch())
112 {
113 if (null == digitalCertificateRecord)
114 {
115 break;
116 }
117
118 string certificateId = digitalCertificateRecord.GetString(1); // get the identifier of the certificate
119
120 // Export to a file, because the MSI API's require us to provide a file path on disk
121 string certPath = Path.Combine(this.Context.IntermediateFolder, "MsiDigitalCertificate");
122 Directory.CreateDirectory(certPath);
123 certPath = Path.Combine(certPath, string.Concat(certificateId, ".cer"));
124
125 using (FileStream fs = File.Create(certPath))
126 {
127 int bytesRead;
128 byte[] buffer = new byte[1024 * 4];
129
130 while (0 != (bytesRead = digitalCertificateRecord.GetStream(2, buffer, buffer.Length)))
131 {
132 fs.Write(buffer, 0, bytesRead);
133 }
134 }
135
136 // Add it to our "add to MsiDigitalCertificate" table dictionary
137 Row digitalCertificateRow = digitalCertificateTable.CreateRow(null);
138 digitalCertificateRow[0] = certificateId;
139
140 // Now set the file path on disk where this binary stream will be picked up at import time
141 digitalCertificateRow[1] = string.Concat(certificateId, ".cer");
142
143 // Load the cert to get it's thumbprint
144 X509Certificate cert = X509Certificate.CreateFromCertFile(certPath);
145 X509Certificate2 cert2 = new X509Certificate2(cert);
146
147 certificates.Add(cert2.Thumbprint, certificateId);
148 }
149 }
150 }
151 }
152
153 using (View mediaView = database.OpenExecuteView("SELECT * FROM `Media`"))
154 {
155 while (true)
156 {
157 using (Record mediaRecord = mediaView.Fetch())
158 {
159 if (null == mediaRecord)
160 {
161 break;
162 }
163
164 X509Certificate2 cert2 = null;
165 Row digitalSignatureRow = null;
166
167 string cabName = mediaRecord.GetString(4); // get the name of the cab
168 // If there is no cabinet or it's an internal cab, skip it.
169 if (String.IsNullOrEmpty(cabName) || cabName.StartsWith("#", StringComparison.Ordinal))
170 {
171 continue;
172 }
173
174 string cabId = mediaRecord.GetString(1); // get the ID of the cab
175 string cabPath = Path.Combine(Path.GetDirectoryName(this.Context.InputFilePath), cabName);
176
177 // If the cabs aren't there, throw an error but continue to catch the other errors
178 if (!File.Exists(cabPath))
179 {
180 this.Context.Messaging.OnMessage(WixErrors.WixFileNotFound(cabPath));
181 continue;
182 }
183
184 try
185 {
186 // Get the certificate from the cab
187 X509Certificate signedFileCert = X509Certificate.CreateFromSignedFile(cabPath);
188 cert2 = new X509Certificate2(signedFileCert);
189 }
190 catch (System.Security.Cryptography.CryptographicException e)
191 {
192 uint HResult = unchecked((uint)Marshal.GetHRForException(e));
193
194 // If the file has no cert, continue, but flag that we found at least one so we can later give a warning
195 if (0x80092009 == HResult) // CRYPT_E_NO_MATCH
196 {
197 foundUnsignedExternals = true;
198 continue;
199 }
200
201 // todo: exactly which HRESULT corresponds to this issue?
202 // If it's one of these exact platforms, warn the user that it may be due to their OS.
203 if ((5 == Environment.OSVersion.Version.Major && 2 == Environment.OSVersion.Version.Minor) || // W2K3
204 (5 == Environment.OSVersion.Version.Major && 1 == Environment.OSVersion.Version.Minor)) // XP
205 {
206 this.Context.Messaging.OnMessage(WixErrors.UnableToGetAuthenticodeCertOfFileDownlevelOS(cabPath, String.Format(CultureInfo.InvariantCulture, "HRESULT: 0x{0:x8}", HResult)));
207 }
208 else // otherwise, generic error
209 {
210 this.Context.Messaging.OnMessage(WixErrors.UnableToGetAuthenticodeCertOfFile(cabPath, String.Format(CultureInfo.InvariantCulture, "HRESULT: 0x{0:x8}", HResult)));
211 }
212 }
213
214 // If we haven't added this cert to the MsiDigitalCertificate table, set it up to be added
215 if (!certificates.ContainsKey(cert2.Thumbprint))
216 {
217 // generate a stable identifier
218 string certificateGeneratedId = Common.GenerateIdentifier("cer", cert2.Thumbprint);
219
220 // Add it to our "add to MsiDigitalCertificate" table dictionary
221 Row digitalCertificateRow = digitalCertificateTable.CreateRow(null);
222 digitalCertificateRow[0] = certificateGeneratedId;
223
224 // Export to a file, because the MSI API's require us to provide a file path on disk
225 string certPath = Path.Combine(this.Context.IntermediateFolder, "MsiDigitalCertificate");
226 Directory.CreateDirectory(certPath);
227 certPath = Path.Combine(certPath, string.Concat(cert2.Thumbprint, ".cer"));
228 File.Delete(certPath);
229
230 using (BinaryWriter writer = new BinaryWriter(File.Open(certPath, FileMode.Create)))
231 {
232 writer.Write(cert2.RawData);
233 writer.Close();
234 }
235
236 // Now set the file path on disk where this binary stream will be picked up at import time
237 digitalCertificateRow[1] = string.Concat(cert2.Thumbprint, ".cer");
238
239 certificates.Add(cert2.Thumbprint, certificateGeneratedId);
240 }
241
242 digitalSignatureRow = digitalSignatureTable.CreateRow(null);
243
244 digitalSignatureRow[0] = "Media";
245 digitalSignatureRow[1] = cabId;
246 digitalSignatureRow[2] = certificates[cert2.Thumbprint];
247 }
248 }
249 }
250
251 if (digitalCertificateTable.Rows.Count > 0)
252 {
253 database.ImportTable(codepage, digitalCertificateTable, this.Context.IntermediateFolder, true);
254 shouldCommit = true;
255 }
256
257 if (digitalSignatureTable.Rows.Count > 0)
258 {
259 database.ImportTable(codepage, digitalSignatureTable, this.Context.IntermediateFolder, true);
260 shouldCommit = true;
261 }
262
263 // TODO: if we created the table(s), then we should add the _Validation records for them.
264
265 certificates = null;
266
267 // If we did find external cabs but none of them were signed, give a warning
268 if (foundUnsignedExternals)
269 {
270 this.Context.Messaging.OnMessage(WixWarnings.ExternalCabsAreNotSigned(this.Context.InputFilePath));
271 }
272
273 if (shouldCommit)
274 {
275 database.Commit();
276 }
277 }
278
279 return shouldCommit;
280 }
281 }
282}
diff --git a/src/WixToolset.Core.WindowsInstaller/MergeMod/NativeMethods.cs b/src/WixToolset.Core.WindowsInstaller/MergeMod/NativeMethods.cs
new file mode 100644
index 00000000..daf259b4
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/MergeMod/NativeMethods.cs
@@ -0,0 +1,508 @@
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#if false
3namespace WixToolset.MergeMod
4{
5 using System;
6 using System.Collections;
7 using System.Runtime.CompilerServices;
8 using System.Runtime.InteropServices;
9
10 /// <summary>
11 /// Errors returned by merge operations.
12 /// </summary>
13 [Guid("0ADDA825-2C26-11D2-AD65-00A0C9AF11A6")]
14 internal enum MsmErrorType
15 {
16 /// <summary>
17 /// A request was made to open a module with a language not supported by the module.
18 /// No more general language is supported by the module.
19 /// Adds msmErrorLanguageUnsupported to the Type property and the requested language
20 /// to the Language Property (Error Object). All Error object properties are empty.
21 /// The OpenModule function returns ERROR_INSTALL_LANGUAGE_UNSUPPORTED (as HRESULT).
22 /// </summary>
23 msmErrorLanguageUnsupported = 1,
24
25 /// <summary>
26 /// A request was made to open a module with a supported language but the module has
27 /// an invalid language transform. Adds msmErrorLanguageFailed to the Type property
28 /// and the applied transform's language to the Language Property of the Error object.
29 /// This may not be the requested language if a more general language was used.
30 /// All other properties of the Error object are empty. The OpenModule function
31 /// returns ERROR_INSTALL_LANGUAGE_UNSUPPORTED (as HRESULT).
32 /// </summary>
33 msmErrorLanguageFailed = 2,
34
35 /// <summary>
36 /// The module cannot be merged because it excludes, or is excluded by, another module
37 /// in the database. Adds msmErrorExclusion to the Type property of the Error object.
38 /// The ModuleKeys property or DatabaseKeys property contains the primary keys of the
39 /// excluded module's row in the ModuleExclusion table. If an existing module excludes
40 /// the module being merged, the excluded module's ModuleSignature information is added
41 /// to ModuleKeys. If the module being merged excludes an existing module, DatabaseKeys
42 /// contains the excluded module's ModuleSignature information. All other properties
43 /// are empty (or -1).
44 /// </summary>
45 msmErrorExclusion = 3,
46
47 /// <summary>
48 /// Merge conflict during merge. The value of the Type property is set to
49 /// msmErrorTableMerge. The DatabaseTable property and DatabaseKeys property contain
50 /// the table name and primary keys of the conflicting row in the database. The
51 /// ModuleTable property and ModuleKeys property contain the table name and primary keys
52 /// of the conflicting row in the module. The ModuleTable and ModuleKeys entries may be
53 /// null if the row does not exist in the database. For example, if the conflict is in a
54 /// generated FeatureComponents table entry. On Windows Installer version 2.0, when
55 /// merging a configurable merge module, configuration may cause these properties to
56 /// refer to rows that do not exist in the module.
57 /// </summary>
58 msmErrorTableMerge = 4,
59
60 /// <summary>
61 /// There was a problem resequencing a sequence table to contain the necessary merged
62 /// actions. The Type property is set to msmErrorResequenceMerge. The DatabaseTable
63 /// and DatabaseKeys properties contain the sequence table name and primary keys
64 /// (action name) of the conflicting row. The ModuleTable and ModuleKeys properties
65 /// contain the sequence table name and primary key (action name) of the conflicting row.
66 /// On Windows Installer version 2.0, when merging a configurable merge module,
67 /// configuration may cause these properties to refer to rows that do not exist in the module.
68 /// </summary>
69 msmErrorResequenceMerge = 5,
70
71 /// <summary>
72 /// Not used.
73 /// </summary>
74 msmErrorFileCreate = 6,
75
76 /// <summary>
77 /// There was a problem creating a directory to extract a file to disk. The Path property
78 /// contains the directory that could not be created. All other properties are empty or -1.
79 /// Not available with Windows Installer version 1.0.
80 /// </summary>
81 msmErrorDirCreate = 7,
82
83 /// <summary>
84 /// A feature name is required to complete the merge, but no feature name was provided.
85 /// The Type property is set to msmErrorFeatureRequired. The DatabaseTable and DatabaseKeys
86 /// contain the table name and primary keys of the conflicting row. The ModuleTable and
87 /// ModuleKeys properties contain the table name and primary keys of the row cannot be merged.
88 /// On Windows Installer version 2.0, when merging a configurable merge module, configuration
89 /// may cause these properties to refer to rows that do not exist in the module.
90 /// If the failure is in a generated FeatureComponents table, the DatabaseTable and
91 /// DatabaseKeys properties are empty and the ModuleTable and ModuleKeys properties refer to
92 /// the row in the Component table causing the failure.
93 /// </summary>
94 msmErrorFeatureRequired = 8,
95
96 /// <summary>
97 /// Available with Window Installer version 2.0. Substitution of a Null value into a
98 /// non-nullable column. This enters msmErrorBadNullSubstitution in the Type property and
99 /// enters "ModuleSubstitution" and the keys from the ModuleSubstitution table for this row
100 /// into the ModuleTable property and ModuleKeys property. All other properties of the Error
101 /// object are set to an empty string or -1. This error causes the immediate failure of the
102 /// merge and the MergeEx function to return E_FAIL.
103 /// </summary>
104 msmErrorBadNullSubstitution = 9,
105
106 /// <summary>
107 /// Available with Window Installer version 2.0. Substitution of Text Format Type or Integer
108 /// Format Type into a Binary Type data column. This type of error returns
109 /// msmErrorBadSubstitutionType in the Type property and enters "ModuleSubstitution" and the
110 /// keys from the ModuleSubstitution table for this row into the ModuleTable property.
111 /// All other properties of the Error object are set to an empty string or -1. This error
112 /// causes the immediate failure of the merge and the MergeEx function to return E_FAIL.
113 /// </summary>
114 msmErrorBadSubstitutionType = 10,
115
116 /// <summary>
117 /// Available with Window Installer Version 2.0. A row in the ModuleSubstitution table
118 /// references a configuration item not defined in the ModuleConfiguration table.
119 /// This type of error returns msmErrorMissingConfigItem in the Type property and enters
120 /// "ModuleSubstitution" and the keys from the ModuleSubstitution table for this row into
121 /// the ModuleTable property. All other properties of the Error object are set to an empty
122 /// string or -1. This error causes the immediate failure of the merge and the MergeEx
123 /// function to return E_FAIL.
124 /// </summary>
125 msmErrorMissingConfigItem = 11,
126
127 /// <summary>
128 /// Available with Window Installer version 2.0. The authoring tool has returned a Null
129 /// value for an item marked with the msmConfigItemNonNullable attribute. An error of this
130 /// type returns msmErrorBadNullResponse in the Type property and enters "ModuleSubstitution"
131 /// and the keys from the ModuleSubstitution table for for the item into the ModuleTable property.
132 /// All other properties of the Error object are set to an empty string or -1. This error
133 /// causes the immediate failure of the merge and the MergeEx function to return E_FAIL.
134 /// </summary>
135 msmErrorBadNullResponse = 12,
136
137 /// <summary>
138 /// Available with Window Installer version 2.0. The authoring tool returned a failure code
139 /// (not S_OK or S_FALSE) when asked for data. An error of this type will return
140 /// msmErrorDataRequestFailed in the Type property and enters "ModuleSubstitution"
141 /// and the keys from the ModuleSubstitution table for the item into the ModuleTable property.
142 /// All other properties of the Error object are set to an empty string or -1. This error
143 /// causes the immediate failure of the merge and the MergeEx function to return E_FAIL.
144 /// </summary>
145 msmErrorDataRequestFailed = 13,
146
147 /// <summary>
148 /// Available with Windows Installer 2.0 and later versions. Indicates that an attempt was
149 /// made to merge a 64-bit module into a package that was not a 64-bit package. An error of
150 /// this type returns msmErrorPlatformMismatch in the Type property. All other properties of
151 /// the error object are set to an empty string or -1. This error causes the immediate failure
152 /// of the merge and causes the Merge function or MergeEx function to return E_FAIL.
153 /// </summary>
154 msmErrorPlatformMismatch = 14,
155 }
156
157 /// <summary>
158 /// IMsmMerge2 interface.
159 /// </summary>
160 [ComImport, Guid("351A72AB-21CB-47ab-B7AA-C4D7B02EA305")]
161 internal interface IMsmMerge2
162 {
163 /// <summary>
164 /// The OpenDatabase method of the Merge object opens a Windows Installer installation
165 /// database, located at a specified path, that is to be merged with a module.
166 /// </summary>
167 /// <param name="path">Path to the database being opened.</param>
168 void OpenDatabase(string path);
169
170 /// <summary>
171 /// The OpenModule method of the Merge object opens a Windows Installer merge module
172 /// in read-only mode. A module must be opened before it can be merged with an installation database.
173 /// </summary>
174 /// <param name="fileName">Fully qualified file name pointing to a merge module.</param>
175 /// <param name="language">A valid language identifier (LANGID).</param>
176 void OpenModule(string fileName, short language);
177
178 /// <summary>
179 /// The CloseDatabase method of the Merge object closes the currently open Windows Installer database.
180 /// </summary>
181 /// <param name="commit">true if changes should be saved, false otherwise.</param>
182 void CloseDatabase(bool commit);
183
184 /// <summary>
185 /// The CloseModule method of the Merge object closes the currently open Windows Installer merge module.
186 /// </summary>
187 void CloseModule();
188
189 /// <summary>
190 /// The OpenLog method of the Merge object opens a log file that receives progress and error messages.
191 /// If the log file already exists, the installer appends new messages. If the log file does not exist,
192 /// the installer creates a log file.
193 /// </summary>
194 /// <param name="fileName">Fully qualified filename pointing to a file to open or create.</param>
195 void OpenLog(string fileName);
196
197 /// <summary>
198 /// The CloseLog method of the Merge object closes the current log file.
199 /// </summary>
200 void CloseLog();
201
202 /// <summary>
203 /// The Log method of the Merge object writes a text string to the currently open log file.
204 /// </summary>
205 /// <param name="message">The text string to display.</param>
206 void Log(string message);
207
208 /// <summary>
209 /// Gets the errors from the last merge operation.
210 /// </summary>
211 /// <value>The errors from the last merge operation.</value>
212 IMsmErrors Errors
213 {
214 get;
215 }
216
217 /// <summary>
218 /// Gets a collection of Dependency objects that enumerates a set of unsatisfied dependencies for the current database.
219 /// </summary>
220 /// <value>A collection of Dependency objects that enumerates a set of unsatisfied dependencies for the current database.</value>
221 object Dependencies
222 {
223 get;
224 }
225
226 /// <summary>
227 /// The Merge method of the Merge object executes a merge of the current database and current
228 /// module. The merge attaches the components in the module to the feature identified by Feature.
229 /// The root of the module's directory tree is redirected to the location given by RedirectDir.
230 /// </summary>
231 /// <param name="feature">The name of a feature in the database.</param>
232 /// <param name="redirectDir">The key of an entry in the Directory table of the database.
233 /// This parameter may be NULL or an empty string.</param>
234 void Merge(string feature, string redirectDir);
235
236 /// <summary>
237 /// The Connect method of the Merge object connects a module to an additional feature.
238 /// The module must have already been merged into the database or will be merged into the database.
239 /// The feature must exist before calling this function.
240 /// </summary>
241 /// <param name="feature">The name of a feature already existing in the database.</param>
242 void Connect(string feature);
243
244 /// <summary>
245 /// The ExtractCAB method of the Merge object extracts the embedded .cab file from a module and
246 /// saves it as the specified file. The installer creates this file if it does not already exist
247 /// and overwritten if it does exist.
248 /// </summary>
249 /// <param name="fileName">The fully qualified destination file.</param>
250 void ExtractCAB(string fileName);
251
252 /// <summary>
253 /// The ExtractFiles method of the Merge object extracts the embedded .cab file from a module
254 /// and then writes those files to the destination directory.
255 /// </summary>
256 /// <param name="path">The fully qualified destination directory.</param>
257 void ExtractFiles(string path);
258
259 /// <summary>
260 /// The MergeEx method of the Merge object is equivalent to the Merge function, except that it
261 /// takes an extra argument. The Merge method executes a merge of the current database and
262 /// current module. The merge attaches the components in the module to the feature identified
263 /// by Feature. The root of the module's directory tree is redirected to the location given by RedirectDir.
264 /// </summary>
265 /// <param name="feature">The name of a feature in the database.</param>
266 /// <param name="redirectDir">The key of an entry in the Directory table of the database. This parameter may
267 /// be NULL or an empty string.</param>
268 /// <param name="configuration">The pConfiguration argument is an interface implemented by the client. The argument may
269 /// be NULL. The presence of this argument indicates that the client is capable of supporting the configuration
270 /// functionality, but does not obligate the client to provide configuration data for any specific configurable item.</param>
271 void MergeEx(string feature, string redirectDir, IMsmConfigureModule configuration);
272
273 /// <summary>
274 /// The ExtractFilesEx method of the Merge object extracts the embedded .cab file from a module and
275 /// then writes those files to the destination directory.
276 /// </summary>
277 /// <param name="path">The fully qualified destination directory.</param>
278 /// <param name="longFileNames">Set to specify using long file names for path segments and final file names.</param>
279 /// <param name="filePaths">This is a list of fully-qualified paths for the files that were successfully extracted.
280 /// The list is empty if no files can be extracted. This argument may be null. No list is provided if pFilePaths is null.</param>
281 void ExtractFilesEx(string path, bool longFileNames, ref IntPtr filePaths);
282
283 /// <summary>
284 /// Gets a collection ConfigurableItem objects, each of which represents a single row from the ModuleConfiguration table.
285 /// </summary>
286 /// <value>A collection ConfigurableItem objects, each of which represents a single row from the ModuleConfiguration table.</value>
287 /// <remarks>Semantically, each interface in the enumerator represents an item that can be configured by the module consumer.
288 /// The collection is a read-only collection and implements the standard read-only collection interfaces of Item(), Count() and _NewEnum().
289 /// The IEnumMsmConfigItems enumerator implements Next(), Skip(), Reset(), and Clone() with the standard semantics.</remarks>
290 object ConfigurableItems
291 {
292 get;
293 }
294
295 /// <summary>
296 /// The CreateSourceImage method of the Merge object allows the client to extract the files from a module to
297 /// a source image on disk after a merge, taking into account changes to the module that might have been made
298 /// during module configuration. The list of files to be extracted is taken from the file table of the module
299 /// during the merge process. The list of files consists of every file successfully copied from the file table
300 /// of the module to the target database. File table entries that were not copied due to primary key conflicts
301 /// with existing rows in the database are not a part of this list. At image creation time, the directory for
302 /// each of these files comes from the open (post-merge) database. The path specified in the Path parameter is
303 /// the root of the source image for the install. fLongFileNames determines whether or not long file names are
304 /// used for both path segments and final file names. The function fails if no database is open, no module is
305 /// open, or no merge has been performed.
306 /// </summary>
307 /// <param name="path">The path of the root of the source image for the install.</param>
308 /// <param name="longFileNames">Determines whether or not long file names are used for both path segments and final file names. </param>
309 /// <param name="filePaths">This is a list of fully-qualified paths for the files that were successfully extracted.
310 /// The list is empty if no files can be extracted. This argument may be null. No list is provided if pFilePaths is null.</param>
311 void CreateSourceImage(string path, bool longFileNames, ref IntPtr filePaths);
312
313 /// <summary>
314 /// The get_ModuleFiles function implements the ModuleFiles property of the GetFiles object. This function
315 /// returns the primary keys in the File table of the currently open module. The primary keys are returned
316 /// as a collection of strings. The module must be opened by a call to the OpenModule function before calling get_ModuleFiles.
317 /// </summary>
318 IMsmStrings ModuleFiles
319 {
320 get;
321 }
322 }
323
324 /// <summary>
325 /// Collection of merge errors.
326 /// </summary>
327 [ComImport, Guid("0ADDA82A-2C26-11D2-AD65-00A0C9AF11A6")]
328 internal interface IMsmErrors
329 {
330 /// <summary>
331 /// Gets the IMsmError at the specified index.
332 /// </summary>
333 /// <param name="index">The one-based index of the IMsmError to get.</param>
334 IMsmError this[int index]
335 {
336 get;
337 }
338
339 /// <summary>
340 /// Gets the count of IMsmErrors in this collection.
341 /// </summary>
342 /// <value>The count of IMsmErrors in this collection.</value>
343 int Count
344 {
345 get;
346 }
347 }
348
349 /// <summary>
350 /// A merge error.
351 /// </summary>
352 [ComImport, Guid("0ADDA828-2C26-11D2-AD65-00A0C9AF11A6")]
353 internal interface IMsmError
354 {
355 /// <summary>
356 /// Gets the type of merge error.
357 /// </summary>
358 /// <value>The type of merge error.</value>
359 MsmErrorType Type
360 {
361 get;
362 }
363
364 /// <summary>
365 /// Gets the path information from the merge error.
366 /// </summary>
367 /// <value>The path information from the merge error.</value>
368 string Path
369 {
370 get;
371 }
372
373 /// <summary>
374 /// Gets the language information from the merge error.
375 /// </summary>
376 /// <value>The language information from the merge error.</value>
377 short Language
378 {
379 get;
380 }
381
382 /// <summary>
383 /// Gets the database table from the merge error.
384 /// </summary>
385 /// <value>The database table from the merge error.</value>
386 string DatabaseTable
387 {
388 get;
389 }
390
391 /// <summary>
392 /// Gets the collection of database keys from the merge error.
393 /// </summary>
394 /// <value>The collection of database keys from the merge error.</value>
395 IMsmStrings DatabaseKeys
396 {
397 get;
398 }
399
400 /// <summary>
401 /// Gets the module table from the merge error.
402 /// </summary>
403 /// <value>The module table from the merge error.</value>
404 string ModuleTable
405 {
406 get;
407 }
408
409 /// <summary>
410 /// Gets the collection of module keys from the merge error.
411 /// </summary>
412 /// <value>The collection of module keys from the merge error.</value>
413 IMsmStrings ModuleKeys
414 {
415 get;
416 }
417 }
418
419 /// <summary>
420 /// A collection of strings.
421 /// </summary>
422 [ComImport, Guid("0ADDA827-2C26-11D2-AD65-00A0C9AF11A6")]
423 internal interface IMsmStrings
424 {
425 /// <summary>
426 /// Gets the string at the specified index.
427 /// </summary>
428 /// <param name="index">The one-based index of the string to get.</param>
429 string this[int index]
430 {
431 get;
432 }
433
434 /// <summary>
435 /// Gets the count of strings in this collection.
436 /// </summary>
437 /// <value>The count of strings in this collection.</value>
438 int Count
439 {
440 get;
441 }
442 }
443
444 /// <summary>
445 /// Callback for configurable merge modules.
446 /// </summary>
447 [ComImport, Guid("AC013209-18A7-4851-8A21-2353443D70A0"), InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
448 internal interface IMsmConfigureModule
449 {
450 /// <summary>
451 /// Callback to retrieve text data for configurable merge modules.
452 /// </summary>
453 /// <param name="name">Name of the data to be retrieved.</param>
454 /// <param name="configData">The data corresponding to the name.</param>
455 /// <returns>The error code (HRESULT).</returns>
456 [PreserveSig]
457 int ProvideTextData([In, MarshalAs(UnmanagedType.BStr)] string name, [MarshalAs(UnmanagedType.BStr)] out string configData);
458
459 /// <summary>
460 /// Callback to retrieve integer data for configurable merge modules.
461 /// </summary>
462 /// <param name="name">Name of the data to be retrieved.</param>
463 /// <param name="configData">The data corresponding to the name.</param>
464 /// <returns>The error code (HRESULT).</returns>
465 [PreserveSig]
466 int ProvideIntegerData([In, MarshalAs(UnmanagedType.BStr)] string name, out int configData);
467 }
468
469 /// <summary>
470 /// Merge merge modules into an MSI file.
471 /// </summary>
472 [ComImport, Guid("F94985D5-29F9-4743-9805-99BC3F35B678")]
473 internal class MsmMerge2
474 {
475 }
476
477 /// <summary>
478 /// Defines the standard COM IClassFactory interface.
479 /// </summary>
480 [ComImport, Guid("00000001-0000-0000-C000-000000000046")]
481 [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
482 internal interface IClassFactory
483 {
484 [return:MarshalAs(UnmanagedType.IUnknown)]
485 object CreateInstance(IntPtr unkOuter, [MarshalAs(UnmanagedType.LPStruct)] Guid iid);
486 }
487
488 /// <summary>
489 /// Contains native methods for merge operations.
490 /// </summary>
491 internal class NativeMethods
492 {
493 [DllImport("mergemod.dll", EntryPoint="DllGetClassObject", PreserveSig=false)]
494 [return: MarshalAs(UnmanagedType.IUnknown)]
495 private static extern object MergeModGetClassObject([MarshalAs(UnmanagedType.LPStruct)] Guid clsid, [MarshalAs(UnmanagedType.LPStruct)] Guid iid);
496
497 /// <summary>
498 /// Load the merge object directly from a local mergemod.dll without going through COM registration.
499 /// </summary>
500 /// <returns>Merge interface.</returns>
501 internal static IMsmMerge2 GetMsmMerge()
502 {
503 IClassFactory classFactory = (IClassFactory) MergeModGetClassObject(typeof(MsmMerge2).GUID, typeof(IClassFactory).GUID);
504 return (IMsmMerge2) classFactory.CreateInstance(IntPtr.Zero, typeof(IMsmMerge2).GUID);
505 }
506 }
507}
508#endif \ No newline at end of file
diff --git a/src/WixToolset.Core.WindowsInstaller/Msi/Database.cs b/src/WixToolset.Core.WindowsInstaller/Msi/Database.cs
new file mode 100644
index 00000000..801ebdde
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Msi/Database.cs
@@ -0,0 +1,303 @@
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.Msi
4{
5 using System;
6 using System.ComponentModel;
7 using System.Globalization;
8 using System.IO;
9 using System.Text;
10 using System.Threading;
11 using WixToolset.Data;
12 using WixToolset.Core.Native;
13
14 /// <summary>
15 /// Wrapper class for managing MSI API database handles.
16 /// </summary>
17 internal sealed class Database : MsiHandle
18 {
19 private const int STG_E_LOCKVIOLATION = unchecked((int)0x80030021);
20
21 /// <summary>
22 /// Constructor that opens an MSI database.
23 /// </summary>
24 /// <param name="path">Path to the database to be opened.</param>
25 /// <param name="type">Persist mode to use when opening the database.</param>
26 public Database(string path, OpenDatabase type)
27 {
28 uint handle = 0;
29 int error = MsiInterop.MsiOpenDatabase(path, new IntPtr((int)type), out handle);
30 if (0 != error)
31 {
32 throw new MsiException(error);
33 }
34 this.Handle = handle;
35 }
36
37 public void ApplyTransform(string transformFile)
38 {
39 // get the curret validation bits
40 TransformErrorConditions conditions = TransformErrorConditions.None;
41 using (SummaryInformation summaryInfo = new SummaryInformation(transformFile))
42 {
43 string value = summaryInfo.GetProperty((int)SummaryInformation.Transform.ValidationFlags);
44 try
45 {
46 int validationFlags = Int32.Parse(value, CultureInfo.InvariantCulture);
47 conditions = (TransformErrorConditions)(validationFlags & 0xffff);
48 }
49 catch (FormatException)
50 {
51 // fallback to default of None
52 }
53 }
54
55 this.ApplyTransform(transformFile, conditions);
56 }
57
58 /// <summary>
59 /// Applies a transform to this database.
60 /// </summary>
61 /// <param name="transformFile">Path to the transform file being applied.</param>
62 /// <param name="errorConditions">Specifies the error conditions that are to be suppressed.</param>
63 public void ApplyTransform(string transformFile, TransformErrorConditions errorConditions)
64 {
65 int error = MsiInterop.MsiDatabaseApplyTransform(this.Handle, transformFile, errorConditions);
66 if (0 != error)
67 {
68 throw new MsiException(error);
69 }
70 }
71
72 /// <summary>
73 /// Commits changes made to the database.
74 /// </summary>
75 public void Commit()
76 {
77 // Retry this call 3 times to deal with an MSI internal locking problem.
78 const int retryWait = 300;
79 const int retryLimit = 3;
80 int error = 0;
81
82 for (int i = 1; i <= retryLimit; ++i)
83 {
84 error = MsiInterop.MsiDatabaseCommit(this.Handle);
85
86 if (0 == error)
87 {
88 return;
89 }
90 else
91 {
92 MsiException exception = new MsiException(error);
93
94 // We need to see if the error code is contained in any of the strings in ErrorInfo.
95 // Join the array together and search for the error code to cover the string array.
96 if (!String.Join(", ", exception.ErrorInfo).Contains(STG_E_LOCKVIOLATION.ToString()))
97 {
98 break;
99 }
100
101 Console.Error.WriteLine(String.Format("Failed to create the database. Info: {0}. Retrying ({1} of {2})", String.Join(", ", exception.ErrorInfo), i, retryLimit));
102 Thread.Sleep(retryWait);
103 }
104 }
105
106 throw new MsiException(error);
107 }
108
109 /// <summary>
110 /// Creates and populates the summary information stream of an existing transform file.
111 /// </summary>
112 /// <param name="referenceDatabase">Required database that does not include the changes.</param>
113 /// <param name="transformFile">The name of the generated transform file.</param>
114 /// <param name="errorConditions">Required error conditions that should be suppressed when the transform is applied.</param>
115 /// <param name="validations">Required when the transform is applied to a database;
116 /// shows which properties should be validated to verify that this transform can be applied to the database.</param>
117 public void CreateTransformSummaryInfo(Database referenceDatabase, string transformFile, TransformErrorConditions errorConditions, TransformValidations validations)
118 {
119 int error = MsiInterop.MsiCreateTransformSummaryInfo(this.Handle, referenceDatabase.Handle, transformFile, errorConditions, validations);
120 if (0 != error)
121 {
122 throw new MsiException(error);
123 }
124 }
125
126 /// <summary>
127 /// Imports an installer text archive table (idt file) into an open database.
128 /// </summary>
129 /// <param name="idtPath">Specifies the path to the file to import.</param>
130 /// <exception cref="WixInvalidIdtException">Attempted to import an IDT file with an invalid format or unsupported data.</exception>
131 /// <exception cref="MsiException">Another error occured while importing the IDT file.</exception>
132 public void Import(string idtPath)
133 {
134 string folderPath = Path.GetFullPath(Path.GetDirectoryName(idtPath));
135 string fileName = Path.GetFileName(idtPath);
136
137 int error = MsiInterop.MsiDatabaseImport(this.Handle, folderPath, fileName);
138 if (1627 == error) // ERROR_FUNCTION_FAILED
139 {
140 throw new WixInvalidIdtException(idtPath);
141 }
142 else if (0 != error)
143 {
144 throw new MsiException(error);
145 }
146 }
147
148 /// <summary>
149 /// Exports an installer table from an open database to a text archive file (idt file).
150 /// </summary>
151 /// <param name="tableName">Specifies the name of the table to export.</param>
152 /// <param name="folderPath">Specifies the name of the folder that contains archive files. If null or empty string, uses current directory.</param>
153 /// <param name="fileName">Specifies the name of the exported table archive file.</param>
154 public void Export(string tableName, string folderPath, string fileName)
155 {
156 if (null == folderPath || 0 == folderPath.Length)
157 {
158 folderPath = System.Environment.CurrentDirectory;
159 }
160
161 int error = MsiInterop.MsiDatabaseExport(this.Handle, tableName, folderPath, fileName);
162 if (0 != error)
163 {
164 throw new MsiException(error);
165 }
166 }
167
168 /// <summary>
169 /// Creates a transform that, when applied to the reference database, results in this database.
170 /// </summary>
171 /// <param name="referenceDatabase">Required database that does not include the changes.</param>
172 /// <param name="transformFile">The name of the generated transform file. This is optional.</param>
173 /// <returns>true if a transform is generated; false if a transform is not generated because
174 /// there are no differences between the two databases.</returns>
175 public bool GenerateTransform(Database referenceDatabase, string transformFile)
176 {
177 int error = MsiInterop.MsiDatabaseGenerateTransform(this.Handle, referenceDatabase.Handle, transformFile, 0, 0);
178 if (0 != error && 0xE8 != error) // ERROR_NO_DATA(0xE8) means no differences were found
179 {
180 throw new MsiException(error);
181 }
182
183 return (0xE8 != error);
184 }
185
186 /// <summary>
187 /// Merges two databases together.
188 /// </summary>
189 /// <param name="mergeDatabase">The database to merge into the base database.</param>
190 /// <param name="tableName">The name of the table to receive merge conflict information.</param>
191 public void Merge(Database mergeDatabase, string tableName)
192 {
193 int error = MsiInterop.MsiDatabaseMerge(this.Handle, mergeDatabase.Handle, tableName);
194 if (0 != error)
195 {
196 throw new MsiException(error);
197 }
198 }
199
200 /// <summary>
201 /// Prepares a database query and creates a <see cref="View">View</see> object.
202 /// </summary>
203 /// <param name="query">Specifies a SQL query string for querying the database.</param>
204 /// <returns>A view object is returned if the query was successful.</returns>
205 public View OpenView(string query)
206 {
207 return new View(this, query);
208 }
209
210 /// <summary>
211 /// Prepares and executes a database query and creates a <see cref="View">View</see> object.
212 /// </summary>
213 /// <param name="query">Specifies a SQL query string for querying the database.</param>
214 /// <returns>A view object is returned if the query was successful.</returns>
215 public View OpenExecuteView(string query)
216 {
217 View view = new View(this, query);
218
219 view.Execute();
220 return view;
221 }
222
223 /// <summary>
224 /// Verifies the existence or absence of a table.
225 /// </summary>
226 /// <param name="tableName">Table name to to verify the existence of.</param>
227 /// <returns>Returns true if the table exists, false if it does not.</returns>
228 public bool TableExists(string tableName)
229 {
230 int result = MsiInterop.MsiDatabaseIsTablePersistent(this.Handle, tableName);
231 return MsiInterop.MSICONDITIONTRUE == result;
232 }
233
234 /// <summary>
235 /// Returns a <see cref="Record">Record</see> containing the names of all the primary
236 /// key columns for a specified table.
237 /// </summary>
238 /// <param name="tableName">Specifies the name of the table from which to obtain
239 /// primary key names.</param>
240 /// <returns>Returns a <see cref="Record">Record</see> containing the names of all the
241 /// primary key columns for a specified table.</returns>
242 public Record PrimaryKeys(string tableName)
243 {
244 uint recordHandle;
245 int error = MsiInterop.MsiDatabaseGetPrimaryKeys(this.Handle, tableName, out recordHandle);
246 if (0 != error)
247 {
248 throw new MsiException(error);
249 }
250
251 return new Record(recordHandle);
252 }
253
254 /// <summary>
255 /// Imports a table into the database.
256 /// </summary>
257 /// <param name="codepage">Codepage of the database to import table to.</param>
258 /// <param name="table">Table to import into database.</param>
259 /// <param name="baseDirectory">The base directory where intermediate files are created.</param>
260 /// <param name="keepAddedColumns">Whether to keep columns added in a transform.</param>
261 public void ImportTable(int codepage, Table table, string baseDirectory, bool keepAddedColumns)
262 {
263 // write out the table to an IDT file
264 string idtPath = Path.Combine(baseDirectory, String.Concat(table.Name, ".idt"));
265 Encoding encoding;
266
267 // If UTF8 encoding, use the UTF8-specific constructor to avoid writing
268 // the byte order mark at the beginning of the file
269 if (Encoding.UTF8.CodePage == codepage)
270 {
271 encoding = new UTF8Encoding(false, true);
272 }
273 else
274 {
275 if (0 == codepage)
276 {
277 codepage = Encoding.ASCII.CodePage;
278 }
279
280 encoding = Encoding.GetEncoding(codepage, new EncoderExceptionFallback(), new DecoderExceptionFallback());
281 }
282
283 using (StreamWriter idtWriter = new StreamWriter(idtPath, false, encoding))
284 {
285 table.ToIdtDefinition(idtWriter, keepAddedColumns);
286 }
287
288 // try to import the table into the MSI
289 try
290 {
291 this.Import(idtPath);
292 }
293 catch (WixInvalidIdtException)
294 {
295 table.ValidateRows();
296
297 // If ValidateRows finds anything it doesn't like, it throws. Otherwise, we'll
298 // throw WixInvalidIdtException here which is caught in light and turns off tidy.
299 throw new WixInvalidIdtException(idtPath, table.Name);
300 }
301 }
302 }
303}
diff --git a/src/WixToolset.Core.WindowsInstaller/Msi/Installer.cs b/src/WixToolset.Core.WindowsInstaller/Msi/Installer.cs
new file mode 100644
index 00000000..f8bce602
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Msi/Installer.cs
@@ -0,0 +1,363 @@
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.Msi
4{
5 using System;
6 using System.Diagnostics;
7 using System.Text;
8 using WixToolset.Core.Native;
9
10 /// <summary>
11 /// Windows Installer message types.
12 /// </summary>
13 [Flags]
14 internal enum InstallMessage
15 {
16 /// <summary>
17 /// Premature termination, possibly fatal out of memory.
18 /// </summary>
19 FatalExit = 0x00000000,
20
21 /// <summary>
22 /// Formatted error message, [1] is message number in Error table.
23 /// </summary>
24 Error = 0x01000000,
25
26 /// <summary>
27 /// Formatted warning message, [1] is message number in Error table.
28 /// </summary>
29 Warning = 0x02000000,
30
31 /// <summary>
32 /// User request message, [1] is message number in Error table.
33 /// </summary>
34 User = 0x03000000,
35
36 /// <summary>
37 /// Informative message for log, not to be displayed.
38 /// </summary>
39 Info = 0x04000000,
40
41 /// <summary>
42 /// List of files in use that need to be replaced.
43 /// </summary>
44 FilesInUse = 0x05000000,
45
46 /// <summary>
47 /// Request to determine a valid source location.
48 /// </summary>
49 ResolveSource = 0x06000000,
50
51 /// <summary>
52 /// Insufficient disk space message.
53 /// </summary>
54 OutOfDiskSpace = 0x07000000,
55
56 /// <summary>
57 /// Progress: start of action, [1] action name, [2] description, [3] template for ACTIONDATA messages.
58 /// </summary>
59 ActionStart = 0x08000000,
60
61 /// <summary>
62 /// Action data. Record fields correspond to the template of ACTIONSTART message.
63 /// </summary>
64 ActionData = 0x09000000,
65
66 /// <summary>
67 /// Progress bar information. See the description of record fields below.
68 /// </summary>
69 Progress = 0x0A000000,
70
71 /// <summary>
72 /// To enable the Cancel button set [1] to 2 and [2] to 1. To disable the Cancel button set [1] to 2 and [2] to 0.
73 /// </summary>
74 CommonData = 0x0B000000,
75
76 /// <summary>
77 /// Sent prior to UI initialization, no string data.
78 /// </summary>
79 Initilize = 0x0C000000,
80
81 /// <summary>
82 /// Sent after UI termination, no string data.
83 /// </summary>
84 Terminate = 0x0D000000,
85
86 /// <summary>
87 /// Sent prior to display or authored dialog or wizard.
88 /// </summary>
89 ShowDialog = 0x0E000000
90 }
91
92 /// <summary>
93 /// Windows Installer log modes.
94 /// </summary>
95 [Flags]
96 internal enum InstallLogModes
97 {
98 /// <summary>
99 /// Premature termination of installation.
100 /// </summary>
101 FatalExit = (1 << ((int)InstallMessage.FatalExit >> 24)),
102
103 /// <summary>
104 /// The error messages are logged.
105 /// </summary>
106 Error = (1 << ((int)InstallMessage.Error >> 24)),
107
108 /// <summary>
109 /// The warning messages are logged.
110 /// </summary>
111 Warning = (1 << ((int)InstallMessage.Warning >> 24)),
112
113 /// <summary>
114 /// The user requests are logged.
115 /// </summary>
116 User = (1 << ((int)InstallMessage.User >> 24)),
117
118 /// <summary>
119 /// The status messages that are not displayed are logged.
120 /// </summary>
121 Info = (1 << ((int)InstallMessage.Info >> 24)),
122
123 /// <summary>
124 /// Request to determine a valid source location.
125 /// </summary>
126 ResolveSource = (1 << ((int)InstallMessage.ResolveSource >> 24)),
127
128 /// <summary>
129 /// The was insufficient disk space.
130 /// </summary>
131 OutOfDiskSpace = (1 << ((int)InstallMessage.OutOfDiskSpace >> 24)),
132
133 /// <summary>
134 /// The start of new installation actions are logged.
135 /// </summary>
136 ActionStart = (1 << ((int)InstallMessage.ActionStart >> 24)),
137
138 /// <summary>
139 /// The data record with the installation action is logged.
140 /// </summary>
141 ActionData = (1 << ((int)InstallMessage.ActionData >> 24)),
142
143 /// <summary>
144 /// The parameters for user-interface initialization are logged.
145 /// </summary>
146 CommonData = (1 << ((int)InstallMessage.CommonData >> 24)),
147
148 /// <summary>
149 /// Logs the property values at termination.
150 /// </summary>
151 PropertyDump = (1 << ((int)InstallMessage.Progress >> 24)),
152
153 /// <summary>
154 /// Sends large amounts of information to a log file not generally useful to users.
155 /// May be used for technical support.
156 /// </summary>
157 Verbose = (1 << ((int)InstallMessage.Initilize >> 24)),
158
159 /// <summary>
160 /// Sends extra debugging information, such as handle creation information, to the log file.
161 /// </summary>
162 ExtraDebug = (1 << ((int)InstallMessage.Terminate >> 24)),
163
164 /// <summary>
165 /// Progress bar information. This message includes information on units so far and total number of units.
166 /// See MsiProcessMessage for an explanation of the message format.
167 /// This message is only sent to an external user interface and is not logged.
168 /// </summary>
169 Progress = (1 << ((int)InstallMessage.Progress >> 24)),
170
171 /// <summary>
172 /// If this is not a quiet installation, then the basic UI has been initialized.
173 /// If this is a full UI installation, the full UI is not yet initialized.
174 /// This message is only sent to an external user interface and is not logged.
175 /// </summary>
176 Initialize = (1 << ((int)InstallMessage.Initilize >> 24)),
177
178 /// <summary>
179 /// If a full UI is being used, the full UI has ended.
180 /// If this is not a quiet installation, the basic UI has not yet ended.
181 /// This message is only sent to an external user interface and is not logged.
182 /// </summary>
183 Terminate = (1 << ((int)InstallMessage.Terminate >> 24)),
184
185 /// <summary>
186 /// Sent prior to display of the full UI dialog.
187 /// This message is only sent to an external user interface and is not logged.
188 /// </summary>
189 ShowDialog = (1 << ((int)InstallMessage.ShowDialog >> 24)),
190
191 /// <summary>
192 /// Files in use information. When this message is received, a FilesInUse Dialog should be displayed.
193 /// </summary>
194 FilesInUse = (1 << ((int)InstallMessage.FilesInUse >> 24))
195 }
196
197 /// <summary>
198 /// Windows Installer UI levels.
199 /// </summary>
200 [Flags]
201 internal enum InstallUILevels
202 {
203 /// <summary>
204 /// No change in the UI level. However, if phWnd is not Null, the parent window can change.
205 /// </summary>
206 NoChange = 0,
207
208 /// <summary>
209 /// The installer chooses an appropriate user interface level.
210 /// </summary>
211 Default = 1,
212
213 /// <summary>
214 /// Completely silent installation.
215 /// </summary>
216 None = 2,
217
218 /// <summary>
219 /// Simple progress and error handling.
220 /// </summary>
221 Basic = 3,
222
223 /// <summary>
224 /// Authored user interface with wizard dialog boxes suppressed.
225 /// </summary>
226 Reduced = 4,
227
228 /// <summary>
229 /// Authored user interface with wizards, progress, and errors.
230 /// </summary>
231 Full = 5,
232
233 /// <summary>
234 /// If combined with the Basic value, the installer shows simple progress dialog boxes but
235 /// does not display a Cancel button on the dialog. This prevents users from canceling the install.
236 /// Available with Windows Installer version 2.0.
237 /// </summary>
238 HideCancel = 0x20,
239
240 /// <summary>
241 /// If combined with the Basic value, the installer shows simple progress
242 /// dialog boxes but does not display any modal dialog boxes or error dialog boxes.
243 /// </summary>
244 ProgressOnly = 0x40,
245
246 /// <summary>
247 /// If combined with any above value, the installer displays a modal dialog
248 /// box at the end of a successful installation or if there has been an error.
249 /// No dialog box is displayed if the user cancels.
250 /// </summary>
251 EndDialog = 0x80,
252
253 /// <summary>
254 /// If this value is combined with the None value, the installer displays only the dialog
255 /// boxes used for source resolution. No other dialog boxes are shown. This value has no
256 /// effect if the UI level is not INSTALLUILEVEL_NONE. It is used with an external user
257 /// interface designed to handle all of the UI except for source resolution. In this case,
258 /// the installer handles source resolution. This value is only available with Windows Installer 2.0 and later.
259 /// </summary>
260 SourceResOnly = 0x100
261 }
262
263 /// <summary>
264 /// Represents the Windows Installer, provides wrappers to
265 /// create the top-level objects and access their methods.
266 /// </summary>
267 internal sealed class Installer
268 {
269 /// <summary>
270 /// Protect the constructor.
271 /// </summary>
272 private Installer()
273 {
274 }
275
276 /// <summary>
277 /// Takes the path to a file and returns a 128-bit hash of that file.
278 /// </summary>
279 /// <param name="filePath">Path to file that is to be hashed.</param>
280 /// <param name="options">The value in this column must be 0. This parameter is reserved for future use.</param>
281 /// <param name="hash">Int array that receives the returned file hash information.</param>
282 internal static void GetFileHash(string filePath, int options, out int[] hash)
283 {
284 MsiInterop.MSIFILEHASHINFO hashInterop = new MsiInterop.MSIFILEHASHINFO();
285 hashInterop.FileHashInfoSize = 20;
286
287 int error = MsiInterop.MsiGetFileHash(filePath, Convert.ToUInt32(options), hashInterop);
288 if (0 != error)
289 {
290 throw new MsiException(error);
291 }
292
293 Debug.Assert(20 == hashInterop.FileHashInfoSize);
294
295 hash = new int[4];
296 hash[0] = hashInterop.Data0;
297 hash[1] = hashInterop.Data1;
298 hash[2] = hashInterop.Data2;
299 hash[3] = hashInterop.Data3;
300 }
301
302 /// <summary>
303 /// Returns the version string and language string in the format that the installer
304 /// expects to find them in the database. If you just want version information, set
305 /// lpLangBuf and pcchLangBuf to zero. If you just want language information, set
306 /// lpVersionBuf and pcchVersionBuf to zero.
307 /// </summary>
308 /// <param name="filePath">Specifies the path to the file.</param>
309 /// <param name="version">Returns the file version. Set to 0 for language information only.</param>
310 /// <param name="language">Returns the file language. Set to 0 for version information only.</param>
311 internal static void GetFileVersion(string filePath, out string version, out string language)
312 {
313 int versionLength = 20;
314 int languageLength = 20;
315 StringBuilder versionBuffer = new StringBuilder(versionLength);
316 StringBuilder languageBuffer = new StringBuilder(languageLength);
317
318 int error = MsiInterop.MsiGetFileVersion(filePath, versionBuffer, ref versionLength, languageBuffer, ref languageLength);
319 if (234 == error)
320 {
321 versionBuffer.EnsureCapacity(++versionLength);
322 languageBuffer.EnsureCapacity(++languageLength);
323 error = MsiInterop.MsiGetFileVersion(filePath, versionBuffer, ref versionLength, languageBuffer, ref languageLength);
324 }
325 else if (1006 == error)
326 {
327 // file has no version or language, so no error
328 error = 0;
329 }
330
331 if (0 != error)
332 {
333 throw new MsiException(error);
334 }
335
336 version = versionBuffer.ToString();
337 language = languageBuffer.ToString();
338 }
339
340 /// <summary>
341 /// Enables an external user-interface handler.
342 /// </summary>
343 /// <param name="installUIHandler">Specifies a callback function.</param>
344 /// <param name="messageFilter">Specifies which messages to handle using the external message handler.</param>
345 /// <param name="context">Pointer to an application context that is passed to the callback function.</param>
346 /// <returns>The return value is the previously set external handler, or null if there was no previously set handler.</returns>
347 internal static InstallUIHandler SetExternalUI(InstallUIHandler installUIHandler, int messageFilter, IntPtr context)
348 {
349 return MsiInterop.MsiSetExternalUI(installUIHandler, messageFilter, context);
350 }
351
352 /// <summary>
353 /// Enables the installer's internal user interface.
354 /// </summary>
355 /// <param name="uiLevel">Specifies the level of complexity of the user interface.</param>
356 /// <param name="hwnd">Pointer to a window. This window becomes the owner of any user interface created.</param>
357 /// <returns>The previous user interface level is returned. If an invalid dwUILevel is passed, then INSTALLUILEVEL_NOCHANGE is returned.</returns>
358 internal static int SetInternalUI(int uiLevel, ref IntPtr hwnd)
359 {
360 return MsiInterop.MsiSetInternalUI(uiLevel, ref hwnd);
361 }
362 }
363}
diff --git a/src/WixToolset.Core.WindowsInstaller/Msi/Interop/MsiInterop.cs b/src/WixToolset.Core.WindowsInstaller/Msi/Interop/MsiInterop.cs
new file mode 100644
index 00000000..054289ee
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Msi/Interop/MsiInterop.cs
@@ -0,0 +1,697 @@
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#if false
3namespace WixToolset.Msi.Interop
4{
5 using System;
6 using System.Text;
7 using System.Runtime.InteropServices;
8 using FILETIME = System.Runtime.InteropServices.ComTypes.FILETIME;
9
10 /// <summary>
11 /// A callback function that the installer calls for progress notification and error messages.
12 /// </summary>
13 /// <param name="context">Pointer to an application context.
14 /// This parameter can be used for error checking.</param>
15 /// <param name="messageType">Specifies a combination of one message box style,
16 /// one message box icon type, one default button, and one installation message type.</param>
17 /// <param name="message">Specifies the message text.</param>
18 /// <returns>-1 for an error, 0 if no action was taken, 1 if OK, 3 to abort.</returns>
19 internal delegate int InstallUIHandler(IntPtr context, uint messageType, [MarshalAs(UnmanagedType.LPWStr)] string message);
20
21 /// <summary>
22 /// Class exposing static functions and structs from MSI API.
23 /// </summary>
24 internal sealed class MsiInterop
25 {
26 // Patching constants
27 internal const int MsiMaxStreamNameLength = 62; // http://msdn2.microsoft.com/library/aa370551.aspx
28
29 // Component.Attributes
30 internal const int MsidbComponentAttributesLocalOnly = 0;
31 internal const int MsidbComponentAttributesSourceOnly = 1;
32 internal const int MsidbComponentAttributesOptional = 2;
33 internal const int MsidbComponentAttributesRegistryKeyPath = 4;
34 internal const int MsidbComponentAttributesSharedDllRefCount = 8;
35 internal const int MsidbComponentAttributesPermanent = 16;
36 internal const int MsidbComponentAttributesODBCDataSource = 32;
37 internal const int MsidbComponentAttributesTransitive = 64;
38 internal const int MsidbComponentAttributesNeverOverwrite = 128;
39 internal const int MsidbComponentAttributes64bit = 256;
40 internal const int MsidbComponentAttributesDisableRegistryReflection = 512;
41 internal const int MsidbComponentAttributesUninstallOnSupersedence = 1024;
42 internal const int MsidbComponentAttributesShared = 2048;
43
44 // BBControl.Attributes & Control.Attributes
45 internal const int MsidbControlAttributesVisible = 0x00000001;
46 internal const int MsidbControlAttributesEnabled = 0x00000002;
47 internal const int MsidbControlAttributesSunken = 0x00000004;
48 internal const int MsidbControlAttributesIndirect = 0x00000008;
49 internal const int MsidbControlAttributesInteger = 0x00000010;
50 internal const int MsidbControlAttributesRTLRO = 0x00000020;
51 internal const int MsidbControlAttributesRightAligned = 0x00000040;
52 internal const int MsidbControlAttributesLeftScroll = 0x00000080;
53 internal const int MsidbControlAttributesBiDi = MsidbControlAttributesRTLRO | MsidbControlAttributesRightAligned | MsidbControlAttributesLeftScroll;
54
55 // Text controls
56 internal const int MsidbControlAttributesTransparent = 0x00010000;
57 internal const int MsidbControlAttributesNoPrefix = 0x00020000;
58 internal const int MsidbControlAttributesNoWrap = 0x00040000;
59 internal const int MsidbControlAttributesFormatSize = 0x00080000;
60 internal const int MsidbControlAttributesUsersLanguage = 0x00100000;
61
62 // Edit controls
63 internal const int MsidbControlAttributesMultiline = 0x00010000;
64 internal const int MsidbControlAttributesPasswordInput = 0x00200000;
65
66 // ProgressBar controls
67 internal const int MsidbControlAttributesProgress95 = 0x00010000;
68
69 // VolumeSelectCombo and DirectoryCombo controls
70 internal const int MsidbControlAttributesRemovableVolume = 0x00010000;
71 internal const int MsidbControlAttributesFixedVolume = 0x00020000;
72 internal const int MsidbControlAttributesRemoteVolume = 0x00040000;
73 internal const int MsidbControlAttributesCDROMVolume = 0x00080000;
74 internal const int MsidbControlAttributesRAMDiskVolume = 0x00100000;
75 internal const int MsidbControlAttributesFloppyVolume = 0x00200000;
76
77 // VolumeCostList controls
78 internal const int MsidbControlShowRollbackCost = 0x00400000;
79
80 // ListBox and ComboBox controls
81 internal const int MsidbControlAttributesSorted = 0x00010000;
82 internal const int MsidbControlAttributesComboList = 0x00020000;
83
84 // picture button controls
85 internal const int MsidbControlAttributesImageHandle = 0x00010000;
86 internal const int MsidbControlAttributesPushLike = 0x00020000;
87 internal const int MsidbControlAttributesBitmap = 0x00040000;
88 internal const int MsidbControlAttributesIcon = 0x00080000;
89 internal const int MsidbControlAttributesFixedSize = 0x00100000;
90 internal const int MsidbControlAttributesIconSize16 = 0x00200000;
91 internal const int MsidbControlAttributesIconSize32 = 0x00400000;
92 internal const int MsidbControlAttributesIconSize48 = 0x00600000;
93 internal const int MsidbControlAttributesElevationShield = 0x00800000;
94
95 // RadioButton controls
96 internal const int MsidbControlAttributesHasBorder = 0x01000000;
97
98 // CustomAction.Type
99 // executable types
100 internal const int MsidbCustomActionTypeDll = 0x00000001; // Target = entry point name
101 internal const int MsidbCustomActionTypeExe = 0x00000002; // Target = command line args
102 internal const int MsidbCustomActionTypeTextData = 0x00000003; // Target = text string to be formatted and set into property
103 internal const int MsidbCustomActionTypeJScript = 0x00000005; // Target = entry point name; null if none to call
104 internal const int MsidbCustomActionTypeVBScript = 0x00000006; // Target = entry point name; null if none to call
105 internal const int MsidbCustomActionTypeInstall = 0x00000007; // Target = property list for nested engine initialization
106 internal const int MsidbCustomActionTypeSourceBits = 0x00000030;
107 internal const int MsidbCustomActionTypeTargetBits = 0x00000007;
108 internal const int MsidbCustomActionTypeReturnBits = 0x000000C0;
109 internal const int MsidbCustomActionTypeExecuteBits = 0x00000700;
110
111 // source of code
112 internal const int MsidbCustomActionTypeBinaryData = 0x00000000; // Source = Binary.Name; data stored in stream
113 internal const int MsidbCustomActionTypeSourceFile = 0x00000010; // Source = File.File; file part of installation
114 internal const int MsidbCustomActionTypeDirectory = 0x00000020; // Source = Directory.Directory; folder containing existing file
115 internal const int MsidbCustomActionTypeProperty = 0x00000030; // Source = Property.Property; full path to executable
116
117 // return processing; default is syncronous execution; process return code
118 internal const int MsidbCustomActionTypeContinue = 0x00000040; // ignore action return status; continue running
119 internal const int MsidbCustomActionTypeAsync = 0x00000080; // run asynchronously
120
121 // execution scheduling flags; default is execute whenever sequenced
122 internal const int MsidbCustomActionTypeFirstSequence = 0x00000100; // skip if UI sequence already run
123 internal const int MsidbCustomActionTypeOncePerProcess = 0x00000200; // skip if UI sequence already run in same process
124 internal const int MsidbCustomActionTypeClientRepeat = 0x00000300; // run on client only if UI already run on client
125 internal const int MsidbCustomActionTypeInScript = 0x00000400; // queue for execution within script
126 internal const int MsidbCustomActionTypeRollback = 0x00000100; // in conjunction with InScript: queue in Rollback script
127 internal const int MsidbCustomActionTypeCommit = 0x00000200; // in conjunction with InScript: run Commit ops from script on success
128
129 // security context flag; default to impersonate as user; valid only if InScript
130 internal const int MsidbCustomActionTypeNoImpersonate = 0x00000800; // no impersonation; run in system context
131 internal const int MsidbCustomActionTypeTSAware = 0x00004000; // impersonate for per-machine installs on TS machines
132 internal const int MsidbCustomActionType64BitScript = 0x00001000; // script should run in 64bit process
133 internal const int MsidbCustomActionTypeHideTarget = 0x00002000; // don't record the contents of the Target field in the log file.
134
135 internal const int MsidbCustomActionTypePatchUninstall = 0x00008000; // run on patch uninstall
136
137 // Dialog.Attributes
138 internal const int MsidbDialogAttributesVisible = 0x00000001;
139 internal const int MsidbDialogAttributesModal = 0x00000002;
140 internal const int MsidbDialogAttributesMinimize = 0x00000004;
141 internal const int MsidbDialogAttributesSysModal = 0x00000008;
142 internal const int MsidbDialogAttributesKeepModeless = 0x00000010;
143 internal const int MsidbDialogAttributesTrackDiskSpace = 0x00000020;
144 internal const int MsidbDialogAttributesUseCustomPalette = 0x00000040;
145 internal const int MsidbDialogAttributesRTLRO = 0x00000080;
146 internal const int MsidbDialogAttributesRightAligned = 0x00000100;
147 internal const int MsidbDialogAttributesLeftScroll = 0x00000200;
148 internal const int MsidbDialogAttributesBiDi = MsidbDialogAttributesRTLRO | MsidbDialogAttributesRightAligned | MsidbDialogAttributesLeftScroll;
149 internal const int MsidbDialogAttributesError = 0x00010000;
150 internal const int CommonControlAttributesInvert = MsidbControlAttributesVisible + MsidbControlAttributesEnabled;
151 internal const int DialogAttributesInvert = MsidbDialogAttributesVisible + MsidbDialogAttributesModal + MsidbDialogAttributesMinimize;
152
153 // Feature.Attributes
154 internal const int MsidbFeatureAttributesFavorLocal = 0;
155 internal const int MsidbFeatureAttributesFavorSource = 1;
156 internal const int MsidbFeatureAttributesFollowParent = 2;
157 internal const int MsidbFeatureAttributesFavorAdvertise = 4;
158 internal const int MsidbFeatureAttributesDisallowAdvertise = 8;
159 internal const int MsidbFeatureAttributesUIDisallowAbsent = 16;
160 internal const int MsidbFeatureAttributesNoUnsupportedAdvertise = 32;
161
162 // File.Attributes
163 internal const int MsidbFileAttributesReadOnly = 1;
164 internal const int MsidbFileAttributesHidden = 2;
165 internal const int MsidbFileAttributesSystem = 4;
166 internal const int MsidbFileAttributesVital = 512;
167 internal const int MsidbFileAttributesChecksum = 1024;
168 internal const int MsidbFileAttributesPatchAdded = 4096;
169 internal const int MsidbFileAttributesNoncompressed = 8192;
170 internal const int MsidbFileAttributesCompressed = 16384;
171
172 // IniFile.Action & RemoveIniFile.Action
173 internal const int MsidbIniFileActionAddLine = 0;
174 internal const int MsidbIniFileActionCreateLine = 1;
175 internal const int MsidbIniFileActionRemoveLine = 2;
176 internal const int MsidbIniFileActionAddTag = 3;
177 internal const int MsidbIniFileActionRemoveTag = 4;
178
179 // MoveFile.Options
180 internal const int MsidbMoveFileOptionsMove = 1;
181
182 // ServiceInstall.Attributes
183 internal const int MsidbServiceInstallOwnProcess = 0x00000010;
184 internal const int MsidbServiceInstallShareProcess = 0x00000020;
185 internal const int MsidbServiceInstallInteractive = 0x00000100;
186 internal const int MsidbServiceInstallAutoStart = 0x00000002;
187 internal const int MsidbServiceInstallDemandStart = 0x00000003;
188 internal const int MsidbServiceInstallDisabled = 0x00000004;
189 internal const int MsidbServiceInstallErrorIgnore = 0x00000000;
190 internal const int MsidbServiceInstallErrorNormal = 0x00000001;
191 internal const int MsidbServiceInstallErrorCritical = 0x00000003;
192 internal const int MsidbServiceInstallErrorControlVital = 0x00008000;
193
194 // ServiceConfig.Event
195 internal const int MsidbServiceConfigEventInstall = 0x00000001;
196 internal const int MsidbServiceConfigEventUninstall = 0x00000002;
197 internal const int MsidbServiceConfigEventReinstall = 0x00000004;
198
199 // ServiceControl.Attributes
200 internal const int MsidbServiceControlEventStart = 0x00000001;
201 internal const int MsidbServiceControlEventStop = 0x00000002;
202 internal const int MsidbServiceControlEventDelete = 0x00000008;
203 internal const int MsidbServiceControlEventUninstallStart = 0x00000010;
204 internal const int MsidbServiceControlEventUninstallStop = 0x00000020;
205 internal const int MsidbServiceControlEventUninstallDelete = 0x00000080;
206
207 // TextStyle.StyleBits
208 internal const int MsidbTextStyleStyleBitsBold = 1;
209 internal const int MsidbTextStyleStyleBitsItalic = 2;
210 internal const int MsidbTextStyleStyleBitsUnderline = 4;
211 internal const int MsidbTextStyleStyleBitsStrike = 8;
212
213 // Upgrade.Attributes
214 internal const int MsidbUpgradeAttributesMigrateFeatures = 0x00000001;
215 internal const int MsidbUpgradeAttributesOnlyDetect = 0x00000002;
216 internal const int MsidbUpgradeAttributesIgnoreRemoveFailure = 0x00000004;
217 internal const int MsidbUpgradeAttributesVersionMinInclusive = 0x00000100;
218 internal const int MsidbUpgradeAttributesVersionMaxInclusive = 0x00000200;
219 internal const int MsidbUpgradeAttributesLanguagesExclusive = 0x00000400;
220
221 // Registry Hive Roots
222 internal const int MsidbRegistryRootClassesRoot = 0;
223 internal const int MsidbRegistryRootCurrentUser = 1;
224 internal const int MsidbRegistryRootLocalMachine = 2;
225 internal const int MsidbRegistryRootUsers = 3;
226
227 // Locator Types
228 internal const int MsidbLocatorTypeDirectory = 0;
229 internal const int MsidbLocatorTypeFileName = 1;
230 internal const int MsidbLocatorTypeRawValue = 2;
231 internal const int MsidbLocatorType64bit = 16;
232
233 internal const int MsidbClassAttributesRelativePath = 1;
234
235 // RemoveFile.InstallMode
236 internal const int MsidbRemoveFileInstallModeOnInstall = 0x00000001;
237 internal const int MsidbRemoveFileInstallModeOnRemove = 0x00000002;
238 internal const int MsidbRemoveFileInstallModeOnBoth = 0x00000003;
239
240 // ODBCDataSource.Registration
241 internal const int MsidbODBCDataSourceRegistrationPerMachine = 0;
242 internal const int MsidbODBCDataSourceRegistrationPerUser = 1;
243
244 // ModuleConfiguration.Format
245 internal const int MsidbModuleConfigurationFormatText = 0;
246 internal const int MsidbModuleConfigurationFormatKey = 1;
247 internal const int MsidbModuleConfigurationFormatInteger = 2;
248 internal const int MsidbModuleConfigurationFormatBitfield = 3;
249
250 // ModuleConfiguration.Attributes
251 internal const int MsidbMsmConfigurableOptionKeyNoOrphan = 1;
252 internal const int MsidbMsmConfigurableOptionNonNullable = 2;
253
254 // ' Windows API function ShowWindow constants - used in Shortcut table
255 internal const int SWSHOWNORMAL = 0x00000001;
256 internal const int SWSHOWMAXIMIZED = 0x00000003;
257 internal const int SWSHOWMINNOACTIVE = 0x00000007;
258
259 // NameToBit arrays
260 // UI elements
261 internal static readonly string[] CommonControlAttributes = { "Hidden", "Disabled", "Sunken", "Indirect", "Integer", "RightToLeft", "RightAligned", "LeftScroll" };
262 internal static readonly string[] TextControlAttributes = { "Transparent", "NoPrefix", "NoWrap", "FormatSize", "UserLanguage" };
263 internal static readonly string[] HyperlinkControlAttributes = { "Transparent" };
264 internal static readonly string[] EditControlAttributes = { "Multiline", null, null, null, null, "Password" };
265 internal static readonly string[] ProgressControlAttributes = { "ProgressBlocks" };
266 internal static readonly string[] VolumeControlAttributes = { "Removable", "Fixed", "Remote", "CDROM", "RAMDisk", "Floppy", "ShowRollbackCost" };
267 internal static readonly string[] ListboxControlAttributes = { "Sorted", null, null, null, "UserLanguage" };
268 internal static readonly string[] ListviewControlAttributes = { "Sorted", null, null, null, "FixedSize", "Icon16", "Icon32" };
269 internal static readonly string[] ComboboxControlAttributes = { "Sorted", "ComboList", null, null, "UserLanguage" };
270 internal static readonly string[] RadioControlAttributes = { "Image", "PushLike", "Bitmap", "Icon", "FixedSize", "Icon16", "Icon32", null, "HasBorder" };
271 internal static readonly string[] ButtonControlAttributes = { "Image", null, "Bitmap", "Icon", "FixedSize", "Icon16", "Icon32", "ElevationShield" };
272 internal static readonly string[] IconControlAttributes = { "Image", null, null, null, "FixedSize", "Icon16", "Icon32" };
273 internal static readonly string[] BitmapControlAttributes = { "Image", null, null, null, "FixedSize" };
274 internal static readonly string[] CheckboxControlAttributes = { null, "PushLike", "Bitmap", "Icon", "FixedSize", "Icon16", "Icon32" };
275
276 internal const int MsidbEmbeddedUI = 0x01;
277 internal const int MsidbEmbeddedHandlesBasic = 0x02;
278
279 internal const int INSTALLLOGMODE_FATALEXIT = 0x00001;
280 internal const int INSTALLLOGMODE_ERROR = 0x00002;
281 internal const int INSTALLLOGMODE_WARNING = 0x00004;
282 internal const int INSTALLLOGMODE_USER = 0x00008;
283 internal const int INSTALLLOGMODE_INFO = 0x00010;
284 internal const int INSTALLLOGMODE_FILESINUSE = 0x00020;
285 internal const int INSTALLLOGMODE_RESOLVESOURCE = 0x00040;
286 internal const int INSTALLLOGMODE_OUTOFDISKSPACE = 0x00080;
287 internal const int INSTALLLOGMODE_ACTIONSTART = 0x00100;
288 internal const int INSTALLLOGMODE_ACTIONDATA = 0x00200;
289 internal const int INSTALLLOGMODE_PROGRESS = 0x00400;
290 internal const int INSTALLLOGMODE_COMMONDATA = 0x00800;
291 internal const int INSTALLLOGMODE_INITIALIZE = 0x01000;
292 internal const int INSTALLLOGMODE_TERMINATE = 0x02000;
293 internal const int INSTALLLOGMODE_SHOWDIALOG = 0x04000;
294 internal const int INSTALLLOGMODE_RMFILESINUSE = 0x02000000;
295 internal const int INSTALLLOGMODE_INSTALLSTART = 0x04000000;
296 internal const int INSTALLLOGMODE_INSTALLEND = 0x08000000;
297
298 internal const int MSICONDITIONFALSE = 0; // The table is temporary.
299 internal const int MSICONDITIONTRUE = 1; // The table is persistent.
300 internal const int MSICONDITIONNONE = 2; // The table is unknown.
301 internal const int MSICONDITIONERROR = 3; // An invalid handle or invalid parameter was passed to the function.
302
303 internal const int MSIDBOPENREADONLY = 0;
304 internal const int MSIDBOPENTRANSACT = 1;
305 internal const int MSIDBOPENDIRECT = 2;
306 internal const int MSIDBOPENCREATE = 3;
307 internal const int MSIDBOPENCREATEDIRECT = 4;
308 internal const int MSIDBOPENPATCHFILE = 32;
309
310 internal const int MSIMODIFYSEEK = -1; // Refreshes the information in the supplied record without changing the position in the result set and without affecting subsequent fetch operations. The record may then be used for subsequent Update, Delete, and Refresh. All primary key columns of the table must be in the query and the record must have at least as many fields as the query. Seek cannot be used with multi-table queries. This mode cannot be used with a view containing joins. See also the remarks.
311 internal const int MSIMODIFYREFRESH = 0; // Refreshes the information in the record. Must first call MsiViewFetch with the same record. Fails for a deleted row. Works with read-write and read-only records.
312 internal const int MSIMODIFYINSERT = 1; // Inserts a record. Fails if a row with the same primary keys exists. Fails with a read-only database. This mode cannot be used with a view containing joins.
313 internal const int MSIMODIFYUPDATE = 2; // Updates an existing record. Nonprimary keys only. Must first call MsiViewFetch. Fails with a deleted record. Works only with read-write records.
314 internal const int MSIMODIFYASSIGN = 3; // Writes current data in the cursor to a table row. Updates record if the primary keys match an existing row and inserts if they do not match. Fails with a read-only database. This mode cannot be used with a view containing joins.
315 internal const int MSIMODIFYREPLACE = 4; // Updates or deletes and inserts a record into a table. Must first call MsiViewFetch with the same record. Updates record if the primary keys are unchanged. Deletes old row and inserts new if primary keys have changed. Fails with a read-only database. This mode cannot be used with a view containing joins.
316 internal const int MSIMODIFYMERGE = 5; // Inserts or validates a record in a table. Inserts if primary keys do not match any row and validates if there is a match. Fails if the record does not match the data in the table. Fails if there is a record with a duplicate key that is not identical. Works only with read-write records. This mode cannot be used with a view containing joins.
317 internal const int MSIMODIFYDELETE = 6; // Remove a row from the table. You must first call the MsiViewFetch function with the same record. Fails if the row has been deleted. Works only with read-write records. This mode cannot be used with a view containing joins.
318 internal const int MSIMODIFYINSERTTEMPORARY = 7; // Inserts a temporary record. The information is not persistent. Fails if a row with the same primary key exists. Works only with read-write records. This mode cannot be used with a view containing joins.
319 internal const int MSIMODIFYVALIDATE = 8; // Validates a record. Does not validate across joins. You must first call the MsiViewFetch function with the same record. Obtain validation errors with MsiViewGetError. Works with read-write and read-only records. This mode cannot be used with a view containing joins.
320 internal const int MSIMODIFYVALIDATENEW = 9; // Validate a new record. Does not validate across joins. Checks for duplicate keys. Obtain validation errors by calling MsiViewGetError. Works with read-write and read-only records. This mode cannot be used with a view containing joins.
321 internal const int MSIMODIFYVALIDATEFIELD = 10; // Validates fields of a fetched or new record. Can validate one or more fields of an incomplete record. Obtain validation errors by calling MsiViewGetError. Works with read-write and read-only records. This mode cannot be used with a view containing joins.
322 internal const int MSIMODIFYVALIDATEDELETE = 11; // Validates a record that will be deleted later. You must first call MsiViewFetch. Fails if another row refers to the primary keys of this row. Validation does not check for the existence of the primary keys of this row in properties or strings. Does not check if a column is a foreign key to multiple tables. Obtain validation errors by calling MsiViewGetError. Works with read-write and read-only records. This mode cannot be used with a view containing joins.
323
324 internal const uint VTI2 = 2;
325 internal const uint VTI4 = 3;
326 internal const uint VTLPWSTR = 30;
327 internal const uint VTFILETIME = 64;
328
329 internal const int MSICOLINFONAMES = 0; // return column names
330 internal const int MSICOLINFOTYPES = 1; // return column definitions, datatype code followed by width
331
332 /// <summary>
333 /// Protect the constructor.
334 /// </summary>
335 private MsiInterop()
336 {
337 }
338
339 /// <summary>
340 /// PInvoke of MsiCloseHandle.
341 /// </summary>
342 /// <param name="database">Handle to a database.</param>
343 /// <returns>Error code.</returns>
344 [DllImport("msi.dll", EntryPoint = "MsiCloseHandle", CharSet = CharSet.Unicode, ExactSpelling = true)]
345 internal static extern int MsiCloseHandle(uint database);
346
347 /// <summary>
348 /// PInvoke of MsiCreateRecord
349 /// </summary>
350 /// <param name="parameters">Count of columns in the record.</param>
351 /// <returns>Handle referencing the record.</returns>
352 [DllImport("msi.dll", EntryPoint = "MsiCreateRecord", CharSet = CharSet.Unicode, ExactSpelling = true)]
353 internal static extern uint MsiCreateRecord(int parameters);
354
355 /// <summary>
356 /// Creates summary information of an existing transform to include validation and error conditions.
357 /// </summary>
358 /// <param name="database">The handle to the database that contains the new database summary information.</param>
359 /// <param name="referenceDatabase">The handle to the database that contains the original summary information.</param>
360 /// <param name="transformFile">The name of the transform to which the summary information is added.</param>
361 /// <param name="errorConditions">The error conditions that should be suppressed when the transform is applied.</param>
362 /// <param name="validations">Specifies the properties to be validated to verify that the transform can be applied to the database.</param>
363 /// <returns>Error code.</returns>
364 [DllImport("msi.dll", EntryPoint = "MsiCreateTransformSummaryInfoW", CharSet = CharSet.Unicode, ExactSpelling = true)]
365 internal static extern int MsiCreateTransformSummaryInfo(uint database, uint referenceDatabase, string transformFile, TransformErrorConditions errorConditions, TransformValidations validations);
366
367 /// <summary>
368 /// Applies a transform to a database.
369 /// </summary>
370 /// <param name="database">Handle to the database obtained from MsiOpenDatabase to transform.</param>
371 /// <param name="transformFile">Specifies the name of the transform file to apply.</param>
372 /// <param name="errorConditions">Error conditions that should be suppressed.</param>
373 /// <returns>Error code.</returns>
374 [DllImport("msi.dll", EntryPoint = "MsiDatabaseApplyTransformW", CharSet = CharSet.Unicode, ExactSpelling = true)]
375 internal static extern int MsiDatabaseApplyTransform(uint database, string transformFile, TransformErrorConditions errorConditions);
376
377 /// <summary>
378 /// PInvoke of MsiDatabaseCommit.
379 /// </summary>
380 /// <param name="database">Handle to a databse.</param>
381 /// <returns>Error code.</returns>
382 [DllImport("msi.dll", EntryPoint = "MsiDatabaseCommit", CharSet = CharSet.Unicode, ExactSpelling = true)]
383 internal static extern int MsiDatabaseCommit(uint database);
384
385 /// <summary>
386 /// PInvoke of MsiDatabaseExportW.
387 /// </summary>
388 /// <param name="database">Handle to a database.</param>
389 /// <param name="tableName">Table name.</param>
390 /// <param name="folderPath">Folder path.</param>
391 /// <param name="fileName">File name.</param>
392 /// <returns>Error code.</returns>
393 [DllImport("msi.dll", EntryPoint = "MsiDatabaseExportW", CharSet = CharSet.Unicode, ExactSpelling = true)]
394 internal static extern int MsiDatabaseExport(uint database, string tableName, string folderPath, string fileName);
395
396 /// <summary>
397 /// Generates a transform file of differences between two databases.
398 /// </summary>
399 /// <param name="database">Handle to the database obtained from MsiOpenDatabase that includes the changes.</param>
400 /// <param name="databaseReference">Handle to the database obtained from MsiOpenDatabase that does not include the changes.</param>
401 /// <param name="transformFile">A null-terminated string that specifies the name of the transform file being generated.
402 /// This parameter can be null. If szTransformFile is null, you can use MsiDatabaseGenerateTransform to test whether two
403 /// databases are identical without creating a transform. If the databases are identical, the function returns ERROR_NO_DATA.
404 /// If the databases are different the function returns NOERROR.</param>
405 /// <param name="reserved1">This is a reserved argument and must be set to 0.</param>
406 /// <param name="reserved2">This is a reserved argument and must be set to 0.</param>
407 /// <returns>Error code.</returns>
408 [DllImport("msi.dll", EntryPoint = "MsiDatabaseGenerateTransformW", CharSet = CharSet.Unicode, ExactSpelling = true)]
409 internal static extern int MsiDatabaseGenerateTransform(uint database, uint databaseReference, string transformFile, int reserved1, int reserved2);
410
411 /// <summary>
412 /// PInvoke of MsiDatabaseImportW.
413 /// </summary>
414 /// <param name="database">Handle to a database.</param>
415 /// <param name="folderPath">Folder path.</param>
416 /// <param name="fileName">File name.</param>
417 /// <returns>Error code.</returns>
418 [DllImport("msi.dll", EntryPoint = "MsiDatabaseImportW", CharSet = CharSet.Unicode, ExactSpelling = true)]
419 internal static extern int MsiDatabaseImport(uint database, string folderPath, string fileName);
420
421 /// <summary>
422 /// PInvoke of MsiDatabaseMergeW.
423 /// </summary>
424 /// <param name="database">The handle to the database obtained from MsiOpenDatabase.</param>
425 /// <param name="databaseMerge">The handle to the database obtained from MsiOpenDatabase to merge into the base database.</param>
426 /// <param name="tableName">The name of the table to receive merge conflict information.</param>
427 /// <returns>Error code.</returns>
428 [DllImport("msi.dll", EntryPoint = "MsiDatabaseMergeW", CharSet = CharSet.Unicode, ExactSpelling = true)]
429 internal static extern int MsiDatabaseMerge(uint database, uint databaseMerge, string tableName);
430
431 /// <summary>
432 /// PInvoke of MsiDatabaseOpenViewW.
433 /// </summary>
434 /// <param name="database">Handle to a database.</param>
435 /// <param name="query">SQL query.</param>
436 /// <param name="view">View handle.</param>
437 /// <returns>Error code.</returns>
438 [DllImport("msi.dll", EntryPoint = "MsiDatabaseOpenViewW", CharSet = CharSet.Unicode, ExactSpelling = true)]
439 internal static extern int MsiDatabaseOpenView(uint database, string query, out uint view);
440
441 /// <summary>
442 /// PInvoke of MsiGetFileHashW.
443 /// </summary>
444 /// <param name="filePath">File path.</param>
445 /// <param name="options">Hash options (must be 0).</param>
446 /// <param name="hash">Buffer to recieve hash.</param>
447 /// <returns>Error code.</returns>
448 [DllImport("msi.dll", EntryPoint = "MsiGetFileHashW", CharSet = CharSet.Unicode, ExactSpelling = true)]
449 internal static extern int MsiGetFileHash(string filePath, uint options, MSIFILEHASHINFO hash);
450
451 /// <summary>
452 /// PInvoke of MsiGetFileVersionW.
453 /// </summary>
454 /// <param name="filePath">File path.</param>
455 /// <param name="versionBuf">Buffer to receive version info.</param>
456 /// <param name="versionBufSize">Size of version buffer.</param>
457 /// <param name="langBuf">Buffer to recieve lang info.</param>
458 /// <param name="langBufSize">Size of lang buffer.</param>
459 /// <returns>Error code.</returns>
460 [DllImport("msi.dll", EntryPoint = "MsiGetFileVersionW", CharSet = CharSet.Unicode, ExactSpelling = true)]
461 internal static extern int MsiGetFileVersion(string filePath, StringBuilder versionBuf, ref int versionBufSize, StringBuilder langBuf, ref int langBufSize);
462
463 /// <summary>
464 /// PInvoke of MsiGetLastErrorRecord.
465 /// </summary>
466 /// <returns>Handle to error record if one exists.</returns>
467 [DllImport("msi.dll", EntryPoint = "MsiGetLastErrorRecord", CharSet = CharSet.Unicode, ExactSpelling = true)]
468 internal static extern uint MsiGetLastErrorRecord();
469
470 /// <summary>
471 /// PInvoke of MsiDatabaseGetPrimaryKeysW.
472 /// </summary>
473 /// <param name="database">Handle to a database.</param>
474 /// <param name="tableName">Table name.</param>
475 /// <param name="record">Handle to receive resulting record.</param>
476 /// <returns>Error code.</returns>
477 [DllImport("msi.dll", EntryPoint = "MsiDatabaseGetPrimaryKeysW", CharSet = CharSet.Unicode, ExactSpelling = true)]
478 internal static extern int MsiDatabaseGetPrimaryKeys(uint database, string tableName, out uint record);
479
480 /// <summary>
481 /// PInvoke of MsiDoActionW.
482 /// </summary>
483 /// <param name="product">Handle to the installation provided to a DLL custom action or
484 /// obtained through MsiOpenPackage, MsiOpenPackageEx, or MsiOpenProduct.</param>
485 /// <param name="action">Specifies the action to execute.</param>
486 /// <returns>Error code.</returns>
487 [DllImport("msi.dll", EntryPoint = "MsiDoActionW", CharSet = CharSet.Unicode, ExactSpelling = true)]
488 internal static extern int MsiDoAction(uint product, string action);
489
490 /// <summary>
491 /// PInvoke of MsiGetSummaryInformationW. Can use either database handle or database path as input.
492 /// </summary>
493 /// <param name="database">Handle to a database.</param>
494 /// <param name="databasePath">Path to a database.</param>
495 /// <param name="updateCount">Max number of updated values.</param>
496 /// <param name="summaryInfo">Handle to summary information.</param>
497 /// <returns>Error code.</returns>
498 [DllImport("msi.dll", EntryPoint = "MsiGetSummaryInformationW", CharSet = CharSet.Unicode, ExactSpelling = true)]
499 internal static extern int MsiGetSummaryInformation(uint database, string databasePath, uint updateCount, ref uint summaryInfo);
500
501 /// <summary>
502 /// PInvoke of MsiDatabaseIsTablePersitentW.
503 /// </summary>
504 /// <param name="database">Handle to a database.</param>
505 /// <param name="tableName">Table name.</param>
506 /// <returns>MSICONDITION</returns>
507 [DllImport("msi.dll", EntryPoint = "MsiDatabaseIsTablePersistentW", CharSet = CharSet.Unicode, ExactSpelling = true)]
508 internal static extern int MsiDatabaseIsTablePersistent(uint database, string tableName);
509
510 /// <summary>
511 /// PInvoke of MsiOpenDatabaseW.
512 /// </summary>
513 /// <param name="databasePath">Path to database.</param>
514 /// <param name="persist">Persist mode.</param>
515 /// <param name="database">Handle to database.</param>
516 /// <returns>Error code.</returns>
517 [DllImport("msi.dll", EntryPoint = "MsiOpenDatabaseW", CharSet = CharSet.Unicode, ExactSpelling = true)]
518 internal static extern int MsiOpenDatabase(string databasePath, IntPtr persist, out uint database);
519
520 /// <summary>
521 /// PInvoke of MsiOpenPackageW.
522 /// </summary>
523 /// <param name="packagePath">The path to the package.</param>
524 /// <param name="product">A pointer to a variable that receives the product handle.</param>
525 /// <returns>Error code.</returns>
526 [DllImport("msi.dll", EntryPoint = "MsiOpenPackageW", CharSet = CharSet.Unicode, ExactSpelling = true)]
527 internal static extern int MsiOpenPackage(string packagePath, out uint product);
528
529 /// <summary>
530 /// PInvoke of MsiRecordIsNull.
531 /// </summary>
532 /// <param name="record">MSI Record handle.</param>
533 /// <param name="field">Index of field to check for null value.</param>
534 /// <returns>true if the field is null, false if not, and an error code for any error.</returns>
535 [DllImport("msi.dll", EntryPoint = "MsiRecordIsNull", CharSet = CharSet.Unicode, ExactSpelling = true)]
536 internal static extern int MsiRecordIsNull(uint record, int field);
537
538 /// <summary>
539 /// PInvoke of MsiRecordGetInteger.
540 /// </summary>
541 /// <param name="record">MSI Record handle.</param>
542 /// <param name="field">Index of field to retrieve integer from.</param>
543 /// <returns>Integer value.</returns>
544 [DllImport("msi.dll", EntryPoint = "MsiRecordGetInteger", CharSet = CharSet.Unicode, ExactSpelling = true)]
545 internal static extern int MsiRecordGetInteger(uint record, int field);
546
547 /// <summary>
548 /// PInvoke of MsiRectordSetInteger.
549 /// </summary>
550 /// <param name="record">MSI Record handle.</param>
551 /// <param name="field">Index of field to set integer value in.</param>
552 /// <param name="value">Value to set field to.</param>
553 /// <returns>Error code.</returns>
554 [DllImport("msi.dll", EntryPoint = "MsiRecordSetInteger", CharSet = CharSet.Unicode, ExactSpelling = true)]
555 internal static extern int MsiRecordSetInteger(uint record, int field, int value);
556
557 /// <summary>
558 /// PInvoke of MsiRecordGetStringW.
559 /// </summary>
560 /// <param name="record">MSI Record handle.</param>
561 /// <param name="field">Index of field to get string value from.</param>
562 /// <param name="valueBuf">Buffer to recieve value.</param>
563 /// <param name="valueBufSize">Size of buffer.</param>
564 /// <returns>Error code.</returns>
565 [DllImport("msi.dll", EntryPoint = "MsiRecordGetStringW", CharSet = CharSet.Unicode, ExactSpelling = true)]
566 internal static extern int MsiRecordGetString(uint record, int field, StringBuilder valueBuf, ref int valueBufSize);
567
568 /// <summary>
569 /// PInvoke of MsiRecordSetStringW.
570 /// </summary>
571 /// <param name="record">MSI Record handle.</param>
572 /// <param name="field">Index of field to set string value in.</param>
573 /// <param name="value">String value.</param>
574 /// <returns>Error code.</returns>
575 [DllImport("msi.dll", EntryPoint = "MsiRecordSetStringW", CharSet = CharSet.Unicode, ExactSpelling = true)]
576 internal static extern int MsiRecordSetString(uint record, int field, string value);
577
578 /// <summary>
579 /// PInvoke of MsiRecordSetStreamW.
580 /// </summary>
581 /// <param name="record">MSI Record handle.</param>
582 /// <param name="field">Index of field to set stream value in.</param>
583 /// <param name="filePath">Path to file to set stream value to.</param>
584 /// <returns>Error code.</returns>
585 [DllImport("msi.dll", EntryPoint = "MsiRecordSetStreamW", CharSet = CharSet.Unicode, ExactSpelling = true)]
586 internal static extern int MsiRecordSetStream(uint record, int field, string filePath);
587
588 /// <summary>
589 /// PInvoke of MsiRecordReadStreamW.
590 /// </summary>
591 /// <param name="record">MSI Record handle.</param>
592 /// <param name="field">Index of field to read stream from.</param>
593 /// <param name="dataBuf">Data buffer to recieve stream value.</param>
594 /// <param name="dataBufSize">Size of data buffer.</param>
595 /// <returns>Error code.</returns>
596 [DllImport("msi.dll", EntryPoint = "MsiRecordReadStream", CharSet = CharSet.Unicode, ExactSpelling = true)]
597 internal static extern int MsiRecordReadStream(uint record, int field, byte[] dataBuf, ref int dataBufSize);
598
599 /// <summary>
600 /// PInvoke of MsiRecordGetFieldCount.
601 /// </summary>
602 /// <param name="record">MSI Record handle.</param>
603 /// <returns>Count of fields in the record.</returns>
604 [DllImport("msi.dll", EntryPoint = "MsiRecordGetFieldCount", CharSet = CharSet.Unicode, ExactSpelling = true)]
605 internal static extern int MsiRecordGetFieldCount(uint record);
606
607 /// <summary>
608 /// PInvoke of MsiSetExternalUIW.
609 /// </summary>
610 /// <param name="installUIHandler">Specifies a callback function that conforms to the INSTALLUI_HANDLER specification.</param>
611 /// <param name="installLogMode">Specifies which messages to handle using the external message handler. If the external
612 /// handler returns a non-zero result, then that message will not be sent to the UI, instead the message will be logged
613 /// if logging has been enabled.</param>
614 /// <param name="context">Pointer to an application context that is passed to the callback function.
615 /// This parameter can be used for error checking.</param>
616 /// <returns>The return value is the previously set external handler, or zero (0) if there was no previously set handler.</returns>
617 [DllImport("msi.dll", EntryPoint = "MsiSetExternalUIW", CharSet = CharSet.Unicode, ExactSpelling = true)]
618 internal static extern InstallUIHandler MsiSetExternalUI(InstallUIHandler installUIHandler, int installLogMode, IntPtr context);
619
620 /// <summary>
621 /// PInvoke of MsiSetInternalUI.
622 /// </summary>
623 /// <param name="uiLevel">Specifies the level of complexity of the user interface.</param>
624 /// <param name="hwnd">Pointer to a window. This window becomes the owner of any user interface created.
625 /// A pointer to the previous owner of the user interface is returned.
626 /// If this parameter is null, the owner of the user interface does not change.</param>
627 /// <returns>The previous user interface level is returned. If an invalid dwUILevel is passed, then INSTALLUILEVEL_NOCHANGE is returned.</returns>
628 [DllImport("msi.dll", EntryPoint = "MsiSetInternalUI", CharSet = CharSet.Unicode, ExactSpelling = true)]
629 internal static extern int MsiSetInternalUI(int uiLevel, ref IntPtr hwnd);
630
631 /// <summary>
632 /// PInvoke of MsiSummaryInfoGetPropertyW.
633 /// </summary>
634 /// <param name="summaryInfo">Handle to summary info.</param>
635 /// <param name="property">Property to get value from.</param>
636 /// <param name="dataType">Data type of property.</param>
637 /// <param name="integerValue">Integer to receive integer value.</param>
638 /// <param name="fileTimeValue">File time to receive file time value.</param>
639 /// <param name="stringValueBuf">String buffer to receive string value.</param>
640 /// <param name="stringValueBufSize">Size of string buffer.</param>
641 /// <returns>Error code.</returns>
642 [DllImport("msi.dll", EntryPoint = "MsiSummaryInfoGetPropertyW", CharSet = CharSet.Unicode, ExactSpelling = true)]
643 internal static extern int MsiSummaryInfoGetProperty(uint summaryInfo, int property, out uint dataType, out int integerValue, ref FILETIME fileTimeValue, StringBuilder stringValueBuf, ref int stringValueBufSize);
644
645 /// <summary>
646 /// PInvoke of MsiViewGetColumnInfo.
647 /// </summary>
648 /// <param name="view">Handle to view.</param>
649 /// <param name="columnInfo">Column info.</param>
650 /// <param name="record">Handle for returned record.</param>
651 /// <returns>Error code.</returns>
652 [DllImport("msi.dll", EntryPoint = "MsiViewGetColumnInfo", CharSet = CharSet.Unicode, ExactSpelling = true)]
653 internal static extern int MsiViewGetColumnInfo(uint view, int columnInfo, out uint record);
654
655 /// <summary>
656 /// PInvoke of MsiViewExecute.
657 /// </summary>
658 /// <param name="view">Handle of view to execute.</param>
659 /// <param name="record">Handle to a record that supplies the parameters for the view.</param>
660 /// <returns>Error code.</returns>
661 [DllImport("msi.dll", EntryPoint = "MsiViewExecute", CharSet = CharSet.Unicode, ExactSpelling = true)]
662 internal static extern int MsiViewExecute(uint view, uint record);
663
664 /// <summary>
665 /// PInvoke of MsiViewFetch.
666 /// </summary>
667 /// <param name="view">Handle of view to fetch a row from.</param>
668 /// <param name="record">Handle to receive record info.</param>
669 /// <returns>Error code.</returns>
670 [DllImport("msi.dll", EntryPoint = "MsiViewFetch", CharSet = CharSet.Unicode, ExactSpelling = true)]
671 internal static extern int MsiViewFetch(uint view, out uint record);
672
673 /// <summary>
674 /// PInvoke of MsiViewModify.
675 /// </summary>
676 /// <param name="view">Handle of view to modify.</param>
677 /// <param name="modifyMode">Modify mode.</param>
678 /// <param name="record">Handle of record.</param>
679 /// <returns>Error code.</returns>
680 [DllImport("msi.dll", EntryPoint = "MsiViewModify", CharSet = CharSet.Unicode, ExactSpelling = true)]
681 internal static extern int MsiViewModify(uint view, int modifyMode, uint record);
682
683 /// <summary>
684 /// contains the file hash information returned by MsiGetFileHash and used in the MsiFileHash table.
685 /// </summary>
686 [StructLayout(LayoutKind.Explicit)]
687 internal class MSIFILEHASHINFO
688 {
689 [FieldOffset(0)] internal uint FileHashInfoSize;
690 [FieldOffset(4)] internal int Data0;
691 [FieldOffset(8)] internal int Data1;
692 [FieldOffset(12)]internal int Data2;
693 [FieldOffset(16)]internal int Data3;
694 }
695 }
696}
697#endif \ No newline at end of file
diff --git a/src/WixToolset.Core.WindowsInstaller/Msi/MsiException.cs b/src/WixToolset.Core.WindowsInstaller/Msi/MsiException.cs
new file mode 100644
index 00000000..b33bf27a
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Msi/MsiException.cs
@@ -0,0 +1,78 @@
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.Msi
4{
5 using System;
6 using System.ComponentModel;
7 using WixToolset.Core.Native;
8
9 /// <summary>
10 /// Exception that wraps MsiGetLastError().
11 /// </summary>
12 [Serializable]
13 public class MsiException : Win32Exception
14 {
15 /// <summary>
16 /// Instantiate a new MsiException with a given error.
17 /// </summary>
18 /// <param name="error">The error code from the MsiXxx() function call.</param>
19 public MsiException(int error) : base(error)
20 {
21 uint handle = MsiInterop.MsiGetLastErrorRecord();
22 if (0 != handle)
23 {
24 using (Record record = new Record(handle))
25 {
26 this.MsiError = record.GetInteger(1);
27
28 int errorInfoCount = record.GetFieldCount() - 1;
29 this.ErrorInfo = new string[errorInfoCount];
30 for (int i = 0; i < errorInfoCount; ++i)
31 {
32 this.ErrorInfo[i] = record.GetString(i + 2);
33 }
34 }
35 }
36 else
37 {
38 this.MsiError = 0;
39 this.ErrorInfo = new string[0];
40 }
41
42 this.Error = error;
43 }
44
45 /// <summary>
46 /// Gets the error number.
47 /// </summary>
48 public int Error { get; private set; }
49
50 /// <summary>
51 /// Gets the internal MSI error number.
52 /// </summary>
53 public int MsiError { get; private set; }
54
55 /// <summary>
56 /// Gets any additional the error information.
57 /// </summary>
58 public string[] ErrorInfo { get; private set; }
59
60 /// <summary>
61 /// Overrides Message property to return useful error message.
62 /// </summary>
63 public override string Message
64 {
65 get
66 {
67 if (0 == this.MsiError)
68 {
69 return base.Message;
70 }
71 else
72 {
73 return String.Format("Internal MSI failure. Win32 error: {0}, MSI error: {1}, detail: {2}", this.Error, this.MsiError, String.Join(", ", this.ErrorInfo));
74 }
75 }
76 }
77 }
78}
diff --git a/src/WixToolset.Core.WindowsInstaller/Msi/MsiHandle.cs b/src/WixToolset.Core.WindowsInstaller/Msi/MsiHandle.cs
new file mode 100644
index 00000000..6d2dc984
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Msi/MsiHandle.cs
@@ -0,0 +1,116 @@
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.Msi
4{
5 using System;
6 using System.ComponentModel;
7 using System.Diagnostics;
8 using System.Threading;
9 using WixToolset.Core.Native;
10
11 /// <summary>
12 /// Wrapper class for MSI handle.
13 /// </summary>
14 public class MsiHandle : IDisposable
15 {
16 private bool disposed;
17 private uint handle;
18 private int owningThread;
19#if DEBUG
20 private string creationStack;
21#endif
22
23 /// <summary>
24 /// MSI handle destructor.
25 /// </summary>
26 ~MsiHandle()
27 {
28 this.Dispose(false);
29 }
30
31 /// <summary>
32 /// Gets or sets the MSI handle.
33 /// </summary>
34 /// <value>The MSI handle.</value>
35 internal uint Handle
36 {
37 get
38 {
39 if (this.disposed)
40 {
41 throw new ObjectDisposedException("MsiHandle");
42 }
43
44 return this.handle;
45 }
46
47 set
48 {
49 if (this.disposed)
50 {
51 throw new ObjectDisposedException("MsiHandle");
52 }
53
54 this.handle = value;
55 this.owningThread = Thread.CurrentThread.ManagedThreadId;
56#if DEBUG
57 this.creationStack = Environment.StackTrace;
58#endif
59 }
60 }
61
62 /// <summary>
63 /// Close the MSI handle.
64 /// </summary>
65 public void Close()
66 {
67 this.Dispose();
68 }
69
70 /// <summary>
71 /// Disposes the managed and unmanaged objects in this object.
72 /// </summary>
73 public void Dispose()
74 {
75 this.Dispose(true);
76 GC.SuppressFinalize(this);
77 }
78
79 /// <summary>
80 /// Disposes the managed and unmanaged objects in this object.
81 /// </summary>
82 /// <param name="disposing">true to dispose the managed objects.</param>
83 protected virtual void Dispose(bool disposing)
84 {
85 if (!this.disposed)
86 {
87 if (0 != this.handle)
88 {
89 if (Thread.CurrentThread.ManagedThreadId == this.owningThread)
90 {
91 int error = MsiInterop.MsiCloseHandle(this.handle);
92 if (0 != error)
93 {
94 throw new Win32Exception(error);
95 }
96 this.handle = 0;
97 }
98 else
99 {
100 // Don't try to close the handle on a different thread than it was opened.
101 // This will occasionally cause MSI to AV.
102 string message = String.Format("Leaked msi handle {0} created on thread {1} by type {2}. This handle cannot be closed on thread {3}",
103 this.handle, this.owningThread, this.GetType(), Thread.CurrentThread.ManagedThreadId);
104#if DEBUG
105 throw new InvalidOperationException(String.Format("{0}. Created {1}", message, this.creationStack));
106#else
107 Debug.WriteLine(message);
108#endif
109 }
110 }
111
112 this.disposed = true;
113 }
114 }
115 }
116}
diff --git a/src/WixToolset.Core.WindowsInstaller/Msi/Record.cs b/src/WixToolset.Core.WindowsInstaller/Msi/Record.cs
new file mode 100644
index 00000000..438aa3b0
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Msi/Record.cs
@@ -0,0 +1,182 @@
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.Msi
4{
5 using System;
6 using System.ComponentModel;
7 using System.Text;
8 using WixToolset.Core.Native;
9
10 /// <summary>
11 /// Wrapper class around msi.dll interop for a record.
12 /// </summary>
13 public sealed class Record : MsiHandle
14 {
15 /// <summary>
16 /// Creates a record with the specified number of fields.
17 /// </summary>
18 /// <param name="fieldCount">Number of fields in record.</param>
19 public Record(int fieldCount)
20 {
21 this.Handle = MsiInterop.MsiCreateRecord(fieldCount);
22 if (0 == this.Handle)
23 {
24 throw new OutOfMemoryException();
25 }
26 }
27
28 /// <summary>
29 /// Creates a record from a handle.
30 /// </summary>
31 /// <param name="handle">Handle to create record from.</param>
32 internal Record(uint handle)
33 {
34 this.Handle = handle;
35 }
36
37 /// <summary>
38 /// Gets a string value at specified location.
39 /// </summary>
40 /// <param name="field">Index into record to get string.</param>
41 public string this[int field]
42 {
43 get { return this.GetString(field); }
44 set { this.SetString(field, (string)value); }
45 }
46
47 /// <summary>
48 /// Determines if the value is null at the specified location.
49 /// </summary>
50 /// <param name="field">Index into record of the field to query.</param>
51 /// <returns>true if the value is null, false otherwise.</returns>
52 public bool IsNull(int field)
53 {
54 int error = MsiInterop.MsiRecordIsNull(this.Handle, field);
55
56 switch (error)
57 {
58 case 0:
59 return false;
60 case 1:
61 return true;
62 default:
63 throw new Win32Exception(error);
64 }
65 }
66
67 /// <summary>
68 /// Gets integer value at specified location.
69 /// </summary>
70 /// <param name="field">Index into record to get integer</param>
71 /// <returns>Integer value</returns>
72 public int GetInteger(int field)
73 {
74 return MsiInterop.MsiRecordGetInteger(this.Handle, field);
75 }
76
77 /// <summary>
78 /// Sets integer value at specified location.
79 /// </summary>
80 /// <param name="field">Index into record to set integer.</param>
81 /// <param name="value">Value to set into record.</param>
82 public void SetInteger(int field, int value)
83 {
84 int error = MsiInterop.MsiRecordSetInteger(this.Handle, field, value);
85 if (0 != error)
86 {
87 throw new Win32Exception(error);
88 }
89 }
90
91 /// <summary>
92 /// Gets string value at specified location.
93 /// </summary>
94 /// <param name="field">Index into record to get string.</param>
95 /// <returns>String value</returns>
96 public string GetString(int field)
97 {
98 int bufferSize = 255;
99 StringBuilder buffer = new StringBuilder(bufferSize);
100 int error = MsiInterop.MsiRecordGetString(this.Handle, field, buffer, ref bufferSize);
101 if (234 == error)
102 {
103 buffer.EnsureCapacity(++bufferSize);
104 error = MsiInterop.MsiRecordGetString(this.Handle, field, buffer, ref bufferSize);
105 }
106
107 if (0 != error)
108 {
109 throw new Win32Exception(error);
110 }
111
112 return (0 < buffer.Length ? buffer.ToString() : null);
113 }
114
115 /// <summary>
116 /// Set string value at specified location
117 /// </summary>
118 /// <param name="field">Index into record to set string.</param>
119 /// <param name="value">Value to set into record</param>
120 public void SetString(int field, string value)
121 {
122 int error = MsiInterop.MsiRecordSetString(this.Handle, field, value);
123 if (0 != error)
124 {
125 throw new Win32Exception(error);
126 }
127 }
128
129 /// <summary>
130 /// Get stream at specified location.
131 /// </summary>
132 /// <param name="field">Index into record to get stream.</param>
133 /// <param name="buffer">buffer to receive bytes from stream.</param>
134 /// <param name="requestedBufferSize">Buffer size to read.</param>
135 /// <returns>Stream read into string.</returns>
136 public int GetStream(int field, byte[] buffer, int requestedBufferSize)
137 {
138 int bufferSize = 255;
139 if (requestedBufferSize > 0)
140 {
141 bufferSize = requestedBufferSize;
142 }
143
144 int error = MsiInterop.MsiRecordReadStream(this.Handle, field, buffer, ref bufferSize);
145 if (0 != error)
146 {
147 throw new Win32Exception(error);
148 }
149
150 return bufferSize;
151 }
152
153 /// <summary>
154 /// Sets a stream at a specified location.
155 /// </summary>
156 /// <param name="field">Index into record to set stream.</param>
157 /// <param name="path">Path to file to read into stream.</param>
158 public void SetStream(int field, string path)
159 {
160 int error = MsiInterop.MsiRecordSetStream(this.Handle, field, path);
161 if (0 != error)
162 {
163 throw new Win32Exception(error);
164 }
165 }
166
167 /// <summary>
168 /// Gets the number of fields in record.
169 /// </summary>
170 /// <returns>Count of fields in record.</returns>
171 public int GetFieldCount()
172 {
173 int size = MsiInterop.MsiRecordGetFieldCount(this.Handle);
174 if (0 > size)
175 {
176 throw new Win32Exception();
177 }
178
179 return size;
180 }
181 }
182}
diff --git a/src/WixToolset.Core.WindowsInstaller/Msi/Session.cs b/src/WixToolset.Core.WindowsInstaller/Msi/Session.cs
new file mode 100644
index 00000000..d3a19711
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Msi/Session.cs
@@ -0,0 +1,45 @@
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.Msi
4{
5 using System;
6 using System.ComponentModel;
7 using System.Globalization;
8 using WixToolset.Core.Native;
9
10 /// <summary>
11 /// Controls the installation process.
12 /// </summary>
13 internal sealed class Session : MsiHandle
14 {
15 /// <summary>
16 /// Instantiate a new Session.
17 /// </summary>
18 /// <param name="database">The database to open.</param>
19 public Session(Database database)
20 {
21 string packagePath = String.Format(CultureInfo.InvariantCulture, "#{0}", (uint)database.Handle);
22
23 uint handle = 0;
24 int error = MsiInterop.MsiOpenPackage(packagePath, out handle);
25 if (0 != error)
26 {
27 throw new MsiException(error);
28 }
29 this.Handle = handle;
30 }
31
32 /// <summary>
33 /// Executes a built-in action, custom action, or user-interface wizard action.
34 /// </summary>
35 /// <param name="action">Specifies the action to execute.</param>
36 public void DoAction(string action)
37 {
38 int error = MsiInterop.MsiDoAction(this.Handle, action);
39 if (0 != error)
40 {
41 throw new MsiException(error);
42 }
43 }
44 }
45}
diff --git a/src/WixToolset.Core.WindowsInstaller/Msi/SummaryInformation.cs b/src/WixToolset.Core.WindowsInstaller/Msi/SummaryInformation.cs
new file mode 100644
index 00000000..26831731
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Msi/SummaryInformation.cs
@@ -0,0 +1,245 @@
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.Msi
4{
5 using System;
6 using System.Globalization;
7 using System.Text;
8 using FILETIME = System.Runtime.InteropServices.ComTypes.FILETIME;
9 using WixToolset.Core.Native;
10
11 /// <summary>
12 /// Summary information for the MSI files.
13 /// </summary>
14 internal sealed class SummaryInformation : MsiHandle
15 {
16 /// <summary>
17 /// Summary information properties for transforms.
18 /// </summary>
19 public enum Transform
20 {
21 /// <summary>PID_CODEPAGE = code page for the summary information stream</summary>
22 CodePage = 1,
23
24 /// <summary>PID_TITLE = typically just "Transform"</summary>
25 Title = 2,
26
27 /// <summary>PID_SUBJECT = original subject of target</summary>
28 TargetSubject = 3,
29
30 /// <summary>PID_AUTHOR = original manufacturer of target</summary>
31 TargetManufacturer = 4,
32
33 /// <summary>PID_KEYWORDS = keywords for the transform, typically including at least "Installer"</summary>
34 Keywords = 5,
35
36 /// <summary>PID_COMMENTS = describes what this package does</summary>
37 Comments = 6,
38
39 /// <summary>PID_TEMPLATE = target platform;language</summary>
40 TargetPlatformAndLanguage = 7,
41
42 /// <summary>PID_LASTAUTHOR = updated platform;language</summary>
43 UpdatedPlatformAndLanguage = 8,
44
45 /// <summary>PID_REVNUMBER = {productcode}version;{newproductcode}newversion;upgradecode</summary>
46 ProductCodes = 9,
47
48 /// <summary>PID_LASTPRINTED should be null for transforms</summary>
49 Reserved11 = 11,
50
51 ///.<summary>PID_CREATE_DTM = the timestamp when the transform was created</summary>
52 CreationTime = 12,
53
54 /// <summary>PID_PAGECOUNT = minimum installer version</summary>
55 InstallerRequirement = 14,
56
57 /// <summary>PID_CHARCOUNT = validation and error flags</summary>
58 ValidationFlags = 16,
59
60 /// <summary>PID_APPNAME = the application that created the transform</summary>
61 CreatingApplication = 18,
62
63 /// <summary>PID_SECURITY = whether read-only is enforced; should always be 4 for transforms</summary>
64 Security = 19,
65 }
66
67 /// <summary>
68 /// Summary information properties for patches.
69 /// </summary>
70 public enum Patch
71 {
72 /// <summary>PID_CODEPAGE = code page of the summary information stream</summary>
73 CodePage = 1,
74
75 /// <summary>PID_TITLE = a brief description of the package type</summary>
76 Title = 2,
77
78 /// <summary>PID_SUBJECT = package name</summary>
79 PackageName = 3,
80
81 /// <summary>PID_AUTHOR = manufacturer of the patch package</summary>
82 Manufacturer = 4,
83
84 /// <summary>PID_KEYWORDS = alternate sources for the patch package</summary>
85 Sources = 5,
86
87 /// <summary>PID_COMMENTS = general purpose of the patch package</summary>
88 Comments = 6,
89
90 /// <summary>PID_TEMPLATE = semicolon delimited list of ProductCodes</summary>
91 ProductCodes = 7,
92
93 /// <summary>PID_LASTAUTHOR = semicolon delimited list of transform names</summary>
94 TransformNames = 8,
95
96 /// <summary>PID_REVNUMBER = GUID patch code</summary>
97 PatchCode = 9,
98
99 /// <summary>PID_LASTPRINTED should be null for patches</summary>
100 Reserved11 = 11,
101
102 /// <summary>PID_PAGECOUNT should be null for patches</summary>
103 Reserved14 = 14,
104
105 /// <summary>PID_WORDCOUNT = minimum installer version</summary>
106 InstallerRequirement = 15,
107
108 /// <summary>PID_CHARCOUNT should be null for patches</summary>
109 Reserved16 = 16,
110
111 /// <summary>PID_SECURITY = read-only attribute of the patch package</summary>
112 Security = 19,
113 }
114
115 /// <summary>
116 /// Summary information values for the InstallerRequirement property.
117 /// </summary>
118 public enum InstallerRequirement
119 {
120 /// <summary>Any version of the installer will do</summary>
121 Version10 = 1,
122
123 /// <summary>At least 1.2</summary>
124 Version12 = 2,
125
126 /// <summary>At least 2.0</summary>
127 Version20 = 3,
128
129 /// <summary>At least 3.0</summary>
130 Version30 = 4,
131
132 /// <summary>At least 3.1</summary>
133 Version31 = 5,
134 }
135
136 /// <summary>
137 /// Instantiate a new SummaryInformation class from an open database.
138 /// </summary>
139 /// <param name="db">Database to retrieve summary information from.</param>
140 public SummaryInformation(Database db)
141 {
142 if (null == db)
143 {
144 throw new ArgumentNullException("db");
145 }
146
147 uint handle = 0;
148 int error = MsiInterop.MsiGetSummaryInformation(db.Handle, null, 0, ref handle);
149 if (0 != error)
150 {
151 throw new MsiException(error);
152 }
153 this.Handle = handle;
154 }
155
156 /// <summary>
157 /// Instantiate a new SummaryInformation class from a database file.
158 /// </summary>
159 /// <param name="databaseFile">The database file.</param>
160 public SummaryInformation(string databaseFile)
161 {
162 if (null == databaseFile)
163 {
164 throw new ArgumentNullException("databaseFile");
165 }
166
167 uint handle = 0;
168 int error = MsiInterop.MsiGetSummaryInformation(0, databaseFile, 0, ref handle);
169 if (0 != error)
170 {
171 throw new MsiException(error);
172 }
173 this.Handle = handle;
174 }
175
176 /// <summary>
177 /// Variant types in the summary information table.
178 /// </summary>
179 private enum VT : uint
180 {
181 /// <summary>Variant has not been assigned.</summary>
182 EMPTY = 0,
183
184 /// <summary>Null variant type.</summary>
185 NULL = 1,
186
187 /// <summary>16-bit integer variant type.</summary>
188 I2 = 2,
189
190 /// <summary>32-bit integer variant type.</summary>
191 I4 = 3,
192
193 /// <summary>String variant type.</summary>
194 LPSTR = 30,
195
196 /// <summary>Date time (FILETIME, converted to Variant time) variant type.</summary>
197 FILETIME = 64,
198 }
199
200 /// <summary>
201 /// Gets a summary information property.
202 /// </summary>
203 /// <param name="index">Index of the summary information property.</param>
204 /// <returns>The summary information property.</returns>
205 public string GetProperty(int index)
206 {
207 uint dataType;
208 StringBuilder stringValue = new StringBuilder("");
209 int bufSize = 0;
210 int intValue;
211 FILETIME timeValue;
212 timeValue.dwHighDateTime = 0;
213 timeValue.dwLowDateTime = 0;
214
215 int error = MsiInterop.MsiSummaryInfoGetProperty(this.Handle, index, out dataType, out intValue, ref timeValue, stringValue, ref bufSize);
216 if (234 == error)
217 {
218 stringValue.EnsureCapacity(++bufSize);
219 error = MsiInterop.MsiSummaryInfoGetProperty(this.Handle, index, out dataType, out intValue, ref timeValue, stringValue, ref bufSize);
220 }
221
222 if (0 != error)
223 {
224 throw new MsiException(error);
225 }
226
227 switch ((VT)dataType)
228 {
229 case VT.EMPTY:
230 return String.Empty;
231 case VT.LPSTR:
232 return stringValue.ToString();
233 case VT.I2:
234 case VT.I4:
235 return Convert.ToString(intValue, CultureInfo.InvariantCulture);
236 case VT.FILETIME:
237 long longFileTime = (((long)timeValue.dwHighDateTime) << 32) | unchecked((uint)timeValue.dwLowDateTime);
238 DateTime dateTime = DateTime.FromFileTime(longFileTime);
239 return dateTime.ToString("yyyy/MM/dd HH:mm:ss", CultureInfo.InvariantCulture);
240 default:
241 throw new InvalidOperationException();
242 }
243 }
244 }
245}
diff --git a/src/WixToolset.Core.WindowsInstaller/Msi/View.cs b/src/WixToolset.Core.WindowsInstaller/Msi/View.cs
new file mode 100644
index 00000000..d6542824
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Msi/View.cs
@@ -0,0 +1,189 @@
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.Msi
4{
5 using System;
6 using System.ComponentModel;
7 using System.Globalization;
8 using WixToolset.Core.Native;
9
10 /// <summary>
11 /// Enumeration of different modify modes.
12 /// </summary>
13 public enum ModifyView
14 {
15 /// <summary>
16 /// Writes current data in the cursor to a table row. Updates record if the primary
17 /// keys match an existing row and inserts if they do not match. Fails with a read-only
18 /// database. This mode cannot be used with a view containing joins.
19 /// </summary>
20 Assign = MsiInterop.MSIMODIFYASSIGN,
21
22 /// <summary>
23 /// Remove a row from the table. You must first call the Fetch function with the same
24 /// record. Fails if the row has been deleted. Works only with read-write records. This
25 /// mode cannot be used with a view containing joins.
26 /// </summary>
27 Delete = MsiInterop.MSIMODIFYDELETE,
28
29 /// <summary>
30 /// Inserts a record. Fails if a row with the same primary keys exists. Fails with a read-only
31 /// database. This mode cannot be used with a view containing joins.
32 /// </summary>
33 Insert = MsiInterop.MSIMODIFYINSERT,
34
35 /// <summary>
36 /// Inserts a temporary record. The information is not persistent. Fails if a row with the
37 /// same primary key exists. Works only with read-write records. This mode cannot be
38 /// used with a view containing joins.
39 /// </summary>
40 InsertTemporary = MsiInterop.MSIMODIFYINSERTTEMPORARY,
41
42 /// <summary>
43 /// Inserts or validates a record in a table. Inserts if primary keys do not match any row
44 /// and validates if there is a match. Fails if the record does not match the data in
45 /// the table. Fails if there is a record with a duplicate key that is not identical.
46 /// Works only with read-write records. This mode cannot be used with a view containing joins.
47 /// </summary>
48 Merge = MsiInterop.MSIMODIFYMERGE,
49
50 /// <summary>
51 /// Refreshes the information in the record. Must first call Fetch with the
52 /// same record. Fails for a deleted row. Works with read-write and read-only records.
53 /// </summary>
54 Refresh = MsiInterop.MSIMODIFYREFRESH,
55
56 /// <summary>
57 /// Updates or deletes and inserts a record into a table. Must first call Fetch with
58 /// the same record. Updates record if the primary keys are unchanged. Deletes old row and
59 /// inserts new if primary keys have changed. Fails with a read-only database. This mode cannot
60 /// be used with a view containing joins.
61 /// </summary>
62 Replace = MsiInterop.MSIMODIFYREPLACE,
63
64 /// <summary>
65 /// Refreshes the information in the supplied record without changing the position in the
66 /// result set and without affecting subsequent fetch operations. The record may then
67 /// be used for subsequent Update, Delete, and Refresh. All primary key columns of the
68 /// table must be in the query and the record must have at least as many fields as the
69 /// query. Seek cannot be used with multi-table queries. This mode cannot be used with
70 /// a view containing joins. See also the remarks.
71 /// </summary>
72 Seek = MsiInterop.MSIMODIFYSEEK,
73
74 /// <summary>
75 /// Updates an existing record. Non-primary keys only. Must first call Fetch. Fails with a
76 /// deleted record. Works only with read-write records.
77 /// </summary>
78 Update = MsiInterop.MSIMODIFYUPDATE
79 }
80
81 /// <summary>
82 /// Wrapper class for MSI API views.
83 /// </summary>
84 internal sealed class View : MsiHandle
85 {
86 /// <summary>
87 /// Constructor that creates a view given a database handle and a query.
88 /// </summary>
89 /// <param name="db">Handle to the database to run the query on.</param>
90 /// <param name="query">Query to be executed.</param>
91 public View(Database db, string query)
92 {
93 if (null == db)
94 {
95 throw new ArgumentNullException("db");
96 }
97
98 if (null == query)
99 {
100 throw new ArgumentNullException("query");
101 }
102
103 uint handle = 0;
104
105 int error = MsiInterop.MsiDatabaseOpenView(db.Handle, query, out handle);
106 if (0 != error)
107 {
108 throw new MsiException(error);
109 }
110
111 this.Handle = handle;
112 }
113
114 /// <summary>
115 /// Executes a view with no customizable parameters.
116 /// </summary>
117 public void Execute()
118 {
119 this.Execute(null);
120 }
121
122 /// <summary>
123 /// Executes a query substituing the values from the records into the customizable parameters
124 /// in the view.
125 /// </summary>
126 /// <param name="record">Record containing parameters to be substituded into the view.</param>
127 public void Execute(Record record)
128 {
129 int error = MsiInterop.MsiViewExecute(this.Handle, null == record ? 0 : record.Handle);
130 if (0 != error)
131 {
132 throw new MsiException(error);
133 }
134 }
135
136 /// <summary>
137 /// Fetches the next row in the view.
138 /// </summary>
139 /// <returns>Returns the fetched record; otherwise null.</returns>
140 public Record Fetch()
141 {
142 uint recordHandle;
143
144 int error = MsiInterop.MsiViewFetch(this.Handle, out recordHandle);
145 if (259 == error)
146 {
147 return null;
148 }
149 else if (0 != error)
150 {
151 throw new MsiException(error);
152 }
153
154 return new Record(recordHandle);
155 }
156
157 /// <summary>
158 /// Updates a fetched record.
159 /// </summary>
160 /// <param name="type">Type of modification mode.</param>
161 /// <param name="record">Record to be modified.</param>
162 public void Modify(ModifyView type, Record record)
163 {
164 int error = MsiInterop.MsiViewModify(this.Handle, Convert.ToInt32(type, CultureInfo.InvariantCulture), record.Handle);
165 if (0 != error)
166 {
167 throw new MsiException(error);
168 }
169 }
170
171 /// <summary>
172 /// Returns a record containing column names or definitions.
173 /// </summary>
174 /// <param name="columnType">Specifies a flag indicating what type of information is needed. Either MSICOLINFO_NAMES or MSICOLINFO_TYPES.</param>
175 /// <returns>The record containing information about the column.</returns>
176 public Record GetColumnInfo(int columnType)
177 {
178 uint recordHandle;
179
180 int error = MsiInterop.MsiViewGetColumnInfo(this.Handle, columnType, out recordHandle);
181 if (0 != error)
182 {
183 throw new MsiException(error);
184 }
185
186 return new Record(recordHandle);
187 }
188 }
189}
diff --git a/src/WixToolset.Core.WindowsInstaller/MsiBackend.cs b/src/WixToolset.Core.WindowsInstaller/MsiBackend.cs
new file mode 100644
index 00000000..716ea000
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/MsiBackend.cs
@@ -0,0 +1,36 @@
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
4{
5 using WixToolset.Core.WindowsInstaller.Bind;
6 using WixToolset.Core.WindowsInstaller.Inscribe;
7 using WixToolset.Core.WindowsInstaller.Unbind;
8 using WixToolset.Data;
9 using WixToolset.Data.Bind;
10 using WixToolset.Extensibility;
11
12 internal class MsiBackend : IBackend
13 {
14 public BindResult Bind(IBindContext context)
15 {
16 var validator = Validator.CreateFromContext(context, "darice.cub");
17
18 var command = new BindDatabaseCommand(context, validator);
19 command.Execute();
20
21 return new BindResult(command.FileTransfers, command.ContentFilePaths);
22 }
23
24 public bool Inscribe(IInscribeContext context)
25 {
26 var command = new InscribeMsiPackageCommand(context);
27 return command.Execute();
28 }
29
30 public Output Unbind(IUnbindContext context)
31 {
32 var command = new UnbindMsiOrMsmCommand(context);
33 return command.Execute();
34 }
35 }
36}
diff --git a/src/WixToolset.Core.WindowsInstaller/MsmBackend.cs b/src/WixToolset.Core.WindowsInstaller/MsmBackend.cs
new file mode 100644
index 00000000..268213d7
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/MsmBackend.cs
@@ -0,0 +1,34 @@
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
4{
5 using WixToolset.Core.WindowsInstaller.Bind;
6 using WixToolset.Core.WindowsInstaller.Unbind;
7 using WixToolset.Data;
8 using WixToolset.Data.Bind;
9 using WixToolset.Extensibility;
10
11 internal class MsmBackend : IBackend
12 {
13 public BindResult Bind(IBindContext context)
14 {
15 var validator = Validator.CreateFromContext(context, "mergemod.cub");
16
17 var command = new BindDatabaseCommand(context, validator);
18 command.Execute();
19
20 return new BindResult(command.FileTransfers, command.ContentFilePaths);
21 }
22
23 public bool Inscribe(IInscribeContext context)
24 {
25 return false;
26 }
27
28 public Output Unbind(IUnbindContext context)
29 {
30 var command = new UnbindMsiOrMsmCommand(context);
31 return command.Execute();
32 }
33 }
34}
diff --git a/src/WixToolset.Core.WindowsInstaller/MspBackend.cs b/src/WixToolset.Core.WindowsInstaller/MspBackend.cs
new file mode 100644
index 00000000..4b13258b
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/MspBackend.cs
@@ -0,0 +1,111 @@
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
4{
5 using System;
6 using System.ComponentModel;
7 using System.IO;
8 using WixToolset.Core.Native;
9 using WixToolset.Core.WindowsInstaller.Unbind;
10 using WixToolset.Data;
11 using WixToolset.Data.Bind;
12 using WixToolset.Extensibility;
13 using WixToolset.Msi;
14 using WixToolset.Ole32;
15
16 internal class MspBackend : IBackend
17 {
18 public BindResult Bind(IBindContext context)
19 {
20 throw new NotImplementedException();
21 }
22
23 public bool Inscribe(IInscribeContext context)
24 {
25 throw new NotImplementedException();
26 }
27
28 public Output Unbind(IUnbindContext context)
29 {
30 Output patch;
31
32 // patch files are essentially database files (use a special flag to let the API know its a patch file)
33 try
34 {
35 using (Database database = new Database(context.InputFilePath, OpenDatabase.ReadOnly | OpenDatabase.OpenPatchFile))
36 {
37 var unbindCommand = new UnbindDatabaseCommand(context.Messaging, database, context.InputFilePath, OutputType.Patch, context.ExportBasePath, context.IntermediateFolder, context.IsAdminImage, context.SuppressDemodularization, skipSummaryInfo: false);
38 patch = unbindCommand.Execute();
39 }
40 }
41 catch (Win32Exception e)
42 {
43 if (0x6E == e.NativeErrorCode) // ERROR_OPEN_FAILED
44 {
45 throw new WixException(WixErrors.OpenDatabaseFailed(context.InputFilePath));
46 }
47
48 throw;
49 }
50
51 // retrieve the transforms (they are in substorages)
52 using (Storage storage = Storage.Open(context.InputFilePath, StorageMode.Read | StorageMode.ShareDenyWrite))
53 {
54 Table summaryInformationTable = patch.Tables["_SummaryInformation"];
55 foreach (Row row in summaryInformationTable.Rows)
56 {
57 if (8 == (int)row[0]) // PID_LASTAUTHOR
58 {
59 string value = (string)row[1];
60
61 foreach (string decoratedSubStorageName in value.Split(';'))
62 {
63 string subStorageName = decoratedSubStorageName.Substring(1);
64 string transformFile = Path.Combine(context.IntermediateFolder, String.Concat("Transform", Path.DirectorySeparatorChar, subStorageName, ".mst"));
65
66 // ensure the parent directory exists
67 System.IO.Directory.CreateDirectory(Path.GetDirectoryName(transformFile));
68
69 // copy the substorage to a new storage for the transform file
70 using (Storage subStorage = storage.OpenStorage(subStorageName))
71 {
72 using (Storage transformStorage = Storage.CreateDocFile(transformFile, StorageMode.ReadWrite | StorageMode.ShareExclusive | StorageMode.Create))
73 {
74 subStorage.CopyTo(transformStorage);
75 }
76 }
77
78 // unbind the transform
79 var unbindCommand= new UnbindTransformCommand(context.Messaging, transformFile, (null == context.ExportBasePath ? null : Path.Combine(context.ExportBasePath, subStorageName)), context.IntermediateFolder);
80 var transform = unbindCommand.Execute();
81
82 patch.SubStorages.Add(new SubStorage(subStorageName, transform));
83 }
84
85 break;
86 }
87 }
88 }
89
90 // extract the files from the cabinets
91 // TODO: use per-transform export paths for support of multi-product patches
92 if (null != context.ExportBasePath && !context.SuppressExtractCabinets)
93 {
94 using (Database database = new Database(context.InputFilePath, OpenDatabase.ReadOnly | OpenDatabase.OpenPatchFile))
95 {
96 foreach (SubStorage subStorage in patch.SubStorages)
97 {
98 // only patch transforms should carry files
99 if (subStorage.Name.StartsWith("#", StringComparison.Ordinal))
100 {
101 var extractCommand = new ExtractCabinetsCommand(subStorage.Data, database, context.InputFilePath, context.ExportBasePath, context.IntermediateFolder);
102 extractCommand.Execute();
103 }
104 }
105 }
106 }
107
108 return patch;
109 }
110 }
111} \ No newline at end of file
diff --git a/src/WixToolset.Core.WindowsInstaller/MstBackend.cs b/src/WixToolset.Core.WindowsInstaller/MstBackend.cs
new file mode 100644
index 00000000..2cb7da89
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/MstBackend.cs
@@ -0,0 +1,37 @@
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
4{
5 using System;
6 using WixToolset.Core.WindowsInstaller.Databases;
7 using WixToolset.Core.WindowsInstaller.Unbind;
8 using WixToolset.Data;
9 using WixToolset.Data.Bind;
10 using WixToolset.Extensibility;
11
12 internal class MstBackend : IBackend
13 {
14 public BindResult Bind(IBindContext context)
15 {
16 var command = new BindTransformCommand();
17 command.Extensions = context.Extensions;
18 command.TempFilesLocation = context.IntermediateFolder;
19 command.Transform = context.IntermediateRepresentation;
20 command.OutputPath = context.OutputPath;
21 command.Execute();
22
23 return new BindResult(Array.Empty<FileTransfer>(), Array.Empty<string>());
24 }
25
26 public bool Inscribe(IInscribeContext context)
27 {
28 throw new NotImplementedException();
29 }
30
31 public Output Unbind(IUnbindContext context)
32 {
33 var command = new UnbindMsiOrMsmCommand(context);
34 return command.Execute();
35 }
36 }
37} \ No newline at end of file
diff --git a/src/WixToolset.Core.WindowsInstaller/Ole32/Storage.cs b/src/WixToolset.Core.WindowsInstaller/Ole32/Storage.cs
new file mode 100644
index 00000000..c6a43bc4
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Ole32/Storage.cs
@@ -0,0 +1,437 @@
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.Ole32
4{
5 using System;
6 using System.Runtime.InteropServices;
7 using FILETIME = System.Runtime.InteropServices.ComTypes.FILETIME;
8 using STATSTG = System.Runtime.InteropServices.ComTypes.STATSTG;
9
10 /// <summary>
11 /// Specifies the access mode to use when opening, creating, or deleting a storage object.
12 /// </summary>
13 internal enum StorageMode
14 {
15 /// <summary>
16 /// Indicates that the object is read-only, meaning that modifications cannot be made.
17 /// </summary>
18 Read = 0x0,
19
20 /// <summary>
21 /// Enables you to save changes to the object, but does not permit access to its data.
22 /// </summary>
23 Write = 0x1,
24
25 /// <summary>
26 /// Enables access and modification of object data.
27 /// </summary>
28 ReadWrite = 0x2,
29
30 /// <summary>
31 /// Specifies that subsequent openings of the object are not denied read or write access.
32 /// </summary>
33 ShareDenyNone = 0x40,
34
35 /// <summary>
36 /// Prevents others from subsequently opening the object in Read mode.
37 /// </summary>
38 ShareDenyRead = 0x30,
39
40 /// <summary>
41 /// Prevents others from subsequently opening the object for Write or ReadWrite access.
42 /// </summary>
43 ShareDenyWrite = 0x20,
44
45 /// <summary>
46 /// Prevents others from subsequently opening the object in any mode.
47 /// </summary>
48 ShareExclusive = 0x10,
49
50 /// <summary>
51 /// Opens the storage object with exclusive access to the most recently committed version.
52 /// </summary>
53 Priority = 0x40000,
54
55 /// <summary>
56 /// Indicates that an existing storage object or stream should be removed before the new object replaces it.
57 /// </summary>
58 Create = 0x1000,
59 }
60
61 /// <summary>
62 /// Wrapper for the compound storage file APIs.
63 /// </summary>
64 internal sealed class Storage : IDisposable
65 {
66 private bool disposed;
67 private IStorage storage;
68
69 /// <summary>
70 /// Instantiate a new Storage.
71 /// </summary>
72 /// <param name="storage">The native storage interface.</param>
73 private Storage(IStorage storage)
74 {
75 this.storage = storage;
76 }
77
78 /// <summary>
79 /// Storage destructor.
80 /// </summary>
81 ~Storage()
82 {
83 this.Dispose();
84 }
85
86 /// <summary>
87 /// The IEnumSTATSTG interface enumerates an array of STATSTG structures.
88 /// </summary>
89 [ComImport, Guid("0000000d-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
90 public interface IEnumSTATSTG
91 {
92 /// <summary>
93 /// Gets a specified number of STATSTG structures.
94 /// </summary>
95 /// <param name="celt">The number of STATSTG structures requested.</param>
96 /// <param name="rgelt">An array of STATSTG structures returned.</param>
97 /// <param name="pceltFetched">The number of STATSTG structures retrieved in the rgelt parameter.</param>
98 /// <returns>The error code.</returns>
99 [PreserveSig]
100 uint Next(uint celt, [MarshalAs(UnmanagedType.LPArray), Out] STATSTG[] rgelt, out uint pceltFetched);
101
102 /// <summary>
103 /// Skips a specified number of STATSTG structures in the enumeration sequence.
104 /// </summary>
105 /// <param name="celt">The number of STATSTG structures to skip.</param>
106 void Skip(uint celt);
107
108 /// <summary>
109 /// Resets the enumeration sequence to the beginning of the STATSTG structure array.
110 /// </summary>
111 void Reset();
112
113 /// <summary>
114 /// Creates a new enumerator that contains the same enumeration state as the current STATSTG structure enumerator.
115 /// </summary>
116 /// <returns>The cloned IEnumSTATSTG interface.</returns>
117 [return: MarshalAs(UnmanagedType.Interface)]
118 IEnumSTATSTG Clone();
119 }
120
121 /// <summary>
122 /// The IStorage interface supports the creation and management of structured storage objects.
123 /// </summary>
124 [ComImport, Guid("0000000b-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
125 private interface IStorage
126 {
127 /// <summary>
128 /// Creates and opens a stream object with the specified name contained in this storage object.
129 /// </summary>
130 /// <param name="pwcsName">The name of the newly created stream.</param>
131 /// <param name="grfMode">Specifies the access mode to use when opening the newly created stream.</param>
132 /// <param name="reserved1">Reserved for future use; must be zero.</param>
133 /// <param name="reserved2">Reserved for future use; must be zero.</param>
134 /// <param name="ppstm">On return, pointer to the location of the new IStream interface pointer.</param>
135 void CreateStream(string pwcsName, uint grfMode, uint reserved1, uint reserved2, out IStream ppstm);
136
137 /// <summary>
138 /// Opens an existing stream object within this storage object using the specified access permissions in grfMode.
139 /// </summary>
140 /// <param name="pwcsName">The name of the stream to open.</param>
141 /// <param name="reserved1">Reserved for future use; must be NULL.</param>
142 /// <param name="grfMode">Specifies the access mode to be assigned to the open stream.</param>
143 /// <param name="reserved2">Reserved for future use; must be zero.</param>
144 /// <param name="ppstm">A pointer to IStream pointer variable that receives the interface pointer to the newly opened stream object.</param>
145 void OpenStream(string pwcsName, IntPtr reserved1, uint grfMode, uint reserved2, out IStream ppstm);
146
147 /// <summary>
148 /// Creates and opens a new storage object nested within this storage object with the specified name in the specified access mode.
149 /// </summary>
150 /// <param name="pwcsName">The name of the newly created storage object.</param>
151 /// <param name="grfMode">A value that specifies the access mode to use when opening the newly created storage object.</param>
152 /// <param name="reserved1">Reserved for future use; must be zero.</param>
153 /// <param name="reserved2">Reserved for future use; must be zero.</param>
154 /// <param name="ppstg">A pointer, when successful, to the location of the IStorage pointer to the newly created storage object.</param>
155 void CreateStorage(string pwcsName, uint grfMode, uint reserved1, uint reserved2, out IStorage ppstg);
156
157 /// <summary>
158 /// Opens an existing storage object with the specified name in the specified access mode.
159 /// </summary>
160 /// <param name="pwcsName">The name of the storage object to open.</param>
161 /// <param name="pstgPriority">Must be NULL.</param>
162 /// <param name="grfMode">Specifies the access mode to use when opening the storage object.</param>
163 /// <param name="snbExclude">Must be NULL.</param>
164 /// <param name="reserved">Reserved for future use; must be zero.</param>
165 /// <param name="ppstg">When successful, pointer to the location of an IStorage pointer to the opened storage object.</param>
166 void OpenStorage(string pwcsName, IStorage pstgPriority, uint grfMode, IntPtr snbExclude, uint reserved, out IStorage ppstg);
167
168 /// <summary>
169 /// Copies the entire contents of an open storage object to another storage object.
170 /// </summary>
171 /// <param name="ciidExclude">The number of elements in the array pointed to by rgiidExclude.</param>
172 /// <param name="rgiidExclude">An array of interface identifiers (IIDs) that either the caller knows about and does not want
173 /// copied or that the storage object does not support, but whose state the caller will later explicitly copy.</param>
174 /// <param name="snbExclude">A string name block (refer to SNB) that specifies a block of storage or stream objects that are not to be copied to the destination.</param>
175 /// <param name="pstgDest">A pointer to the open storage object into which this storage object is to be copied.</param>
176 void CopyTo(uint ciidExclude, IntPtr rgiidExclude, IntPtr snbExclude, IStorage pstgDest);
177
178 /// <summary>
179 /// Copies or moves a substorage or stream from this storage object to another storage object.
180 /// </summary>
181 /// <param name="pwcsName">The name of the element in this storage object to be moved or copied.</param>
182 /// <param name="pstgDest">IStorage pointer to the destination storage object.</param>
183 /// <param name="pwcsNewName">The new name for the element in its new storage object.</param>
184 /// <param name="grfFlags">Specifies whether the operation should be a move (STGMOVE_MOVE) or a copy (STGMOVE_COPY).</param>
185 void MoveElementTo(string pwcsName, IStorage pstgDest, string pwcsNewName, uint grfFlags);
186
187 /// <summary>
188 /// Reflects changes for a transacted storage object to the parent level.
189 /// </summary>
190 /// <param name="grfCommitFlags">Controls how the changes are committed to the storage object.</param>
191 void Commit(uint grfCommitFlags);
192
193 /// <summary>
194 /// Discards all changes that have been made to the storage object since the last commit operation.
195 /// </summary>
196 void Revert();
197
198 /// <summary>
199 /// Returns an enumerator object that can be used to enumerate the storage and stream objects contained within this storage object.
200 /// </summary>
201 /// <param name="reserved1">Reserved for future use; must be zero.</param>
202 /// <param name="reserved2">Reserved for future use; must be NULL.</param>
203 /// <param name="reserved3">Reserved for future use; must be zero.</param>
204 /// <param name="ppenum">Pointer to IEnumSTATSTG* pointer variable that receives the interface pointer to the new enumerator object.</param>
205 void EnumElements(uint reserved1, IntPtr reserved2, uint reserved3, out IEnumSTATSTG ppenum);
206
207 /// <summary>
208 /// Removes the specified storage or stream from this storage object.
209 /// </summary>
210 /// <param name="pwcsName">The name of the storage or stream to be removed.</param>
211 void DestroyElement(string pwcsName);
212
213 /// <summary>
214 /// Renames the specified storage or stream in this storage object.
215 /// </summary>
216 /// <param name="pwcsOldName">The name of the substorage or stream to be changed.</param>
217 /// <param name="pwcsNewName">The new name for the specified substorage or stream.</param>
218 void RenameElement(string pwcsOldName, string pwcsNewName);
219
220 /// <summary>
221 /// Sets the modification, access, and creation times of the indicated storage element, if supported by the underlying file system.
222 /// </summary>
223 /// <param name="pwcsName">The name of the storage object element whose times are to be modified.</param>
224 /// <param name="pctime">Either the new creation time for the element or NULL if the creation time is not to be modified.</param>
225 /// <param name="patime">Either the new access time for the element or NULL if the access time is not to be modified.</param>
226 /// <param name="pmtime">Either the new modification time for the element or NULL if the modification time is not to be modified.</param>
227 void SetElementTimes(string pwcsName, FILETIME pctime, FILETIME patime, FILETIME pmtime);
228
229 /// <summary>
230 /// Assigns the specified CLSID to this storage object.
231 /// </summary>
232 /// <param name="clsid">The CLSID that is to be associated with the storage object.</param>
233 void SetClass(Guid clsid);
234
235 /// <summary>
236 /// Stores up to 32 bits of state information in this storage object.
237 /// </summary>
238 /// <param name="grfStateBits">Specifies the new values of the bits to set.</param>
239 /// <param name="grfMask">A binary mask indicating which bits in grfStateBits are significant in this call.</param>
240 void SetStateBits(uint grfStateBits, uint grfMask);
241
242 /// <summary>
243 /// Returns the STATSTG structure for this open storage object.
244 /// </summary>
245 /// <param name="pstatstg">On return, pointer to a STATSTG structure where this method places information about the open storage object.</param>
246 /// <param name="grfStatFlag">Specifies that some of the members in the STATSTG structure are not returned, thus saving a memory allocation operation.</param>
247 void Stat(out STATSTG pstatstg, uint grfStatFlag);
248 }
249
250 /// <summary>
251 /// The IStream interface lets you read and write data to stream objects.
252 /// </summary>
253 [ComImport, Guid("0000000c-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
254 private interface IStream
255 {
256 /// <summary>
257 /// Reads a specified number of bytes from the stream object into memory starting at the current seek pointer.
258 /// </summary>
259 /// <param name="pv">A pointer to the buffer which the stream data is read into.</param>
260 /// <param name="cb">The number of bytes of data to read from the stream object.</param>
261 /// <param name="pcbRead">A pointer to a ULONG variable that receives the actual number of bytes read from the stream object.</param>
262 void Read([Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] byte[] pv, int cb, IntPtr pcbRead);
263
264 /// <summary>
265 /// Writes a specified number of bytes into the stream object starting at the current seek pointer.
266 /// </summary>
267 /// <param name="pv">A pointer to the buffer that contains the data that is to be written to the stream.</param>
268 /// <param name="cb">The number of bytes of data to attempt to write into the stream.</param>
269 /// <param name="pcbWritten">A pointer to a ULONG variable where this method writes the actual number of bytes written to the stream object.</param>
270 void Write([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] byte[] pv, int cb, IntPtr pcbWritten);
271
272 /// <summary>
273 /// Changes the seek pointer to a new location relative to the beginning of the stream, the end of the stream, or the current seek pointer.
274 /// </summary>
275 /// <param name="dlibMove">The displacement to be added to the location indicated by the dwOrigin parameter.</param>
276 /// <param name="dwOrigin">The origin for the displacement specified in dlibMove.</param>
277 /// <param name="plibNewPosition">A pointer to the location where this method writes the value of the new seek pointer from the beginning of the stream.</param>
278 void Seek(long dlibMove, int dwOrigin, IntPtr plibNewPosition);
279
280 /// <summary>
281 /// Changes the size of the stream object.
282 /// </summary>
283 /// <param name="libNewSize">Specifies the new size of the stream as a number of bytes.</param>
284 void SetSize(long libNewSize);
285
286 /// <summary>
287 /// Copies a specified number of bytes from the current seek pointer in the stream to the current seek pointer in another stream.
288 /// </summary>
289 /// <param name="pstm">A pointer to the destination stream.</param>
290 /// <param name="cb">The number of bytes to copy from the source stream.</param>
291 /// <param name="pcbRead">A pointer to the location where this method writes the actual number of bytes read from the source.</param>
292 /// <param name="pcbWritten">A pointer to the location where this method writes the actual number of bytes written to the destination.</param>
293 void CopyTo(IStream pstm, long cb, IntPtr pcbRead, IntPtr pcbWritten);
294
295 /// <summary>
296 /// Ensures that any changes made to a stream object open in transacted mode are reflected in the parent storage object.
297 /// </summary>
298 /// <param name="grfCommitFlags">Controls how the changes for the stream object are committed.</param>
299 void Commit(int grfCommitFlags);
300
301 /// <summary>
302 /// Discards all changes that have been made to a transacted stream since the last call to IStream::Commit.
303 /// </summary>
304 void Revert();
305
306 /// <summary>
307 /// Restricts access to a specified range of bytes in the stream.
308 /// </summary>
309 /// <param name="libOffset">Integer that specifies the byte offset for the beginning of the range.</param>
310 /// <param name="cb">Integer that specifies the length of the range, in bytes, to be restricted.</param>
311 /// <param name="dwLockType">Specifies the restrictions being requested on accessing the range.</param>
312 void LockRegion(long libOffset, long cb, int dwLockType);
313
314 /// <summary>
315 /// Removes the access restriction on a range of bytes previously restricted with IStream::LockRegion.
316 /// </summary>
317 /// <param name="libOffset">Specifies the byte offset for the beginning of the range.</param>
318 /// <param name="cb">Specifies, in bytes, the length of the range to be restricted.</param>
319 /// <param name="dwLockType">Specifies the access restrictions previously placed on the range.</param>
320 void UnlockRegion(long libOffset, long cb, int dwLockType);
321
322 /// <summary>
323 /// Retrieves the STATSTG structure for this stream.
324 /// </summary>
325 /// <param name="pstatstg">Pointer to a STATSTG structure where this method places information about this stream object.</param>
326 /// <param name="grfStatFlag">Specifies that this method does not return some of the members in the STATSTG structure, thus saving a memory allocation operation.</param>
327 void Stat(out STATSTG pstatstg, int grfStatFlag);
328
329 /// <summary>
330 /// Creates a new stream object that references the same bytes as the original stream but provides a separate seek pointer to those bytes.
331 /// </summary>
332 /// <param name="ppstm">When successful, pointer to the location of an IStream pointer to the new stream object.</param>
333 void Clone(out IStream ppstm);
334 }
335
336 /// <summary>
337 /// Creates a new compound file storage object.
338 /// </summary>
339 /// <param name="storageFile">The compound file being created.</param>
340 /// <param name="mode">Specifies the access mode to use when opening the new storage object.</param>
341 /// <returns>The created Storage object.</returns>
342 public static Storage CreateDocFile(string storageFile, StorageMode mode)
343 {
344 IStorage storage = NativeMethods.StgCreateDocfile(storageFile, (uint)mode, 0);
345
346 return new Storage(storage);
347 }
348
349 /// <summary>
350 /// Opens an existing root storage object in the file system.
351 /// </summary>
352 /// <param name="storageFile">The file that contains the storage object to open.</param>
353 /// <param name="mode">Specifies the access mode to use to open the storage object.</param>
354 /// <returns>The created Storage object.</returns>
355 public static Storage Open(string storageFile, StorageMode mode)
356 {
357 IStorage storage = NativeMethods.StgOpenStorage(storageFile, IntPtr.Zero, (uint)mode, IntPtr.Zero, 0);
358
359 return new Storage(storage);
360 }
361
362 /// <summary>
363 /// Copies the entire contents of this open storage object into another Storage object.
364 /// </summary>
365 /// <param name="destinationStorage">The destination Storage object.</param>
366 public void CopyTo(Storage destinationStorage)
367 {
368 this.storage.CopyTo(0, IntPtr.Zero, IntPtr.Zero, destinationStorage.storage);
369 }
370
371 /// <summary>
372 /// Opens an existing Storage object with the specified name according to the specified access mode.
373 /// </summary>
374 /// <param name="name">The name of the Storage object.</param>
375 /// <returns>The opened Storage object.</returns>
376 public Storage OpenStorage(string name)
377 {
378 IStorage subStorage;
379
380 this.storage.OpenStorage(name, null, (uint)(StorageMode.Read | StorageMode.ShareExclusive), IntPtr.Zero, 0, out subStorage);
381
382 return new Storage(subStorage);
383 }
384
385 /// <summary>
386 /// Disposes the managed and unmanaged objects in this object.
387 /// </summary>
388 public void Dispose()
389 {
390 if (!this.disposed)
391 {
392 Marshal.ReleaseComObject(this.storage);
393
394 this.disposed = true;
395 }
396
397 GC.SuppressFinalize(this);
398 }
399
400 /// <summary>
401 /// The native methods.
402 /// </summary>
403 private sealed class NativeMethods
404 {
405 /// <summary>
406 /// Protect the constructor since this class only contains static methods.
407 /// </summary>
408 private NativeMethods()
409 {
410 }
411
412 /// <summary>
413 /// Creates a new compound file storage object.
414 /// </summary>
415 /// <param name="pwcsName">The name for the compound file being created.</param>
416 /// <param name="grfMode">Specifies the access mode to use when opening the new storage object.</param>
417 /// <param name="reserved">Reserved for future use; must be zero.</param>
418 /// <returns>A pointer to the location of the IStorage pointer to the new storage object.</returns>
419 [DllImport("ole32.dll", PreserveSig = false)]
420 [return: MarshalAs(UnmanagedType.Interface)]
421 internal static extern IStorage StgCreateDocfile([MarshalAs(UnmanagedType.LPWStr)] string pwcsName, uint grfMode, uint reserved);
422
423 /// <summary>
424 /// Opens an existing root storage object in the file system.
425 /// </summary>
426 /// <param name="pwcsName">The file that contains the storage object to open.</param>
427 /// <param name="pstgPriority">Most often NULL.</param>
428 /// <param name="grfMode">Specifies the access mode to use to open the storage object.</param>
429 /// <param name="snbExclude">If not NULL, pointer to a block of elements in the storage to be excluded as the storage object is opened.</param>
430 /// <param name="reserved">Indicates reserved for future use; must be zero.</param>
431 /// <returns>A pointer to a IStorage* pointer variable that receives the interface pointer to the opened storage.</returns>
432 [DllImport("ole32.dll", PreserveSig = false)]
433 [return: MarshalAs(UnmanagedType.Interface)]
434 internal static extern IStorage StgOpenStorage([MarshalAs(UnmanagedType.LPWStr)] string pwcsName, IntPtr pstgPriority, uint grfMode, IntPtr snbExclude, uint reserved);
435 }
436 }
437}
diff --git a/src/WixToolset.Core.WindowsInstaller/Patch.cs b/src/WixToolset.Core.WindowsInstaller/Patch.cs
new file mode 100644
index 00000000..67150e32
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Patch.cs
@@ -0,0 +1,1245 @@
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.Data
4{
5 using System;
6 using System.Collections;
7 using System.Collections.Generic;
8 using System.Diagnostics.CodeAnalysis;
9 using System.Globalization;
10 using WixToolset.Data.Rows;
11 using WixToolset.Extensibility;
12 using WixToolset.Core.Native;
13 using WixToolset.Msi;
14
15 /// <summary>
16 /// Contains output tables and logic for building an MSP package.
17 /// </summary>
18 public class Patch
19 {
20 private List<IInspectorExtension> inspectorExtensions;
21 private Output patch;
22 private TableDefinitionCollection tableDefinitions;
23
24 public Output PatchOutput
25 {
26 get { return this.patch; }
27 }
28
29 public Patch()
30 {
31 this.inspectorExtensions = new List<IInspectorExtension>();
32 this.tableDefinitions = new TableDefinitionCollection(WindowsInstallerStandard.GetTableDefinitions());
33 }
34
35 /// <summary>
36 /// Adds an extension.
37 /// </summary>
38 /// <param name="extension">The extension to add.</param>
39 public void AddExtension(IInspectorExtension extension)
40 {
41 this.inspectorExtensions.Add(extension);
42 }
43
44 public void Load(string patchPath)
45 {
46 this.patch = Output.Load(patchPath, false);
47 }
48
49 /// <summary>
50 /// Include transforms in a patch.
51 /// </summary>
52 /// <param name="transforms">List of transforms to attach.</param>
53 [SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters", MessageId = "System.InvalidOperationException.#ctor(System.String)")]
54 public void AttachTransforms(List<PatchTransform> transforms)
55 {
56 // Track if at least one transform gets attached.
57 bool attachedTransform = false;
58
59 if (transforms == null || transforms.Count == 0)
60 {
61 throw new WixException(WixErrors.PatchWithoutTransforms());
62 }
63
64 // Get the patch id from the WixPatchId table.
65 string patchId = null;
66 string clientPatchId = null;
67 Table wixPatchIdTable = this.patch.Tables["WixPatchId"];
68 if (null != wixPatchIdTable && 0 < wixPatchIdTable.Rows.Count)
69 {
70 Row patchIdRow = wixPatchIdTable.Rows[0];
71 if (null != patchIdRow)
72 {
73 patchId = patchIdRow[0].ToString();
74 clientPatchId = patchIdRow[1].ToString();
75 }
76 }
77
78 if (null == patchId)
79 {
80 throw new WixException(WixErrors.ExpectedPatchIdInWixMsp());
81 }
82 if (null == clientPatchId)
83 {
84 throw new WixException(WixErrors.ExpectedClientPatchIdInWixMsp());
85 }
86
87 // enumerate patch.Media to map diskId to Media row
88 Table patchMediaTable = patch.Tables["Media"];
89
90 if (null == patchMediaTable || patchMediaTable.Rows.Count == 0)
91 {
92 throw new WixException(WixErrors.ExpectedMediaRowsInWixMsp());
93 }
94
95 Hashtable mediaRows = new Hashtable(patchMediaTable.Rows.Count);
96 foreach (MediaRow row in patchMediaTable.Rows)
97 {
98 int media = row.DiskId;
99 mediaRows[media] = row;
100 }
101
102 // enumerate patch.WixPatchBaseline to map baseline to diskId
103 Table patchBaselineTable = patch.Tables["WixPatchBaseline"];
104
105 int numPatchBaselineRows = (null != patchBaselineTable) ? patchBaselineTable.Rows.Count : 0;
106
107 Hashtable baselineMedia = new Hashtable(numPatchBaselineRows);
108 if (patchBaselineTable != null)
109 {
110 foreach (Row row in patchBaselineTable.Rows)
111 {
112 string baseline = (string)row[0];
113 int media = (int)row[1];
114 int validationFlags = (int)row[2];
115 if (baselineMedia.Contains(baseline))
116 {
117 this.OnMessage(WixErrors.SamePatchBaselineId(row.SourceLineNumbers, baseline));
118 }
119 baselineMedia[baseline] = new int[] { media, validationFlags };
120 }
121 }
122
123 // populate MSP summary information
124 Table patchSummaryInfo = patch.EnsureTable(this.tableDefinitions["_SummaryInformation"]);
125
126 // Remove properties that will be calculated or are reserved.
127 for (int i = patchSummaryInfo.Rows.Count - 1; i >= 0; i--)
128 {
129 Row row = patchSummaryInfo.Rows[i];
130 switch ((SummaryInformation.Patch)row[0])
131 {
132 case SummaryInformation.Patch.ProductCodes:
133 case SummaryInformation.Patch.TransformNames:
134 case SummaryInformation.Patch.PatchCode:
135 case SummaryInformation.Patch.InstallerRequirement:
136 case SummaryInformation.Patch.Reserved11:
137 case SummaryInformation.Patch.Reserved14:
138 case SummaryInformation.Patch.Reserved16:
139 patchSummaryInfo.Rows.RemoveAt(i);
140 break;
141 }
142 }
143
144 // Index remaining summary properties.
145 SummaryInfoRowCollection summaryInfo = new SummaryInfoRowCollection(patchSummaryInfo);
146
147 // PID_CODEPAGE
148 if (!summaryInfo.Contains((int)SummaryInformation.Patch.CodePage))
149 {
150 // set the code page by default to the same code page for the
151 // string pool in the database.
152 Row codePage = patchSummaryInfo.CreateRow(null);
153 codePage[0] = (int)SummaryInformation.Patch.CodePage;
154 codePage[1] = this.patch.Codepage.ToString(CultureInfo.InvariantCulture);
155 }
156
157 // GUID patch code for the patch.
158 Row revisionRow = patchSummaryInfo.CreateRow(null);
159 revisionRow[0] = (int)SummaryInformation.Patch.PatchCode;
160 revisionRow[1] = patchId;
161
162 // Indicates the minimum Windows Installer version that is required to install the patch.
163 Row wordsRow = patchSummaryInfo.CreateRow(null);
164 wordsRow[0] = (int)SummaryInformation.Patch.InstallerRequirement;
165 wordsRow[1] = ((int)SummaryInformation.InstallerRequirement.Version31).ToString(CultureInfo.InvariantCulture);
166
167 if (!summaryInfo.Contains((int)SummaryInformation.Patch.Security))
168 {
169 Row security = patchSummaryInfo.CreateRow(null);
170 security[0] = (int)SummaryInformation.Patch.Security;
171 security[1] = "4"; // Read-only enforced
172 }
173
174 // use authored comments or default to DisplayName (required)
175 string comments = null;
176
177 Table msiPatchMetadataTable = patch.Tables["MsiPatchMetadata"];
178 Hashtable metadataTable = new Hashtable();
179 if (null != msiPatchMetadataTable)
180 {
181 foreach (Row row in msiPatchMetadataTable.Rows)
182 {
183 metadataTable.Add(row.Fields[1].Data.ToString(), row.Fields[2].Data.ToString());
184 }
185
186 if (!summaryInfo.Contains((int)SummaryInformation.Patch.Title) && metadataTable.Contains("DisplayName"))
187 {
188 string displayName = (string)metadataTable["DisplayName"];
189
190 Row title = patchSummaryInfo.CreateRow(null);
191 title[0] = (int)SummaryInformation.Patch.Title;
192 title[1] = displayName;
193
194 // default comments use DisplayName as-is (no loc)
195 comments = displayName;
196 }
197
198 if (!summaryInfo.Contains((int)SummaryInformation.Patch.CodePage) && metadataTable.Contains("CodePage"))
199 {
200 Row codePage = patchSummaryInfo.CreateRow(null);
201 codePage[0] = (int)SummaryInformation.Patch.CodePage;
202 codePage[1] = metadataTable["CodePage"];
203 }
204
205 if (!summaryInfo.Contains((int)SummaryInformation.Patch.PackageName) && metadataTable.Contains("Description"))
206 {
207 Row subject = patchSummaryInfo.CreateRow(null);
208 subject[0] = (int)SummaryInformation.Patch.PackageName;
209 subject[1] = metadataTable["Description"];
210 }
211
212 if (!summaryInfo.Contains((int)SummaryInformation.Patch.Manufacturer) && metadataTable.Contains("ManufacturerName"))
213 {
214 Row author = patchSummaryInfo.CreateRow(null);
215 author[0] = (int)SummaryInformation.Patch.Manufacturer;
216 author[1] = metadataTable["ManufacturerName"];
217 }
218 }
219
220 // special metadata marshalled through the build
221 Table wixPatchMetadataTable = patch.Tables["WixPatchMetadata"];
222 Hashtable wixMetadataTable = new Hashtable();
223 if (null != wixPatchMetadataTable)
224 {
225 foreach (Row row in wixPatchMetadataTable.Rows)
226 {
227 wixMetadataTable.Add(row.Fields[0].Data.ToString(), row.Fields[1].Data.ToString());
228 }
229
230 if (wixMetadataTable.Contains("Comments"))
231 {
232 comments = (string)wixMetadataTable["Comments"];
233 }
234 }
235
236 // write the package comments to summary info
237 if (!summaryInfo.Contains((int)SummaryInformation.Patch.Comments) && null != comments)
238 {
239 Row commentsRow = patchSummaryInfo.CreateRow(null);
240 commentsRow[0] = (int)SummaryInformation.Patch.Comments;
241 commentsRow[1] = comments;
242 }
243
244 // enumerate transforms
245 Dictionary<string, object> productCodes = new Dictionary<string, object>();
246 ArrayList transformNames = new ArrayList();
247 ArrayList validTransform = new ArrayList();
248 int transformCount = 0;
249 foreach (PatchTransform mainTransform in transforms)
250 {
251 string baseline = null;
252 int media = -1;
253 int validationFlags = 0;
254
255 if (baselineMedia.Contains(mainTransform.Baseline))
256 {
257 int[] baselineData = (int[])baselineMedia[mainTransform.Baseline];
258 int newMedia = baselineData[0];
259 if (media != -1 && media != newMedia)
260 {
261 throw new InvalidOperationException(String.Format(CultureInfo.CurrentUICulture, WixStrings.EXP_TransformAuthoredIntoMultipleMedia, media, newMedia));
262 }
263 baseline = mainTransform.Baseline;
264 media = newMedia;
265 validationFlags = baselineData[1];
266 }
267
268 if (media == -1)
269 {
270 // transform's baseline not attached to any Media
271 continue;
272 }
273
274 Table patchRefTable = patch.Tables["WixPatchRef"];
275 if (patchRefTable != null && patchRefTable.Rows.Count > 0)
276 {
277 if (!Patch.ReduceTransform(mainTransform.Transform, patchRefTable))
278 {
279 // transform has none of the content authored into this patch
280 continue;
281 }
282 }
283
284 // Validate the transform doesn't break any patch specific rules.
285 mainTransform.Validate();
286
287 // ensure consistent File.Sequence within each Media
288 MediaRow mediaRow = (MediaRow)mediaRows[media];
289
290 // Ensure that files are sequenced after the last file in any transform.
291 Table transformMediaTable = mainTransform.Transform.Tables["Media"];
292 if (null != transformMediaTable && 0 < transformMediaTable.Rows.Count)
293 {
294 foreach (MediaRow transformMediaRow in transformMediaTable.Rows)
295 {
296 if (mediaRow.LastSequence < transformMediaRow.LastSequence)
297 {
298 // The Binder will pre-increment the sequence.
299 mediaRow.LastSequence = transformMediaRow.LastSequence;
300 }
301 }
302 }
303
304 // Use the Media/@DiskId if greater for backward compatibility.
305 if (mediaRow.LastSequence < mediaRow.DiskId)
306 {
307 mediaRow.LastSequence = mediaRow.DiskId;
308 }
309
310 // ignore media table from transform.
311 mainTransform.Transform.Tables.Remove("Media");
312 mainTransform.Transform.Tables.Remove("WixMedia");
313 mainTransform.Transform.Tables.Remove("MsiDigitalSignature");
314
315 string productCode;
316 Output pairedTransform = this.BuildPairedTransform(patchId, clientPatchId, mainTransform.Transform, mediaRow, validationFlags, out productCode);
317 productCodes[productCode] = null;
318 DictionaryEntry entry = new DictionaryEntry();
319 entry.Key = productCode;
320 entry.Value = mainTransform.Transform;
321 validTransform.Add(entry);
322
323 // attach these transforms to the patch object
324 // TODO: is this an acceptable way to auto-generate transform stream names?
325 string transformName = baseline + "." + (++transformCount).ToString(CultureInfo.InvariantCulture);
326 patch.SubStorages.Add(new SubStorage(transformName, mainTransform.Transform));
327 patch.SubStorages.Add(new SubStorage("#" + transformName, pairedTransform));
328 transformNames.Add(":" + transformName);
329 transformNames.Add(":#" + transformName);
330 attachedTransform = true;
331 }
332
333 if (!attachedTransform)
334 {
335 throw new WixException(WixErrors.PatchWithoutValidTransforms());
336 }
337
338 // Validate that a patch authored as removable is actually removable
339 if (metadataTable.Contains("AllowRemoval"))
340 {
341 if ("1" == metadataTable["AllowRemoval"].ToString())
342 {
343 ArrayList tables = Patch.GetPatchUninstallBreakingTables();
344 bool result = true;
345 foreach (DictionaryEntry entry in validTransform)
346 {
347 result &= this.CheckUninstallableTransform(entry.Key.ToString(), (Output)entry.Value, tables);
348 }
349
350 if (!result)
351 {
352 throw new WixException(WixErrors.PatchNotRemovable());
353 }
354 }
355 }
356
357 // Finish filling tables with transform-dependent data.
358 // Semicolon delimited list of the product codes that can accept the patch.
359 Table wixPatchTargetTable = patch.Tables["WixPatchTarget"];
360 if (null != wixPatchTargetTable)
361 {
362 Dictionary<string, object> targets = new Dictionary<string, object>();
363 bool replace = true;
364 foreach (Row wixPatchTargetRow in wixPatchTargetTable.Rows)
365 {
366 string target = wixPatchTargetRow[0].ToString();
367 if (0 == String.CompareOrdinal("*", target))
368 {
369 replace = false;
370 }
371 else
372 {
373 targets[target] = null;
374 }
375 }
376
377 // Replace the target ProductCodes with the authored list.
378 if (replace)
379 {
380 productCodes = targets;
381 }
382 else
383 {
384 // Copy the authored target ProductCodes into the list.
385 foreach (string target in targets.Keys)
386 {
387 productCodes[target] = null;
388 }
389 }
390 }
391
392 string[] uniqueProductCodes = new string[productCodes.Keys.Count];
393 productCodes.Keys.CopyTo(uniqueProductCodes, 0);
394
395 Row templateRow = patchSummaryInfo.CreateRow(null);
396 templateRow[0] = (int)SummaryInformation.Patch.ProductCodes;
397 templateRow[1] = String.Join(";", uniqueProductCodes);
398
399 // Semicolon delimited list of transform substorage names in the order they are applied.
400 Row savedbyRow = patchSummaryInfo.CreateRow(null);
401 savedbyRow[0] = (int)SummaryInformation.Patch.TransformNames;
402 savedbyRow[1] = String.Join(";", (string[])transformNames.ToArray(typeof(string)));
403 }
404
405 /// <summary>
406 /// Ensure transform is uninstallable.
407 /// </summary>
408 /// <param name="productCode">Product code in transform.</param>
409 /// <param name="transform">Transform generated by torch.</param>
410 /// <param name="tables">Tables to be checked</param>
411 /// <returns>True if the transform is uninstallable</returns>
412 private bool CheckUninstallableTransform(string productCode, Output transform, ArrayList tables)
413 {
414 bool ret = true;
415 foreach (string table in tables)
416 {
417 Table wixTable = transform.Tables[table];
418 if (null != wixTable)
419 {
420 foreach (Row row in wixTable.Rows)
421 {
422 if (row.Operation == RowOperation.Add)
423 {
424 ret = false;
425 string primaryKey = row.GetPrimaryKey('/');
426 if (null == primaryKey)
427 {
428 primaryKey = string.Empty;
429 }
430 this.OnMessage(WixErrors.NewRowAddedInTable(row.SourceLineNumbers, productCode, wixTable.Name, primaryKey));
431 }
432 }
433 }
434 }
435
436 return ret;
437 }
438
439 /// <summary>
440 /// Tables affect patch uninstall.
441 /// </summary>
442 /// <returns>list of tables to be checked</returns>
443 private static ArrayList GetPatchUninstallBreakingTables()
444 {
445 ArrayList tables = new ArrayList();
446 tables.Add("AppId");
447 tables.Add("BindImage");
448 tables.Add("Class");
449 tables.Add("Complus");
450 tables.Add("CreateFolder");
451 tables.Add("DuplicateFile");
452 tables.Add("Environment");
453 tables.Add("Extension");
454 tables.Add("Font");
455 tables.Add("IniFile");
456 tables.Add("IsolatedComponent");
457 tables.Add("LockPermissions");
458 tables.Add("MIME");
459 tables.Add("MoveFile");
460 tables.Add("MsiLockPermissionsEx");
461 tables.Add("MsiServiceConfig");
462 tables.Add("MsiServiceConfigFailureActions");
463 tables.Add("ODBCAttribute");
464 tables.Add("ODBCDataSource");
465 tables.Add("ODBCDriver");
466 tables.Add("ODBCSourceAttribute");
467 tables.Add("ODBCTranslator");
468 tables.Add("ProgId");
469 tables.Add("PublishComponent");
470 tables.Add("RemoveIniFile");
471 tables.Add("SelfReg");
472 tables.Add("ServiceControl");
473 tables.Add("ServiceInstall");
474 tables.Add("TypeLib");
475 tables.Add("Verb");
476
477 return tables;
478 }
479
480 /// <summary>
481 /// Reduce the transform according to the patch references.
482 /// </summary>
483 /// <param name="transform">transform generated by torch.</param>
484 /// <param name="patchRefTable">Table contains patch family filter.</param>
485 /// <returns>true if the transform is not empty</returns>
486 public static bool ReduceTransform(Output transform, Table patchRefTable)
487 {
488 // identify sections to keep
489 Hashtable oldSections = new Hashtable(patchRefTable.Rows.Count);
490 Hashtable newSections = new Hashtable(patchRefTable.Rows.Count);
491 Hashtable tableKeyRows = new Hashtable();
492 ArrayList sequenceList = new ArrayList();
493 Hashtable componentFeatureAddsIndex = new Hashtable();
494 Hashtable customActionTable = new Hashtable();
495 Hashtable directoryTableAdds = new Hashtable();
496 Hashtable featureTableAdds = new Hashtable();
497 Hashtable keptComponents = new Hashtable();
498 Hashtable keptDirectories = new Hashtable();
499 Hashtable keptFeatures = new Hashtable();
500 Hashtable keptLockPermissions = new Hashtable();
501 Hashtable keptMsiLockPermissionExs = new Hashtable();
502
503 Dictionary<string, List<string>> componentCreateFolderIndex = new Dictionary<string, List<string>>();
504 Dictionary<string, List<Row>> directoryLockPermissionsIndex = new Dictionary<string, List<Row>>();
505 Dictionary<string, List<Row>> directoryMsiLockPermissionsExIndex = new Dictionary<string, List<Row>>();
506
507 foreach (Row patchRefRow in patchRefTable.Rows)
508 {
509 string tableName = (string)patchRefRow[0];
510 string key = (string)patchRefRow[1];
511
512 // Short circuit filtering if all changes should be included.
513 if ("*" == tableName && "*" == key)
514 {
515 Patch.RemoveProductCodeFromTransform(transform);
516 return true;
517 }
518
519 Table table = transform.Tables[tableName];
520 if (table == null)
521 {
522 // table not found
523 continue;
524 }
525
526 // index this table
527 if (!tableKeyRows.Contains(tableName))
528 {
529 Hashtable newKeyRows = new Hashtable();
530 foreach (Row newRow in table.Rows)
531 {
532 newKeyRows[newRow.GetPrimaryKey('/')] = newRow;
533 }
534 tableKeyRows[tableName] = newKeyRows;
535 }
536 Hashtable keyRows = (Hashtable)tableKeyRows[tableName];
537
538 Row row = (Row)keyRows[key];
539 if (row == null)
540 {
541 // row not found
542 continue;
543 }
544
545 // Differ.sectionDelimiter
546 string[] sections = row.SectionId.Split('/');
547 oldSections[sections[0]] = row;
548 newSections[sections[1]] = row;
549 }
550
551 // throw away sections not referenced
552 int keptRows = 0;
553 Table directoryTable = null;
554 Table featureTable = null;
555 Table lockPermissionsTable = null;
556 Table msiLockPermissionsTable = null;
557
558 foreach (Table table in transform.Tables)
559 {
560 if ("_SummaryInformation" == table.Name)
561 {
562 continue;
563 }
564
565 if (table.Name == "AdminExecuteSequence"
566 || table.Name == "AdminUISequence"
567 || table.Name == "AdvtExecuteSequence"
568 || table.Name == "InstallUISequence"
569 || table.Name == "InstallExecuteSequence")
570 {
571 sequenceList.Add(table);
572 continue;
573 }
574
575 for (int i = 0; i < table.Rows.Count; i++)
576 {
577 Row row = table.Rows[i];
578
579 if (table.Name == "CreateFolder")
580 {
581 string createFolderComponentId = (string)row[1];
582
583 List<string> directoryList;
584 if (!componentCreateFolderIndex.TryGetValue(createFolderComponentId, out directoryList))
585 {
586 directoryList = new List<string>();
587 componentCreateFolderIndex.Add(createFolderComponentId, directoryList);
588 }
589
590 directoryList.Add((string)row[0]);
591 }
592
593 if (table.Name == "CustomAction")
594 {
595 customActionTable.Add(row[0], row);
596 }
597
598 if (table.Name == "Directory")
599 {
600 directoryTable = table;
601 if (RowOperation.Add == row.Operation)
602 {
603 directoryTableAdds.Add(row[0], row);
604 }
605 }
606
607 if (table.Name == "Feature")
608 {
609 featureTable = table;
610 if (RowOperation.Add == row.Operation)
611 {
612 featureTableAdds.Add(row[0], row);
613 }
614 }
615
616 if (table.Name == "FeatureComponents")
617 {
618 if (RowOperation.Add == row.Operation)
619 {
620 string featureId = (string)row[0];
621 string componentId = (string)row[1];
622
623 if (componentFeatureAddsIndex.ContainsKey(componentId))
624 {
625 ArrayList featureList = (ArrayList)componentFeatureAddsIndex[componentId];
626 featureList.Add(featureId);
627 }
628 else
629 {
630 ArrayList featureList = new ArrayList();
631 componentFeatureAddsIndex.Add(componentId, featureList);
632 featureList.Add(featureId);
633 }
634 }
635 }
636
637 if (table.Name == "LockPermissions")
638 {
639 lockPermissionsTable = table;
640 if ("CreateFolder" == (string)row[1])
641 {
642 string directoryId = (string)row[0];
643
644 List<Row> rowList;
645 if (!directoryLockPermissionsIndex.TryGetValue(directoryId, out rowList))
646 {
647 rowList = new List<Row>();
648 directoryLockPermissionsIndex.Add(directoryId, rowList);
649 }
650
651 rowList.Add(row);
652 }
653 }
654
655 if (table.Name == "MsiLockPermissionsEx")
656 {
657 msiLockPermissionsTable = table;
658 if ("CreateFolder" == (string)row[1])
659 {
660 string directoryId = (string)row[0];
661
662 List<Row> rowList;
663 if (!directoryMsiLockPermissionsExIndex.TryGetValue(directoryId, out rowList))
664 {
665 rowList = new List<Row>();
666 directoryMsiLockPermissionsExIndex.Add(directoryId, rowList);
667 }
668
669 rowList.Add(row);
670 }
671 }
672
673 if (null == row.SectionId)
674 {
675 table.Rows.RemoveAt(i);
676 i--;
677 }
678 else
679 {
680 string[] sections = row.SectionId.Split('/');
681 // ignore the row without section id.
682 if (0 == sections[0].Length && 0 == sections[1].Length)
683 {
684 table.Rows.RemoveAt(i);
685 i--;
686 }
687 else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections))
688 {
689 if ("Component" == table.Name)
690 {
691 keptComponents.Add((string)row[0], row);
692 }
693
694 if ("Directory" == table.Name)
695 {
696 keptDirectories.Add(row[0], row);
697 }
698
699 if ("Feature" == table.Name)
700 {
701 keptFeatures.Add(row[0], row);
702 }
703
704 keptRows++;
705 }
706 else
707 {
708 table.Rows.RemoveAt(i);
709 i--;
710 }
711 }
712 }
713 }
714
715 keptRows += ReduceTransformSequenceTable(sequenceList, oldSections, newSections, customActionTable);
716
717 if (null != directoryTable)
718 {
719 foreach (Row componentRow in keptComponents.Values)
720 {
721 string componentId = (string)componentRow[0];
722
723 if (RowOperation.Add == componentRow.Operation)
724 {
725 // make sure each added component has its required directory and feature heirarchy.
726 string directoryId = (string)componentRow[2];
727 while (null != directoryId && directoryTableAdds.ContainsKey(directoryId))
728 {
729 Row directoryRow = (Row)directoryTableAdds[directoryId];
730
731 if (!keptDirectories.ContainsKey(directoryId))
732 {
733 directoryTable.Rows.Add(directoryRow);
734 keptDirectories.Add(directoryRow[0], null);
735 keptRows++;
736 }
737
738 directoryId = (string)directoryRow[1];
739 }
740
741 if (componentFeatureAddsIndex.ContainsKey(componentId))
742 {
743 foreach (string featureId in (ArrayList)componentFeatureAddsIndex[componentId])
744 {
745 string currentFeatureId = featureId;
746 while (null != currentFeatureId && featureTableAdds.ContainsKey(currentFeatureId))
747 {
748 Row featureRow = (Row)featureTableAdds[currentFeatureId];
749
750 if (!keptFeatures.ContainsKey(currentFeatureId))
751 {
752 featureTable.Rows.Add(featureRow);
753 keptFeatures.Add(featureRow[0], null);
754 keptRows++;
755 }
756
757 currentFeatureId = (string)featureRow[1];
758 }
759 }
760 }
761 }
762
763 // Hook in changes LockPermissions and MsiLockPermissions for folders for each component that has been kept.
764 foreach (string keptComponentId in keptComponents.Keys)
765 {
766 List<string> directoryList;
767 if (componentCreateFolderIndex.TryGetValue(keptComponentId, out directoryList))
768 {
769 foreach (string directoryId in directoryList)
770 {
771 List<Row> lockPermissionsRowList;
772 if (directoryLockPermissionsIndex.TryGetValue(directoryId, out lockPermissionsRowList))
773 {
774 foreach (Row lockPermissionsRow in lockPermissionsRowList)
775 {
776 string key = lockPermissionsRow.GetPrimaryKey('/');
777 if (!keptLockPermissions.ContainsKey(key))
778 {
779 lockPermissionsTable.Rows.Add(lockPermissionsRow);
780 keptLockPermissions.Add(key, null);
781 keptRows++;
782 }
783 }
784 }
785
786 List<Row> msiLockPermissionsExRowList;
787 if (directoryMsiLockPermissionsExIndex.TryGetValue(directoryId, out msiLockPermissionsExRowList))
788 {
789 foreach (Row msiLockPermissionsExRow in msiLockPermissionsExRowList)
790 {
791 string key = msiLockPermissionsExRow.GetPrimaryKey('/');
792 if (!keptMsiLockPermissionExs.ContainsKey(key))
793 {
794 msiLockPermissionsTable.Rows.Add(msiLockPermissionsExRow);
795 keptMsiLockPermissionExs.Add(key, null);
796 keptRows++;
797 }
798 }
799 }
800 }
801 }
802 }
803 }
804 }
805
806 keptRows += ReduceTransformSequenceTable(sequenceList, oldSections, newSections, customActionTable);
807
808 // Delete tables that are empty.
809 ArrayList tablesToDelete = new ArrayList();
810 foreach (Table table in transform.Tables)
811 {
812 if (0 == table.Rows.Count)
813 {
814 tablesToDelete.Add(table.Name);
815 }
816 }
817
818 // delete separately to avoid messing up enumeration
819 foreach (string tableName in tablesToDelete)
820 {
821 transform.Tables.Remove(tableName);
822 }
823
824 return keptRows > 0;
825 }
826
827 /// <summary>
828 /// Remove the ProductCode property from the transform.
829 /// </summary>
830 /// <param name="transform">The transform.</param>
831 /// <remarks>
832 /// Changing the ProductCode is not supported in a patch.
833 /// </remarks>
834 private static void RemoveProductCodeFromTransform(Output transform)
835 {
836 Table propertyTable = transform.Tables["Property"];
837 if (null != propertyTable)
838 {
839 for (int i = 0; i < propertyTable.Rows.Count; ++i)
840 {
841 Row propertyRow = propertyTable.Rows[i];
842 string property = (string)propertyRow[0];
843
844 if ("ProductCode" == property)
845 {
846 propertyTable.Rows.RemoveAt(i);
847 break;
848 }
849 }
850 }
851 }
852
853 /// <summary>
854 /// Check if the section is in a PatchFamily.
855 /// </summary>
856 /// <param name="oldSection">Section id in target wixout</param>
857 /// <param name="newSection">Section id in upgrade wixout</param>
858 /// <param name="oldSections">Hashtable contains section id should be kept in the baseline wixout.</param>
859 /// <param name="newSections">Hashtable contains section id should be kept in the upgrade wixout.</param>
860 /// <returns>true if section in patch family</returns>
861 private static bool IsInPatchFamily(string oldSection, string newSection, Hashtable oldSections, Hashtable newSections)
862 {
863 bool result = false;
864
865 if ((String.IsNullOrEmpty(oldSection) && newSections.Contains(newSection)) || (String.IsNullOrEmpty(newSection) && oldSections.Contains(oldSection)))
866 {
867 result = true;
868 }
869 else if (!String.IsNullOrEmpty(oldSection) && !String.IsNullOrEmpty(newSection) && (oldSections.Contains(oldSection) || newSections.Contains(newSection)))
870 {
871 result = true;
872 }
873
874 return result;
875 }
876
877 /// <summary>
878 /// Reduce the transform sequence tables.
879 /// </summary>
880 /// <param name="sequenceList">ArrayList of tables to be reduced</param>
881 /// <param name="oldSections">Hashtable contains section id should be kept in the baseline wixout.</param>
882 /// <param name="newSections">Hashtable contains section id should be kept in the target wixout.</param>
883 /// <param name="customAction">Hashtable contains all the rows in the CustomAction table.</param>
884 /// <returns>Number of rows left</returns>
885 private static int ReduceTransformSequenceTable(ArrayList sequenceList, Hashtable oldSections, Hashtable newSections, Hashtable customAction)
886 {
887 int keptRows = 0;
888
889 foreach (Table currentTable in sequenceList)
890 {
891 for (int i = 0; i < currentTable.Rows.Count; i++)
892 {
893 Row row = currentTable.Rows[i];
894 string actionName = row.Fields[0].Data.ToString();
895 string[] sections = row.SectionId.Split('/');
896 bool isSectionIdEmpty = (sections[0].Length == 0 && sections[1].Length == 0);
897
898 if (row.Operation == RowOperation.None)
899 {
900 // ignore the rows without section id.
901 if (isSectionIdEmpty)
902 {
903 currentTable.Rows.RemoveAt(i);
904 i--;
905 }
906 else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections))
907 {
908 keptRows++;
909 }
910 else
911 {
912 currentTable.Rows.RemoveAt(i);
913 i--;
914 }
915 }
916 else if (row.Operation == RowOperation.Modify)
917 {
918 bool sequenceChanged = row.Fields[2].Modified;
919 bool conditionChanged = row.Fields[1].Modified;
920
921 if (sequenceChanged && !conditionChanged)
922 {
923 keptRows++;
924 }
925 else if (!sequenceChanged && conditionChanged)
926 {
927 if (isSectionIdEmpty)
928 {
929 currentTable.Rows.RemoveAt(i);
930 i--;
931 }
932 else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections))
933 {
934 keptRows++;
935 }
936 else
937 {
938 currentTable.Rows.RemoveAt(i);
939 i--;
940 }
941 }
942 else if (sequenceChanged && conditionChanged)
943 {
944 if (isSectionIdEmpty)
945 {
946 row.Fields[1].Modified = false;
947 keptRows++;
948 }
949 else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections))
950 {
951 keptRows++;
952 }
953 else
954 {
955 row.Fields[1].Modified = false;
956 keptRows++;
957 }
958 }
959 }
960 else if (row.Operation == RowOperation.Delete)
961 {
962 if (isSectionIdEmpty)
963 {
964 // it is a stardard action which is added by wix, we should keep this action.
965 row.Operation = RowOperation.None;
966 keptRows++;
967 }
968 else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections))
969 {
970 keptRows++;
971 }
972 else
973 {
974 if (customAction.ContainsKey(actionName))
975 {
976 currentTable.Rows.RemoveAt(i);
977 i--;
978 }
979 else
980 {
981 // it is a stardard action, we should keep this action.
982 row.Operation = RowOperation.None;
983 keptRows++;
984 }
985 }
986 }
987 else if (row.Operation == RowOperation.Add)
988 {
989 if (isSectionIdEmpty)
990 {
991 keptRows++;
992 }
993 else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections))
994 {
995 keptRows++;
996 }
997 else
998 {
999 if (customAction.ContainsKey(actionName))
1000 {
1001 currentTable.Rows.RemoveAt(i);
1002 i--;
1003 }
1004 else
1005 {
1006 keptRows++;
1007 }
1008 }
1009 }
1010 }
1011 }
1012
1013 return keptRows;
1014 }
1015
1016 /// <summary>
1017 /// Create the #transform for the given main transform.
1018 /// </summary>
1019 /// <param name="patchId">Patch GUID from patch authoring.</param>
1020 /// <param name="clientPatchId">Easily referenced identity for this patch.</param>
1021 /// <param name="mainTransform">Transform generated by torch.</param>
1022 /// <param name="mediaRow">Media authored into patch.</param>
1023 /// <param name="validationFlags">Transform validation flags for the summary information stream.</param>
1024 /// <param name="productCode">Output string to receive ProductCode.</param>
1025 [SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters", MessageId = "System.InvalidOperationException.#ctor(System.String)")]
1026 public Output BuildPairedTransform(string patchId, string clientPatchId, Output mainTransform, MediaRow mediaRow, int validationFlags, out string productCode)
1027 {
1028 productCode = null;
1029 Output pairedTransform = new Output(null);
1030 pairedTransform.Type = OutputType.Transform;
1031 pairedTransform.Codepage = mainTransform.Codepage;
1032
1033 // lookup productVersion property to correct summaryInformation
1034 string newProductVersion = null;
1035 Table mainPropertyTable = mainTransform.Tables["Property"];
1036 if (null != mainPropertyTable)
1037 {
1038 foreach (Row row in mainPropertyTable.Rows)
1039 {
1040 if ("ProductVersion" == (string)row[0])
1041 {
1042 newProductVersion = (string)row[1];
1043 }
1044 }
1045 }
1046
1047 // TODO: build class for manipulating SummaryInformation table
1048 Table mainSummaryTable = mainTransform.Tables["_SummaryInformation"];
1049 // add required properties
1050 Hashtable mainSummaryRows = new Hashtable();
1051 foreach (Row mainSummaryRow in mainSummaryTable.Rows)
1052 {
1053 mainSummaryRows[mainSummaryRow[0]] = mainSummaryRow;
1054 }
1055 if (!mainSummaryRows.Contains((int)SummaryInformation.Transform.ValidationFlags))
1056 {
1057 Row mainSummaryRow = mainSummaryTable.CreateRow(null);
1058 mainSummaryRow[0] = (int)SummaryInformation.Transform.ValidationFlags;
1059 mainSummaryRow[1] = validationFlags.ToString(CultureInfo.InvariantCulture);
1060 }
1061
1062 // copy summary information from core transform
1063 Table pairedSummaryTable = pairedTransform.EnsureTable(this.tableDefinitions["_SummaryInformation"]);
1064 foreach (Row mainSummaryRow in mainSummaryTable.Rows)
1065 {
1066 string value = (string)mainSummaryRow[1];
1067 switch ((SummaryInformation.Transform)mainSummaryRow[0])
1068 {
1069 case SummaryInformation.Transform.ProductCodes:
1070 string[] propertyData = value.Split(';');
1071 string oldProductVersion = propertyData[0].Substring(38);
1072 string upgradeCode = propertyData[2];
1073 productCode = propertyData[0].Substring(0, 38);
1074 if (newProductVersion == null)
1075 {
1076 newProductVersion = oldProductVersion;
1077 }
1078
1079 // force mainTranform to old;new;upgrade and pairedTransform to new;new;upgrade
1080 mainSummaryRow[1] = String.Concat(productCode, oldProductVersion, ';', productCode, newProductVersion, ';', upgradeCode);
1081 value = String.Concat(productCode, newProductVersion, ';', productCode, newProductVersion, ';', upgradeCode);
1082 break;
1083 case SummaryInformation.Transform.ValidationFlags:
1084 // use validation flags authored into the patch XML
1085 mainSummaryRow[1] = value = validationFlags.ToString(CultureInfo.InvariantCulture);
1086 break;
1087 }
1088 Row pairedSummaryRow = pairedSummaryTable.CreateRow(null);
1089 pairedSummaryRow[0] = mainSummaryRow[0];
1090 pairedSummaryRow[1] = value;
1091 }
1092
1093 if (productCode == null)
1094 {
1095 throw new InvalidOperationException(WixStrings.EXP_CouldnotDetermineProductCodeFromTransformSummaryInfo);
1096 }
1097
1098 // copy File table
1099 Table mainFileTable = mainTransform.Tables["File"];
1100 if (null != mainFileTable && 0 < mainFileTable.Rows.Count)
1101 {
1102 // We require file source information.
1103 Table mainWixFileTable = mainTransform.Tables["WixFile"];
1104 if (null == mainWixFileTable)
1105 {
1106 throw new WixException(WixErrors.AdminImageRequired(productCode));
1107 }
1108
1109 RowDictionary<FileRow> mainFileRows = new RowDictionary<FileRow>(mainFileTable);
1110
1111 Table pairedFileTable = pairedTransform.EnsureTable(mainFileTable.Definition);
1112 foreach (WixFileRow mainWixFileRow in mainWixFileTable.Rows)
1113 {
1114 FileRow mainFileRow = mainFileRows[mainWixFileRow.File];
1115
1116 // set File.Sequence to non null to satisfy transform bind
1117 mainFileRow.Sequence = 1;
1118
1119 // delete's don't need rows in the paired transform
1120 if (mainFileRow.Operation == RowOperation.Delete)
1121 {
1122 continue;
1123 }
1124
1125 FileRow pairedFileRow = (FileRow)pairedFileTable.CreateRow(null);
1126 pairedFileRow.Operation = RowOperation.Modify;
1127 for (int i = 0; i < mainFileRow.Fields.Length; i++)
1128 {
1129 pairedFileRow[i] = mainFileRow[i];
1130 }
1131
1132 // override authored media for patch bind
1133 mainWixFileRow.DiskId = mediaRow.DiskId;
1134
1135 // suppress any change to File.Sequence to avoid bloat
1136 mainFileRow.Fields[7].Modified = false;
1137
1138 // force File row to appear in the transform
1139 switch (mainFileRow.Operation)
1140 {
1141 case RowOperation.Modify:
1142 case RowOperation.Add:
1143 // set msidbFileAttributesPatchAdded
1144 pairedFileRow.Attributes |= MsiInterop.MsidbFileAttributesPatchAdded;
1145 pairedFileRow.Fields[6].Modified = true;
1146 pairedFileRow.Operation = mainFileRow.Operation;
1147 break;
1148 default:
1149 pairedFileRow.Fields[6].Modified = false;
1150 break;
1151 }
1152 }
1153 }
1154
1155 // add Media row to pairedTransform
1156 Table pairedMediaTable = pairedTransform.EnsureTable(this.tableDefinitions["Media"]);
1157 Row pairedMediaRow = pairedMediaTable.CreateRow(null);
1158 pairedMediaRow.Operation = RowOperation.Add;
1159 for (int i = 0; i < mediaRow.Fields.Length; i++)
1160 {
1161 pairedMediaRow[i] = mediaRow[i];
1162 }
1163
1164 // add PatchPackage for this Media
1165 Table pairedPackageTable = pairedTransform.EnsureTable(this.tableDefinitions["PatchPackage"]);
1166 pairedPackageTable.Operation = TableOperation.Add;
1167 Row pairedPackageRow = pairedPackageTable.CreateRow(null);
1168 pairedPackageRow.Operation = RowOperation.Add;
1169 pairedPackageRow[0] = patchId;
1170 pairedPackageRow[1] = mediaRow.DiskId;
1171
1172 // add property to both identify client patches and whether those patches are removable or not
1173 int allowRemoval = 0;
1174 Table msiPatchMetadataTable = this.patch.Tables["MsiPatchMetadata"];
1175 if (null != msiPatchMetadataTable)
1176 {
1177 foreach (Row msiPatchMetadataRow in msiPatchMetadataTable.Rows)
1178 {
1179 // get the value of the standard AllowRemoval property, if present
1180 string company = (string)msiPatchMetadataRow[0];
1181 if ((null == company || 0 == company.Length) && "AllowRemoval" == (string)msiPatchMetadataRow[1])
1182 {
1183 allowRemoval = Int32.Parse((string)msiPatchMetadataRow[2], CultureInfo.InvariantCulture);
1184 }
1185 }
1186 }
1187
1188 // add the property to the patch transform's Property table
1189 Table pairedPropertyTable = pairedTransform.EnsureTable(this.tableDefinitions["Property"]);
1190 pairedPropertyTable.Operation = TableOperation.Add;
1191 Row pairedPropertyRow = pairedPropertyTable.CreateRow(null);
1192 pairedPropertyRow.Operation = RowOperation.Add;
1193 pairedPropertyRow[0] = string.Concat(clientPatchId, ".AllowRemoval");
1194 pairedPropertyRow[1] = allowRemoval.ToString(CultureInfo.InvariantCulture);
1195
1196 // add this patch code GUID to the patch transform to identify
1197 // which patches are installed, including in multi-patch
1198 // installations.
1199 pairedPropertyRow = pairedPropertyTable.CreateRow(null);
1200 pairedPropertyRow.Operation = RowOperation.Add;
1201 pairedPropertyRow[0] = string.Concat(clientPatchId, ".PatchCode");
1202 pairedPropertyRow[1] = patchId;
1203
1204 // add PATCHNEWPACKAGECODE to apply to admin layouts
1205 pairedPropertyRow = pairedPropertyTable.CreateRow(null);
1206 pairedPropertyRow.Operation = RowOperation.Add;
1207 pairedPropertyRow[0] = "PATCHNEWPACKAGECODE";
1208 pairedPropertyRow[1] = patchId;
1209
1210 // add PATCHNEWSUMMARYCOMMENTS and PATCHNEWSUMMARYSUBJECT to apply to admin layouts
1211 Table _summaryInformationTable = this.patch.Tables["_SummaryInformation"];
1212 if (null != _summaryInformationTable)
1213 {
1214 foreach (Row row in _summaryInformationTable.Rows)
1215 {
1216 if (3 == (int)row[0]) // PID_SUBJECT
1217 {
1218 pairedPropertyRow = pairedPropertyTable.CreateRow(null);
1219 pairedPropertyRow.Operation = RowOperation.Add;
1220 pairedPropertyRow[0] = "PATCHNEWSUMMARYSUBJECT";
1221 pairedPropertyRow[1] = row[1];
1222 }
1223 else if (6 == (int)row[0]) // PID_COMMENTS
1224 {
1225 pairedPropertyRow = pairedPropertyTable.CreateRow(null);
1226 pairedPropertyRow.Operation = RowOperation.Add;
1227 pairedPropertyRow[0] = "PATCHNEWSUMMARYCOMMENTS";
1228 pairedPropertyRow[1] = row[1];
1229 }
1230 }
1231 }
1232
1233 return pairedTransform;
1234 }
1235
1236 /// <summary>
1237 /// Sends a message to the message delegate if there is one.
1238 /// </summary>
1239 /// <param name="mea">Message event arguments.</param>
1240 public void OnMessage(MessageEventArgs mea)
1241 {
1242 Messaging.Instance.OnMessage(mea);
1243 }
1244 }
1245}
diff --git a/src/WixToolset.Core.WindowsInstaller/PatchAPI/PatchInterop.cs b/src/WixToolset.Core.WindowsInstaller/PatchAPI/PatchInterop.cs
new file mode 100644
index 00000000..fcd749d2
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/PatchAPI/PatchInterop.cs
@@ -0,0 +1,989 @@
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.PatchAPI
4{
5 using System;
6 using System.Collections.Generic;
7 using System.Diagnostics.CodeAnalysis;
8 using System.Globalization;
9 using System.Runtime.InteropServices;
10 using WixToolset.Core;
11
12 /// <summary>
13 /// Interop class for the mspatchc.dll.
14 /// </summary>
15 internal static class PatchInterop
16 {
17 // From WinError.h in the Platform SDK
18 internal const ushort FACILITY_WIN32 = 7;
19
20 /// <summary>
21 /// Parse a number from text in either hex or decimal.
22 /// </summary>
23 /// <param name="source">Source value. Treated as hex if it starts 0x (or 0X), decimal otherwise.</param>
24 /// <returns>Numeric value that source represents.</returns>
25 static internal UInt32 ParseHexOrDecimal(string source)
26 {
27 string value = source.Trim();
28 if (String.Equals(value.Substring(0,2), "0x", StringComparison.OrdinalIgnoreCase))
29 {
30 return UInt32.Parse(value.Substring(2), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture.NumberFormat);
31 }
32 else
33 {
34 return UInt32.Parse(value, CultureInfo.InvariantCulture.NumberFormat);
35 }
36 }
37
38 /// <summary>
39 /// Create a binary delta file.
40 /// </summary>
41 /// <param name="deltaFile">Name of the delta file to create.</param>
42 /// <param name="targetFile">Name of updated file.</param>
43 /// <param name="targetSymbolPath">Optional paths to updated file's symbols.</param>
44 /// <param name="targetRetainOffsets">Optional offsets to the delta retain sections in the updated file.</param>
45 /// <param name="basisFiles">Optional array of target files.</param>
46 /// <param name="basisSymbolPaths">Optional array of target files' symbol paths (must match basisFiles array).</param>
47 /// <param name="basisIgnoreLengths">Optional array of target files' delta ignore section lengths (must match basisFiles array)(each entry must match basisIgnoreOffsets entries).</param>
48 /// <param name="basisIgnoreOffsets">Optional array of target files' delta ignore section offsets (must match basisFiles array)(each entry must match basisIgnoreLengths entries).</param>
49 /// <param name="basisRetainLengths">Optional array of target files' delta protect section lengths (must match basisFiles array)(each entry must match basisRetainOffsets and targetRetainOffsets entries).</param>
50 /// <param name="basisRetainOffsets">Optional array of target files' delta protect section offsets (must match basisFiles array)(each entry must match basisRetainLengths and targetRetainOffsets entries).</param>
51 /// <param name="apiPatchingSymbolFlags">ApiPatchingSymbolFlags value.</param>
52 /// <param name="optimizePatchSizeForLargeFiles">OptimizePatchSizeForLargeFiles value.</param>
53 /// <param name="retainRangesIgnored">Flag to indicate retain ranges were ignored due to mismatch.</param>
54 /// <returns>true if delta file was created, false if whole file should be used instead.</returns>
55 static public bool CreateDelta(
56 string deltaFile,
57 string targetFile,
58 string targetSymbolPath,
59 string targetRetainOffsets,
60 string[] basisFiles,
61 string[] basisSymbolPaths,
62 string[] basisIgnoreLengths,
63 string[] basisIgnoreOffsets,
64 string[] basisRetainLengths,
65 string[] basisRetainOffsets,
66 PatchSymbolFlagsType apiPatchingSymbolFlags,
67 bool optimizePatchSizeForLargeFiles,
68 out bool retainRangesIgnored
69 )
70 {
71 retainRangesIgnored = false;
72 if (0 != (apiPatchingSymbolFlags & ~(PatchSymbolFlagsType.PATCH_SYMBOL_NO_IMAGEHLP | PatchSymbolFlagsType.PATCH_SYMBOL_NO_FAILURES | PatchSymbolFlagsType.PATCH_SYMBOL_UNDECORATED_TOO)))
73 {
74 throw new ArgumentOutOfRangeException("apiPatchingSymbolFlags");
75 }
76
77 if (null == deltaFile || 0 == deltaFile.Length)
78 {
79 throw new ArgumentNullException("deltaFile");
80 }
81
82 if (null == targetFile || 0 == targetFile.Length)
83 {
84 throw new ArgumentNullException("targetFile");
85 }
86
87 if (null == basisFiles || 0 == basisFiles.Length)
88 {
89 return false;
90 }
91 uint countOldFiles = (uint) basisFiles.Length;
92
93 if (null != basisSymbolPaths)
94 {
95 if (0 != basisSymbolPaths.Length)
96 {
97 if ((uint) basisSymbolPaths.Length != countOldFiles)
98 {
99 throw new ArgumentOutOfRangeException("basisSymbolPaths");
100 }
101 }
102 }
103 // a null basisSymbolPaths is allowed.
104
105 if (null != basisIgnoreLengths)
106 {
107 if (0 != basisIgnoreLengths.Length)
108 {
109 if ((uint) basisIgnoreLengths.Length != countOldFiles)
110 {
111 throw new ArgumentOutOfRangeException("basisIgnoreLengths");
112 }
113 }
114 }
115 else
116 {
117 basisIgnoreLengths = new string[countOldFiles];
118 }
119
120 if (null != basisIgnoreOffsets)
121 {
122 if (0 != basisIgnoreOffsets.Length)
123 {
124 if ((uint) basisIgnoreOffsets.Length != countOldFiles)
125 {
126 throw new ArgumentOutOfRangeException("basisIgnoreOffsets");
127 }
128 }
129 }
130 else
131 {
132 basisIgnoreOffsets = new string[countOldFiles];
133 }
134
135 if (null != basisRetainLengths)
136 {
137 if (0 != basisRetainLengths.Length)
138 {
139 if ((uint) basisRetainLengths.Length != countOldFiles)
140 {
141 throw new ArgumentOutOfRangeException("basisRetainLengths");
142 }
143 }
144 }
145 else
146 {
147 basisRetainLengths = new string[countOldFiles];
148 }
149
150 if (null != basisRetainOffsets)
151 {
152 if (0 != basisRetainOffsets.Length)
153 {
154 if ((uint) basisRetainOffsets.Length != countOldFiles)
155 {
156 throw new ArgumentOutOfRangeException("basisRetainOffsets");
157 }
158 }
159 }
160 else
161 {
162 basisRetainOffsets = new string[countOldFiles];
163 }
164
165 PatchOptionData pod = new PatchOptionData();
166 pod.symbolOptionFlags = apiPatchingSymbolFlags;
167 pod.newFileSymbolPath = targetSymbolPath;
168 pod.oldFileSymbolPathArray = basisSymbolPaths;
169 pod.extendedOptionFlags = 0;
170 PatchOldFileInfoW[] oldFileInfoArray = new PatchOldFileInfoW[countOldFiles];
171 string[] newRetainOffsetArray = ((null == targetRetainOffsets) ? new string[0] : targetRetainOffsets.Split(','));
172 for (uint i = 0; i < countOldFiles; ++i)
173 {
174 PatchOldFileInfoW ofi = new PatchOldFileInfoW();
175 ofi.oldFileName = basisFiles[i];
176 string[] ignoreLengthArray = ((null == basisIgnoreLengths[i]) ? new string[0] : basisIgnoreLengths[i].Split(','));
177 string[] ignoreOffsetArray = ((null == basisIgnoreOffsets[i]) ? new string[0] : basisIgnoreOffsets[i].Split(','));
178 string[] retainLengthArray = ((null == basisRetainLengths[i]) ? new string[0] : basisRetainLengths[i].Split(','));
179 string[] retainOffsetArray = ((null == basisRetainOffsets[i]) ? new string[0] : basisRetainOffsets[i].Split(','));
180 // Validate inputs
181 if (ignoreLengthArray.Length != ignoreOffsetArray.Length)
182 {
183 throw new ArgumentOutOfRangeException("basisIgnoreLengths");
184 }
185
186 if (retainLengthArray.Length != retainOffsetArray.Length)
187 {
188 throw new ArgumentOutOfRangeException("basisRetainLengths");
189 }
190
191 if (newRetainOffsetArray.Length != retainOffsetArray.Length)
192 {
193 // remove all retain range information
194 retainRangesIgnored = true;
195 for (uint j = 0; j < countOldFiles; ++j)
196 {
197 basisRetainLengths[j] = null;
198 basisRetainOffsets[j] = null;
199 }
200 retainLengthArray = new string[0];
201 retainOffsetArray = new string[0];
202 newRetainOffsetArray = new string[0];
203 for (uint j = 0; j < oldFileInfoArray.Length; ++j)
204 {
205 oldFileInfoArray[j].retainRange = null;
206 }
207 }
208
209 // Populate IgnoreRange structure
210 PatchIgnoreRange[] ignoreArray = null;
211 if (0 != ignoreLengthArray.Length)
212 {
213 ignoreArray = new PatchIgnoreRange[ignoreLengthArray.Length];
214 for (int j = 0; j < ignoreLengthArray.Length; ++j)
215 {
216 PatchIgnoreRange ignoreRange = new PatchIgnoreRange();
217 ignoreRange.offsetInOldFile = ParseHexOrDecimal(ignoreOffsetArray[j]);
218 ignoreRange.lengthInBytes = ParseHexOrDecimal(ignoreLengthArray[j]);
219 ignoreArray[j] = ignoreRange;
220 }
221 ofi.ignoreRange = ignoreArray;
222 }
223
224 PatchRetainRange[] retainArray = null;
225 if (0 != newRetainOffsetArray.Length)
226 {
227 retainArray = new PatchRetainRange[retainLengthArray.Length];
228 for (int j = 0; j < newRetainOffsetArray.Length; ++j)
229 {
230 PatchRetainRange retainRange = new PatchRetainRange();
231 retainRange.offsetInOldFile = ParseHexOrDecimal(retainOffsetArray[j]);
232 retainRange.lengthInBytes = ParseHexOrDecimal(retainLengthArray[j]);
233 retainRange.offsetInNewFile = ParseHexOrDecimal(newRetainOffsetArray[j]);
234 retainArray[j] = retainRange;
235 }
236 ofi.retainRange = retainArray;
237 }
238 oldFileInfoArray[i] = ofi;
239 }
240
241 if (CreatePatchFileExW(
242 countOldFiles,
243 oldFileInfoArray,
244 targetFile,
245 deltaFile,
246 PatchOptionFlags(optimizePatchSizeForLargeFiles),
247 pod,
248 null,
249 IntPtr.Zero))
250 {
251 return true;
252 }
253
254 // determine if this is an error or a need to use whole file.
255 int err = Marshal.GetLastWin32Error();
256 switch(err)
257 {
258 case unchecked((int) ERROR_PATCH_BIGGER_THAN_COMPRESSED):
259 break;
260
261 // too late to exclude this file -- should have been caught before
262 case unchecked((int) ERROR_PATCH_SAME_FILE):
263 default:
264 throw new System.ComponentModel.Win32Exception(err);
265 }
266 return false;
267 }
268
269 /// <summary>
270 /// Extract the delta header.
271 /// </summary>
272 /// <param name="delta">Name of delta file.</param>
273 /// <param name="deltaHeader">Name of file to create with the delta's header.</param>
274 static public void ExtractDeltaHeader(string delta, string deltaHeader)
275 {
276 if (!ExtractPatchHeaderToFileW(delta, deltaHeader))
277 {
278 throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
279 }
280 }
281
282 /// <summary>
283 /// Returns the PatchOptionFlags to use.
284 /// </summary>
285 /// <param name="optimizeForLargeFiles">True if optimizing for large files.</param>
286 /// <returns>PATCH_OPTION_FLAG values</returns>
287 static private UInt32 PatchOptionFlags(bool optimizeForLargeFiles)
288 {
289 UInt32 flags = PATCH_OPTION_FAIL_IF_SAME_FILE | PATCH_OPTION_FAIL_IF_BIGGER | PATCH_OPTION_USE_LZX_BEST;
290 if (optimizeForLargeFiles)
291 {
292 flags |= PATCH_OPTION_USE_LZX_LARGE;
293 }
294 return flags;
295 }
296
297 //---------------------------------------------------------------------
298 // From PatchApi.h
299 //---------------------------------------------------------------------
300
301 //
302 // The following contants can be combined and used as the OptionFlags
303 // parameter in the patch creation apis.
304
305 internal const uint PATCH_OPTION_USE_BEST = 0x00000000; // auto choose best (slower)
306
307 internal const uint PATCH_OPTION_USE_LZX_BEST = 0x00000003; // auto choose best of LXZ A/B (but not large)
308 internal const uint PATCH_OPTION_USE_LZX_A = 0x00000001; // normal
309 internal const uint PATCH_OPTION_USE_LXZ_B = 0x00000002; // better on some x86 binaries
310 internal const uint PATCH_OPTION_USE_LZX_LARGE = 0x00000004; // better support for large files (requires 5.1 or higher applyer)
311
312 internal const uint PATCH_OPTION_NO_BINDFIX = 0x00010000; // PE bound imports
313 internal const uint PATCH_OPTION_NO_LOCKFIX = 0x00020000; // PE smashed locks
314 internal const uint PATCH_OPTION_NO_REBASE = 0x00040000; // PE rebased image
315 internal const uint PATCH_OPTION_FAIL_IF_SAME_FILE = 0x00080000; // don't create if same
316 internal const uint PATCH_OPTION_FAIL_IF_BIGGER = 0x00100000; // fail if patch is larger than simply compressing new file (slower)
317 internal const uint PATCH_OPTION_NO_CHECKSUM = 0x00200000; // PE checksum zero
318 internal const uint PATCH_OPTION_NO_RESTIMEFIX = 0x00400000; // PE resource timestamps
319 internal const uint PATCH_OPTION_NO_TIMESTAMP = 0x00800000; // don't store new file timestamp in patch
320 internal const uint PATCH_OPTION_SIGNATURE_MD5 = 0x01000000; // use MD5 instead of CRC (reserved for future support)
321 internal const uint PATCH_OPTION_INTERLEAVE_FILES = 0x40000000; // better support for large files (requires 5.2 or higher applyer)
322 internal const uint PATCH_OPTION_RESERVED1 = 0x80000000; // (used internally)
323
324 internal const uint PATCH_OPTION_VALID_FLAGS = 0xC0FF0007;
325
326 //
327 // The following flags are used with PATCH_OPTION_DATA ExtendedOptionFlags:
328 //
329
330 internal const uint PATCH_TRANSFORM_PE_RESOURCE_2 = 0x00000100; // better handling of PE resources (requires 5.2 or higher applyer)
331 internal const uint PATCH_TRANSFORM_PE_IRELOC_2 = 0x00000200; // better handling of PE stripped relocs (requires 5.2 or higher applyer)
332
333 //
334 // In addition to the standard Win32 error codes, the following error codes may
335 // be returned via GetLastError() when one of the patch APIs fails.
336
337 internal const uint ERROR_PATCH_ENCODE_FAILURE = 0xC00E3101; // create
338 internal const uint ERROR_PATCH_INVALID_OPTIONS = 0xC00E3102; // create
339 internal const uint ERROR_PATCH_SAME_FILE = 0xC00E3103; // create
340 internal const uint ERROR_PATCH_RETAIN_RANGES_DIFFER = 0xC00E3104; // create
341 internal const uint ERROR_PATCH_BIGGER_THAN_COMPRESSED = 0xC00E3105; // create
342 internal const uint ERROR_PATCH_IMAGEHLP_FALURE = 0xC00E3106; // create
343
344 /// <summary>
345 /// Delegate type that the PatchAPI calls for progress notification.
346 /// </summary>
347 /// <param name="context">.</param>
348 /// <param name="currentPosition">.</param>
349 /// <param name="maxPosition">.</param>
350 /// <returns>True for success</returns>
351 public delegate bool PatchProgressCallback(
352 IntPtr context,
353 uint currentPosition,
354 uint maxPosition
355 );
356
357 /// <summary>
358 /// Delegate type that the PatchAPI calls for patch symbol load information.
359 /// </summary>
360 /// <param name="whichFile">.</param>
361 /// <param name="symbolFileName">.</param>
362 /// <param name="symType">.</param>
363 /// <param name="symbolFileCheckSum">.</param>
364 /// <param name="symbolFileTimeDate">.</param>
365 /// <param name="imageFileCheckSum">.</param>
366 /// <param name="imageFileTimeDate">.</param>
367 /// <param name="context">.</param>
368 /// <returns>???</returns>
369 public delegate bool PatchSymloadCallback(
370 uint whichFile, // 0 for new file, 1 for first old file, etc
371 [MarshalAs(UnmanagedType.LPStr)] string symbolFileName,
372 uint symType, // see SYM_TYPE in imagehlp.h
373 uint symbolFileCheckSum,
374 uint symbolFileTimeDate,
375 uint imageFileCheckSum,
376 uint imageFileTimeDate,
377 IntPtr context
378 );
379
380 /// <summary>
381 /// Wraps PATCH_IGNORE_RANGE
382 /// </summary>
383 [StructLayout(LayoutKind.Sequential)]
384 internal class PatchIgnoreRange
385 {
386 public uint offsetInOldFile;
387 public uint lengthInBytes;
388 }
389
390 /// <summary>
391 /// Wraps PATCH_RETAIN_RANGE
392 /// </summary>
393 [StructLayout(LayoutKind.Sequential)]
394 internal class PatchRetainRange
395 {
396 public uint offsetInOldFile;
397 public uint lengthInBytes;
398 public uint offsetInNewFile;
399 }
400
401 /// <summary>
402 /// Wraps PATCH_OLD_FILE_INFO (except for the OldFile~ portion)
403 /// </summary>
404 internal class PatchOldFileInfo
405 {
406 public PatchIgnoreRange[] ignoreRange;
407 public PatchRetainRange[] retainRange;
408 }
409
410 /// <summary>
411 /// Wraps PATCH_OLD_FILE_INFO_W
412 /// </summary>
413 internal class PatchOldFileInfoW : PatchOldFileInfo
414 {
415 public string oldFileName;
416 }
417
418 /// <summary>
419 /// Wraps each PATCH_INTERLEAVE_MAP Range
420 /// </summary>
421 [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses"), StructLayout(LayoutKind.Sequential)]
422 internal class PatchInterleaveMapRange
423 {
424 public uint oldOffset;
425 public uint oldLength;
426 public uint newLength;
427 }
428
429 /// <summary>
430 /// Wraps PATCH_INTERLEAVE_MAP
431 /// </summary>
432 internal class PatchInterleaveMap
433 {
434 public PatchInterleaveMapRange[] ranges = null;
435 }
436
437
438 /// <summary>
439 /// Wraps PATCH_OPTION_DATA
440 /// </summary>
441 [BestFitMapping(false, ThrowOnUnmappableChar = true)]
442 internal class PatchOptionData
443 {
444 public PatchSymbolFlagsType symbolOptionFlags; // PATCH_SYMBOL_xxx flags
445 [MarshalAs(UnmanagedType.LPStr)] public string newFileSymbolPath; // always ANSI, never Unicode
446 [MarshalAs(UnmanagedType.LPStr)] public string[] oldFileSymbolPathArray; // array[ OldFileCount ]
447 public uint extendedOptionFlags;
448 public PatchSymloadCallback symLoadCallback = null;
449 public IntPtr symLoadContext = IntPtr.Zero;
450 public PatchInterleaveMap[] interleaveMapArray = null; // array[ OldFileCount ] (requires 5.2 or higher applyer)
451 public uint maxLzxWindowSize = 0; // limit memory requirements (requires 5.2 or higher applyer)
452 }
453
454 //
455 // Note that PATCH_OPTION_DATA contains LPCSTR paths, and no LPCWSTR (Unicode)
456 // path argument is available, even when used with one of the Unicode APIs
457 // such as CreatePatchFileW. This is because the unlerlying system services
458 // for symbol file handling (IMAGEHLP.DLL) only support ANSI file/path names.
459 //
460
461 //
462 // A note about PATCH_RETAIN_RANGE specifiers with multiple old files:
463 //
464 // Each old version file must have the same RetainRangeCount, and the same
465 // retain range LengthInBytes and OffsetInNewFile values in the same order.
466 // Only the OffsetInOldFile values can differ between old foles for retain
467 // ranges.
468 //
469
470 //
471 // The following prototypes are (some of the) interfaces for creating patches from files.
472 //
473
474 /// <summary>
475 /// Creates a new delta.
476 /// </summary>
477 /// <param name="oldFileCount">Size of oldFileInfoArray.</param>
478 /// <param name="oldFileInfoArray">Target file information.</param>
479 /// <param name="newFileName">Name of updated file.</param>
480 /// <param name="patchFileName">Name of delta to create.</param>
481 /// <param name="optionFlags">PATCH_OPTION_xxx.</param>
482 /// <param name="optionData">Optional PATCH_OPTION_DATA structure.</param>
483 /// <param name="progressCallback">Delegate for progress callbacks.</param>
484 /// <param name="context">Context for progress callback delegate.</param>
485 /// <returns>true if successfull, sets Marshal.GetLastWin32Error() if not.</returns>
486 [DllImport("mspatchc.dll", SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = true)]
487 [return: MarshalAs(UnmanagedType.Bool)]
488 internal static extern bool CreatePatchFileExW(
489 uint oldFileCount, // maximum 255
490 [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef=typeof(PatchAPIMarshaler), MarshalCookie="PATCH_OLD_FILE_INFO_W")]
491 PatchOldFileInfoW[] oldFileInfoArray,
492 string newFileName, // input file (required)
493 string patchFileName, // output file (required)
494 uint optionFlags,
495 [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef=typeof(PatchAPIMarshaler), MarshalCookie="PATCH_OPTION_DATA")]
496 PatchOptionData optionData,
497 [MarshalAs (UnmanagedType.FunctionPtr)]
498 PatchProgressCallback progressCallback,
499 IntPtr context
500 );
501
502 /// <summary>
503 /// Extracts delta header from delta.
504 /// </summary>
505 /// <param name="patchFileName">Name of delta file.</param>
506 /// <param name="patchHeaderFileName">Name of file to create with delta header.</param>
507 /// <returns>true if successfull, sets Marshal.GetLastWin32Error() if not.</returns>
508 [DllImport("mspatchc.dll", SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = true)]
509 [return: MarshalAs(UnmanagedType.Bool)]
510 internal static extern bool ExtractPatchHeaderToFileW(
511 string patchFileName, // input file
512 string patchHeaderFileName // output file
513 );
514
515 // TODO: Add rest of APIs to enable custom binders to perform more exhaustive checks
516
517 /// <summary>
518 /// Marshals arguments for the CreatePatch~ APIs
519 /// </summary>
520 [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses")]
521 internal class PatchAPIMarshaler : ICustomMarshaler
522 {
523 internal static ICustomMarshaler GetInstance(string cookie)
524 {
525 return new PatchAPIMarshaler(cookie);
526 }
527
528 private enum MarshalType
529 {
530 PATCH_OPTION_DATA,
531 PATCH_OLD_FILE_INFO_W
532 };
533 private PatchAPIMarshaler.MarshalType marshalType;
534
535 private PatchAPIMarshaler(string cookie)
536 {
537 this.marshalType = (PatchAPIMarshaler.MarshalType) Enum.Parse(typeof(PatchAPIMarshaler.MarshalType), cookie);
538 }
539
540 //
541 // Summary:
542 // Returns the size of the native data to be marshaled.
543 //
544 // Returns:
545 // The size in bytes of the native data.
546 public int GetNativeDataSize()
547 {
548 return Marshal.SizeOf(typeof(IntPtr));
549 }
550
551 //
552 // Summary:
553 // Performs necessary cleanup of the managed data when it is no longer needed.
554 //
555 // Parameters:
556 // ManagedObj:
557 // The managed object to be destroyed.
558 public void CleanUpManagedData(object ManagedObj)
559 {
560 }
561
562 //
563 // Summary:
564 // Performs necessary cleanup of the unmanaged data when it is no longer needed.
565 //
566 // Parameters:
567 // pNativeData:
568 // A pointer to the unmanaged data to be destroyed.
569 public void CleanUpNativeData(IntPtr pNativeData)
570 {
571 if (IntPtr.Zero == pNativeData)
572 {
573 return;
574 }
575
576 switch (this.marshalType)
577 {
578 case PatchAPIMarshaler.MarshalType.PATCH_OPTION_DATA:
579 this.CleanUpPOD(pNativeData);
580 break;
581 default:
582 this.CleanUpPOFI_A(pNativeData);
583 break;
584 }
585 }
586
587 //
588 // Summary:
589 // Converts the managed data to unmanaged data.
590 //
591 // Parameters:
592 // ManagedObj:
593 // The managed object to be converted.
594 //
595 // Returns:
596 // Returns the COM view of the managed object.
597 public IntPtr MarshalManagedToNative(object ManagedObj)
598 {
599 if (null == ManagedObj)
600 {
601 return IntPtr.Zero;
602 }
603
604 switch(this.marshalType)
605 {
606 case PatchAPIMarshaler.MarshalType.PATCH_OPTION_DATA:
607 return this.MarshalPOD(ManagedObj as PatchOptionData);
608 case PatchAPIMarshaler.MarshalType.PATCH_OLD_FILE_INFO_W:
609 return this.MarshalPOFIW_A(ManagedObj as PatchOldFileInfoW[]);
610 default:
611 throw new InvalidOperationException();
612 }
613 }
614
615
616 //
617 // Summary:
618 // Converts the unmanaged data to managed data.
619 //
620 // Parameters:
621 // pNativeData:
622 // A pointer to the unmanaged data to be wrapped.
623 //
624 // Returns:
625 // Returns the managed view of the COM data.
626 public object MarshalNativeToManaged(IntPtr pNativeData)
627 {
628 return null;
629 }
630
631 // Implementation *************************************************
632
633 // PATCH_OPTION_DATA offsets
634 private static readonly int symbolOptionFlagsOffset = Marshal.SizeOf(typeof(Int32));
635 private static readonly int newFileSymbolPathOffset = 2*Marshal.SizeOf(typeof(Int32));
636 private static readonly int oldFileSymbolPathArrayOffset = 2*Marshal.SizeOf(typeof(Int32)) + Marshal.SizeOf(typeof(IntPtr));
637 private static readonly int extendedOptionFlagsOffset = 2*Marshal.SizeOf(typeof(Int32)) + 2*Marshal.SizeOf(typeof(IntPtr));
638 private static readonly int symLoadCallbackOffset = 3*Marshal.SizeOf(typeof(Int32)) + 2*Marshal.SizeOf(typeof(IntPtr));
639 private static readonly int symLoadContextOffset = 3*Marshal.SizeOf(typeof(Int32)) + 3*Marshal.SizeOf(typeof(IntPtr));
640 private static readonly int interleaveMapArrayOffset = 3*Marshal.SizeOf(typeof(Int32)) + 4*Marshal.SizeOf(typeof(IntPtr));
641 private static readonly int maxLzxWindowSizeOffset = 3*Marshal.SizeOf(typeof(Int32)) + 5*Marshal.SizeOf(typeof(IntPtr));
642 private static readonly int patchOptionDataSize = 4*Marshal.SizeOf(typeof(Int32)) + 5*Marshal.SizeOf(typeof(IntPtr));
643
644 // PATCH_OLD_FILE_INFO offsets
645 private static readonly int oldFileOffset = Marshal.SizeOf(typeof(Int32));
646 private static readonly int ignoreRangeCountOffset = Marshal.SizeOf(typeof(Int32)) + Marshal.SizeOf(typeof(IntPtr));
647 private static readonly int ignoreRangeArrayOffset = 2*Marshal.SizeOf(typeof(Int32)) + Marshal.SizeOf(typeof(IntPtr));
648 private static readonly int retainRangeCountOffset = 2*Marshal.SizeOf(typeof(Int32)) + 2*Marshal.SizeOf(typeof(IntPtr));
649 private static readonly int retainRangeArrayOffset = 3*Marshal.SizeOf(typeof(Int32)) + 2*Marshal.SizeOf(typeof(IntPtr));
650 private static readonly int patchOldFileInfoSize = 3*Marshal.SizeOf(typeof(Int32)) + 3*Marshal.SizeOf(typeof(IntPtr));
651
652 // Methods and data used to preserve data needed for cleanup
653
654 // This dictionary holds the quantity of items internal to each native structure that will need to be freed (the OldFileCount)
655 private static readonly Dictionary<IntPtr, int> OldFileCounts = new Dictionary<IntPtr, int>();
656 private static readonly object OldFileCountsLock = new object();
657
658 private IntPtr CreateMainStruct(int oldFileCount)
659 {
660 int nativeSize;
661 switch(this.marshalType)
662 {
663 case PatchAPIMarshaler.MarshalType.PATCH_OPTION_DATA:
664 nativeSize = patchOptionDataSize;
665 break;
666 case PatchAPIMarshaler.MarshalType.PATCH_OLD_FILE_INFO_W:
667 nativeSize = oldFileCount*patchOldFileInfoSize;
668 break;
669 default:
670 throw new InvalidOperationException();
671 }
672
673 IntPtr native = Marshal.AllocCoTaskMem(nativeSize);
674
675 lock (PatchAPIMarshaler.OldFileCountsLock)
676 {
677 PatchAPIMarshaler.OldFileCounts.Add(native, oldFileCount);
678 }
679
680 return native;
681 }
682
683 private static void ReleaseMainStruct(IntPtr native)
684 {
685 lock (PatchAPIMarshaler.OldFileCountsLock)
686 {
687 PatchAPIMarshaler.OldFileCounts.Remove(native);
688 }
689 Marshal.FreeCoTaskMem(native);
690 }
691
692 private static int GetOldFileCount(IntPtr native)
693 {
694 lock (PatchAPIMarshaler.OldFileCountsLock)
695 {
696 return PatchAPIMarshaler.OldFileCounts[native];
697 }
698 }
699
700 // Helper methods
701
702 private static IntPtr OptionalAnsiString(string managed)
703 {
704 return (null == managed) ? IntPtr.Zero : Marshal.StringToCoTaskMemAnsi(managed);
705 }
706
707 private static IntPtr OptionalUnicodeString(string managed)
708 {
709 return (null == managed) ? IntPtr.Zero : Marshal.StringToCoTaskMemUni(managed);
710 }
711
712 // string array must be of the same length as the number of old files
713 private static IntPtr CreateArrayOfStringA(string[] managed)
714 {
715 if (null == managed)
716 {
717 return IntPtr.Zero;
718 }
719
720 int size = managed.Length * Marshal.SizeOf(typeof(IntPtr));
721 IntPtr native = Marshal.AllocCoTaskMem(size);
722
723 for (int i = 0; i < managed.Length; ++i)
724 {
725 Marshal.WriteIntPtr(native, i*Marshal.SizeOf(typeof(IntPtr)), OptionalAnsiString(managed[i]));
726 }
727
728 return native;
729 }
730
731 // string array must be of the same length as the number of old files
732 private static IntPtr CreateArrayOfStringW(string[] managed)
733 {
734 if (null == managed)
735 {
736 return IntPtr.Zero;
737 }
738
739 int size = managed.Length * Marshal.SizeOf(typeof(IntPtr));
740 IntPtr native = Marshal.AllocCoTaskMem(size);
741
742 for (int i = 0; i < managed.Length; ++i)
743 {
744 Marshal.WriteIntPtr(native, i*Marshal.SizeOf(typeof(IntPtr)), OptionalUnicodeString(managed[i]));
745 }
746
747 return native;
748 }
749
750 private static IntPtr CreateInterleaveMapRange(PatchInterleaveMap managed)
751 {
752 if (null == managed)
753 {
754 return IntPtr.Zero;
755 }
756
757 if (null == managed.ranges)
758 {
759 return IntPtr.Zero;
760 }
761
762 if (0 == managed.ranges.Length)
763 {
764 return IntPtr.Zero;
765 }
766
767 IntPtr native = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(UInt32))
768 + managed.ranges.Length*(Marshal.SizeOf(typeof(PatchInterleaveMap))));
769 WriteUInt32(native, (uint) managed.ranges.Length);
770
771 for (int i = 0; i < managed.ranges.Length; ++i)
772 {
773 Marshal.StructureToPtr(managed.ranges[i], (IntPtr)((Int64)native + i*Marshal.SizeOf(typeof(PatchInterleaveMap))), false);
774 }
775 return native;
776 }
777
778 private static IntPtr CreateInterleaveMap(PatchInterleaveMap[] managed)
779 {
780 if (null == managed)
781 {
782 return IntPtr.Zero;
783 }
784
785 IntPtr native = Marshal.AllocCoTaskMem(managed.Length * Marshal.SizeOf(typeof(IntPtr)));
786
787 for (int i = 0; i < managed.Length; ++i)
788 {
789 Marshal.WriteIntPtr(native, i*Marshal.SizeOf(typeof(IntPtr)), CreateInterleaveMapRange(managed[i]));
790 }
791
792 return native;
793 }
794
795 private static void WriteUInt32(IntPtr native, uint data)
796 {
797 Marshal.WriteInt32(native, unchecked((int) data));
798 }
799
800 private static void WriteUInt32(IntPtr native, int offset, uint data)
801 {
802 Marshal.WriteInt32(native, offset, unchecked((int) data));
803 }
804
805 // Marshal operations
806
807 private IntPtr MarshalPOD(PatchOptionData managed)
808 {
809 if (null == managed)
810 {
811 throw new ArgumentNullException("managed");
812 }
813
814 IntPtr native = this.CreateMainStruct(managed.oldFileSymbolPathArray.Length);
815 Marshal.WriteInt32(native, patchOptionDataSize); // SizeOfThisStruct
816 WriteUInt32(native, symbolOptionFlagsOffset, (uint) managed.symbolOptionFlags);
817 Marshal.WriteIntPtr(native, newFileSymbolPathOffset, PatchAPIMarshaler.OptionalAnsiString(managed.newFileSymbolPath));
818 Marshal.WriteIntPtr(native, oldFileSymbolPathArrayOffset, PatchAPIMarshaler.CreateArrayOfStringA(managed.oldFileSymbolPathArray));
819 WriteUInt32(native, extendedOptionFlagsOffset, managed.extendedOptionFlags);
820
821 // GetFunctionPointerForDelegate() throws an ArgumentNullException if the delegate is null.
822 if (null == managed.symLoadCallback)
823 {
824 Marshal.WriteIntPtr(native, symLoadCallbackOffset, IntPtr.Zero);
825 }
826 else
827 {
828 Marshal.WriteIntPtr(native, symLoadCallbackOffset, Marshal.GetFunctionPointerForDelegate(managed.symLoadCallback));
829 }
830
831 Marshal.WriteIntPtr(native, symLoadContextOffset, managed.symLoadContext);
832 Marshal.WriteIntPtr(native, interleaveMapArrayOffset, PatchAPIMarshaler.CreateInterleaveMap(managed.interleaveMapArray));
833 WriteUInt32(native, maxLzxWindowSizeOffset, managed.maxLzxWindowSize);
834 return native;
835 }
836
837 private IntPtr MarshalPOFIW_A(PatchOldFileInfoW[] managed)
838 {
839 if (null == managed)
840 {
841 throw new ArgumentNullException("managed");
842 }
843
844 if (0 == managed.Length)
845 {
846 return IntPtr.Zero;
847 }
848
849 IntPtr native = this.CreateMainStruct(managed.Length);
850
851 for (int i = 0; i < managed.Length; ++i)
852 {
853 PatchAPIMarshaler.MarshalPOFIW(managed[i], (IntPtr)((Int64)native + i * patchOldFileInfoSize));
854 }
855
856 return native;
857 }
858
859 private static void MarshalPOFIW(PatchOldFileInfoW managed, IntPtr native)
860 {
861 PatchAPIMarshaler.MarshalPOFI(managed, native);
862 Marshal.WriteIntPtr(native, oldFileOffset, PatchAPIMarshaler.OptionalUnicodeString(managed.oldFileName)); // OldFileName
863 }
864
865 private static void MarshalPOFI(PatchOldFileInfo managed, IntPtr native)
866 {
867 Marshal.WriteInt32(native, patchOldFileInfoSize); // SizeOfThisStruct
868 WriteUInt32(native, ignoreRangeCountOffset,
869 (null == managed.ignoreRange) ? 0 : (uint) managed.ignoreRange.Length); // IgnoreRangeCount // maximum 255
870 Marshal.WriteIntPtr(native, ignoreRangeArrayOffset, MarshalPIRArray(managed.ignoreRange)); // IgnoreRangeArray
871 WriteUInt32(native, retainRangeCountOffset,
872 (null == managed.retainRange) ? 0 : (uint) managed.retainRange.Length); // RetainRangeCount // maximum 255
873 Marshal.WriteIntPtr(native, retainRangeArrayOffset, MarshalPRRArray(managed.retainRange)); // RetainRangeArray
874 }
875
876 private static IntPtr MarshalPIRArray(PatchIgnoreRange[] array)
877 {
878 if (null == array)
879 {
880 return IntPtr.Zero;
881 }
882
883 if (0 == array.Length)
884 {
885 return IntPtr.Zero;
886 }
887
888 IntPtr native = Marshal.AllocCoTaskMem(array.Length*Marshal.SizeOf(typeof(PatchIgnoreRange)));
889
890 for (int i = 0; i < array.Length; ++i)
891 {
892 Marshal.StructureToPtr(array[i], (IntPtr)((Int64)native + (i*Marshal.SizeOf(typeof(PatchIgnoreRange)))), false);
893 }
894
895 return native;
896 }
897
898 private static IntPtr MarshalPRRArray(PatchRetainRange[] array)
899 {
900 if (null == array)
901 {
902 return IntPtr.Zero;
903 }
904
905 if (0 == array.Length)
906 {
907 return IntPtr.Zero;
908 }
909
910 IntPtr native = Marshal.AllocCoTaskMem(array.Length*Marshal.SizeOf(typeof(PatchRetainRange)));
911
912 for (int i = 0; i < array.Length; ++i)
913 {
914 Marshal.StructureToPtr(array[i], (IntPtr)((Int64)native + (i*Marshal.SizeOf(typeof(PatchRetainRange)))), false);
915 }
916
917 return native;
918 }
919
920 // CleanUp operations
921
922 private void CleanUpPOD(IntPtr native)
923 {
924 Marshal.FreeCoTaskMem(Marshal.ReadIntPtr(native, newFileSymbolPathOffset));
925
926 if (IntPtr.Zero != Marshal.ReadIntPtr(native, oldFileSymbolPathArrayOffset))
927 {
928 for (int i = 0; i < GetOldFileCount(native); ++i)
929 {
930 Marshal.FreeCoTaskMem(
931 Marshal.ReadIntPtr(
932 Marshal.ReadIntPtr(native, oldFileSymbolPathArrayOffset),
933 i*Marshal.SizeOf(typeof(IntPtr))));
934 }
935
936 Marshal.FreeCoTaskMem(Marshal.ReadIntPtr(native, oldFileSymbolPathArrayOffset));
937 }
938
939 if (IntPtr.Zero != Marshal.ReadIntPtr(native, interleaveMapArrayOffset))
940 {
941 for (int i = 0; i < GetOldFileCount(native); ++i)
942 {
943 Marshal.FreeCoTaskMem(
944 Marshal.ReadIntPtr(
945 Marshal.ReadIntPtr(native, interleaveMapArrayOffset),
946 i*Marshal.SizeOf(typeof(IntPtr))));
947 }
948
949 Marshal.FreeCoTaskMem(Marshal.ReadIntPtr(native, interleaveMapArrayOffset));
950 }
951
952 PatchAPIMarshaler.ReleaseMainStruct(native);
953 }
954
955 private void CleanUpPOFI_A(IntPtr native)
956 {
957 for (int i = 0; i < GetOldFileCount(native); ++i)
958 {
959 PatchAPIMarshaler.CleanUpPOFI((IntPtr)((Int64)native + i*patchOldFileInfoSize));
960 }
961
962 PatchAPIMarshaler.ReleaseMainStruct(native);
963 }
964
965 private static void CleanUpPOFI(IntPtr native)
966 {
967 if (IntPtr.Zero != Marshal.ReadIntPtr(native, oldFileOffset))
968 {
969 Marshal.FreeCoTaskMem(Marshal.ReadIntPtr(native, oldFileOffset));
970 }
971
972 PatchAPIMarshaler.CleanUpPOFIH(native);
973 }
974
975 private static void CleanUpPOFIH(IntPtr native)
976 {
977 if (IntPtr.Zero != Marshal.ReadIntPtr(native, ignoreRangeArrayOffset))
978 {
979 Marshal.FreeCoTaskMem(Marshal.ReadIntPtr(native, ignoreRangeArrayOffset));
980 }
981
982 if (IntPtr.Zero != Marshal.ReadIntPtr(native, retainRangeArrayOffset))
983 {
984 Marshal.FreeCoTaskMem(Marshal.ReadIntPtr(native, retainRangeArrayOffset));
985 }
986 }
987 }
988 }
989}
diff --git a/src/WixToolset.Core.WindowsInstaller/Unbind/ExtractCabinetsCommand.cs b/src/WixToolset.Core.WindowsInstaller/Unbind/ExtractCabinetsCommand.cs
new file mode 100644
index 00000000..229e75b4
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Unbind/ExtractCabinetsCommand.cs
@@ -0,0 +1,146 @@
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.Unbind
4{
5 using System;
6 using System.Collections;
7 using System.Collections.Specialized;
8 using System.Globalization;
9 using System.IO;
10 using WixToolset.Core.Cab;
11 using WixToolset.Data;
12 using WixToolset.Data.Rows;
13 using WixToolset.Msi;
14
15 internal class ExtractCabinetsCommand
16 {
17 public ExtractCabinetsCommand(Output output, Database database, string inputFilePath, string exportBasePath, string intermediateFolder)
18 {
19 this.Output = output;
20 this.Database = database;
21 this.InputFilePath = inputFilePath;
22 this.ExportBasePath = exportBasePath;
23 this.IntermediateFolder = intermediateFolder;
24 }
25
26 private Output Output { get; }
27
28 private Database Database { get; }
29
30 private string InputFilePath { get; }
31
32 private string ExportBasePath { get; }
33
34 private string IntermediateFolder { get; }
35
36 public void Execute()
37 {
38 string databaseBasePath = Path.GetDirectoryName(this.InputFilePath);
39 StringCollection cabinetFiles = new StringCollection();
40 SortedList embeddedCabinets = new SortedList();
41
42 // index all of the cabinet files
43 if (OutputType.Module == this.Output.Type)
44 {
45 embeddedCabinets.Add(0, "MergeModule.CABinet");
46 }
47 else if (null != this.Output.Tables["Media"])
48 {
49 foreach (MediaRow mediaRow in this.Output.Tables["Media"].Rows)
50 {
51 if (null != mediaRow.Cabinet)
52 {
53 if (OutputType.Product == this.Output.Type ||
54 (OutputType.Transform == this.Output.Type && RowOperation.Add == mediaRow.Operation))
55 {
56 if (mediaRow.Cabinet.StartsWith("#", StringComparison.Ordinal))
57 {
58 embeddedCabinets.Add(mediaRow.DiskId, mediaRow.Cabinet.Substring(1));
59 }
60 else
61 {
62 cabinetFiles.Add(Path.Combine(databaseBasePath, mediaRow.Cabinet));
63 }
64 }
65 }
66 }
67 }
68
69 // extract the embedded cabinet files from the database
70 if (0 < embeddedCabinets.Count)
71 {
72 using (View streamsView = this.Database.OpenView("SELECT `Data` FROM `_Streams` WHERE `Name` = ?"))
73 {
74 foreach (int diskId in embeddedCabinets.Keys)
75 {
76 using (Record record = new Record(1))
77 {
78 record.SetString(1, (string)embeddedCabinets[diskId]);
79 streamsView.Execute(record);
80 }
81
82 using (Record record = streamsView.Fetch())
83 {
84 if (null != record)
85 {
86 // since the cabinets are stored in case-sensitive streams inside the msi, but the file system is not case-sensitive,
87 // embedded cabinets must be extracted to a canonical file name (like their diskid) to ensure extraction will always work
88 string cabinetFile = Path.Combine(this.IntermediateFolder, String.Concat("Media", Path.DirectorySeparatorChar, diskId.ToString(CultureInfo.InvariantCulture), ".cab"));
89
90 // ensure the parent directory exists
91 System.IO.Directory.CreateDirectory(Path.GetDirectoryName(cabinetFile));
92
93 using (FileStream fs = System.IO.File.Create(cabinetFile))
94 {
95 int bytesRead;
96 byte[] buffer = new byte[512];
97
98 while (0 != (bytesRead = record.GetStream(1, buffer, buffer.Length)))
99 {
100 fs.Write(buffer, 0, bytesRead);
101 }
102 }
103
104 cabinetFiles.Add(cabinetFile);
105 }
106 else
107 {
108 // TODO: warning about missing embedded cabinet
109 }
110 }
111 }
112 }
113 }
114
115 // extract the cabinet files
116 if (0 < cabinetFiles.Count)
117 {
118 string fileDirectory = Path.Combine(this.ExportBasePath, "File");
119
120 // delete the directory and its files to prevent cab extraction due to an existing file
121 if (Directory.Exists(fileDirectory))
122 {
123 Directory.Delete(fileDirectory, true);
124 }
125
126 // ensure the directory exists or extraction will fail
127 Directory.CreateDirectory(fileDirectory);
128
129 foreach (string cabinetFile in cabinetFiles)
130 {
131 using (var extractCab = new WixExtractCab())
132 {
133 try
134 {
135 extractCab.Extract(cabinetFile, fileDirectory);
136 }
137 catch (FileNotFoundException)
138 {
139 throw new WixException(WixErrors.FileNotFound(new SourceLineNumber(this.InputFilePath), cabinetFile));
140 }
141 }
142 }
143 }
144 }
145 }
146}
diff --git a/src/WixToolset.Core.WindowsInstaller/Unbind/UnbindDatabaseCommand.cs b/src/WixToolset.Core.WindowsInstaller/Unbind/UnbindDatabaseCommand.cs
new file mode 100644
index 00000000..208be874
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Unbind/UnbindDatabaseCommand.cs
@@ -0,0 +1,791 @@
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.Unbind
4{
5 using System;
6 using System.Collections;
7 using System.Collections.Generic;
8 using System.Globalization;
9 using System.IO;
10 using System.Text.RegularExpressions;
11 using WixToolset.Core.Native;
12 using WixToolset.Data;
13 using WixToolset.Data.Rows;
14 using WixToolset.Msi;
15
16 internal class UnbindDatabaseCommand
17 {
18 public UnbindDatabaseCommand(Messaging messaging, Database database, string databasePath, OutputType outputType, string exportBasePath, string intermediateFolder, bool isAdminImage, bool suppressDemodularization, bool skipSummaryInfo)
19 {
20 this.Messaging = messaging;
21 this.Database = database;
22 this.DatabasePath = databasePath;
23 this.OutputType = outputType;
24 this.ExportBasePath = exportBasePath;
25 this.IntermediateFolder = intermediateFolder;
26 this.IsAdminImage = isAdminImage;
27 this.SuppressDemodularization = suppressDemodularization;
28 this.SkipSummaryInfo = skipSummaryInfo;
29
30 this.TableDefinitions = WindowsInstallerStandard.GetTableDefinitions();
31 }
32
33 public Messaging Messaging { get; }
34
35 public Database Database { get; }
36
37 public string DatabasePath { get; }
38
39 public OutputType OutputType { get; }
40
41 public string ExportBasePath { get; }
42
43 public string IntermediateFolder { get; }
44
45 public bool IsAdminImage { get; }
46
47 public bool SuppressDemodularization { get; }
48
49 public bool SkipSummaryInfo { get; }
50
51 public TableDefinitionCollection TableDefinitions { get; }
52
53 private int SectionCount { get; set; }
54
55 public Output Execute()
56 {
57 string modularizationGuid = null;
58 Output output = new Output(new SourceLineNumber(this.DatabasePath));
59 View validationView = null;
60
61 // set the output type
62 output.Type = this.OutputType;
63
64 // get the codepage
65 this.Database.Export("_ForceCodepage", this.IntermediateFolder, "_ForceCodepage.idt");
66 using (StreamReader sr = File.OpenText(Path.Combine(this.IntermediateFolder, "_ForceCodepage.idt")))
67 {
68 string line;
69
70 while (null != (line = sr.ReadLine()))
71 {
72 string[] data = line.Split('\t');
73
74 if (2 == data.Length)
75 {
76 output.Codepage = Convert.ToInt32(data[0], CultureInfo.InvariantCulture);
77 }
78 }
79 }
80
81 // get the summary information table if it exists; it won't if unbinding a transform
82 if (!this.SkipSummaryInfo)
83 {
84 using (SummaryInformation summaryInformation = new SummaryInformation(this.Database))
85 {
86 Table table = new Table(null, this.TableDefinitions["_SummaryInformation"]);
87
88 for (int i = 1; 19 >= i; i++)
89 {
90 string value = summaryInformation.GetProperty(i);
91
92 if (0 < value.Length)
93 {
94 Row row = table.CreateRow(output.SourceLineNumbers);
95 row[0] = i;
96 row[1] = value;
97 }
98 }
99
100 output.Tables.Add(table);
101 }
102 }
103
104 try
105 {
106 // open a view on the validation table if it exists
107 if (this.Database.TableExists("_Validation"))
108 {
109 validationView = this.Database.OpenView("SELECT * FROM `_Validation` WHERE `Table` = ? AND `Column` = ?");
110 }
111
112 // get the normal tables
113 using (View tablesView = this.Database.OpenExecuteView("SELECT * FROM _Tables"))
114 {
115 while (true)
116 {
117 using (Record tableRecord = tablesView.Fetch())
118 {
119 if (null == tableRecord)
120 {
121 break;
122 }
123
124 string tableName = tableRecord.GetString(1);
125
126 using (View tableView = this.Database.OpenExecuteView(String.Format(CultureInfo.InvariantCulture, "SELECT * FROM `{0}`", tableName)))
127 {
128 List<ColumnDefinition> columns;
129 using (Record columnNameRecord = tableView.GetColumnInfo(MsiInterop.MSICOLINFONAMES),
130 columnTypeRecord = tableView.GetColumnInfo(MsiInterop.MSICOLINFOTYPES))
131 {
132 // index the primary keys
133 HashSet<string> tablePrimaryKeys = new HashSet<string>();
134 using (Record primaryKeysRecord = this.Database.PrimaryKeys(tableName))
135 {
136 int primaryKeysFieldCount = primaryKeysRecord.GetFieldCount();
137
138 for (int i = 1; i <= primaryKeysFieldCount; i++)
139 {
140 tablePrimaryKeys.Add(primaryKeysRecord.GetString(i));
141 }
142 }
143
144 int columnCount = columnNameRecord.GetFieldCount();
145 columns = new List<ColumnDefinition>(columnCount);
146 for (int i = 1; i <= columnCount; i++)
147 {
148 string columnName = columnNameRecord.GetString(i);
149 string idtType = columnTypeRecord.GetString(i);
150
151 ColumnType columnType;
152 int length;
153 bool nullable;
154
155 ColumnCategory columnCategory = ColumnCategory.Unknown;
156 ColumnModularizeType columnModularizeType = ColumnModularizeType.None;
157 bool primary = tablePrimaryKeys.Contains(columnName);
158 bool minValueSet = false;
159 int minValue = -1;
160 bool maxValueSet = false;
161 int maxValue = -1;
162 string keyTable = null;
163 bool keyColumnSet = false;
164 int keyColumn = -1;
165 string category = null;
166 string set = null;
167 string description = null;
168
169 // get the column type, length, and whether its nullable
170 switch (Char.ToLower(idtType[0], CultureInfo.InvariantCulture))
171 {
172 case 'i':
173 columnType = ColumnType.Number;
174 break;
175 case 'l':
176 columnType = ColumnType.Localized;
177 break;
178 case 's':
179 columnType = ColumnType.String;
180 break;
181 case 'v':
182 columnType = ColumnType.Object;
183 break;
184 default:
185 // TODO: error
186 columnType = ColumnType.Unknown;
187 break;
188 }
189 length = Convert.ToInt32(idtType.Substring(1), CultureInfo.InvariantCulture);
190 nullable = Char.IsUpper(idtType[0]);
191
192 // try to get validation information
193 if (null != validationView)
194 {
195 using (Record validationRecord = new Record(2))
196 {
197 validationRecord.SetString(1, tableName);
198 validationRecord.SetString(2, columnName);
199
200 validationView.Execute(validationRecord);
201 }
202
203 using (Record validationRecord = validationView.Fetch())
204 {
205 if (null != validationRecord)
206 {
207 string validationNullable = validationRecord.GetString(3);
208 minValueSet = !validationRecord.IsNull(4);
209 minValue = (minValueSet ? validationRecord.GetInteger(4) : -1);
210 maxValueSet = !validationRecord.IsNull(5);
211 maxValue = (maxValueSet ? validationRecord.GetInteger(5) : -1);
212 keyTable = (!validationRecord.IsNull(6) ? validationRecord.GetString(6) : null);
213 keyColumnSet = !validationRecord.IsNull(7);
214 keyColumn = (keyColumnSet ? validationRecord.GetInteger(7) : -1);
215 category = (!validationRecord.IsNull(8) ? validationRecord.GetString(8) : null);
216 set = (!validationRecord.IsNull(9) ? validationRecord.GetString(9) : null);
217 description = (!validationRecord.IsNull(10) ? validationRecord.GetString(10) : null);
218
219 // check the validation nullable value against the column definition
220 if (null == validationNullable)
221 {
222 // TODO: warn for illegal validation nullable column
223 }
224 else if ((nullable && "Y" != validationNullable) || (!nullable && "N" != validationNullable))
225 {
226 // TODO: warn for mismatch between column definition and validation nullable
227 }
228
229 // convert category to ColumnCategory
230 if (null != category)
231 {
232 try
233 {
234 columnCategory = (ColumnCategory)Enum.Parse(typeof(ColumnCategory), category, true);
235 }
236 catch (ArgumentException)
237 {
238 columnCategory = ColumnCategory.Unknown;
239 }
240 }
241 }
242 else
243 {
244 // TODO: warn about no validation information
245 }
246 }
247 }
248
249 // guess the modularization type
250 if ("Icon" == keyTable && 1 == keyColumn)
251 {
252 columnModularizeType = ColumnModularizeType.Icon;
253 }
254 else if ("Condition" == columnName)
255 {
256 columnModularizeType = ColumnModularizeType.Condition;
257 }
258 else if (ColumnCategory.Formatted == columnCategory || ColumnCategory.FormattedSDDLText == columnCategory)
259 {
260 columnModularizeType = ColumnModularizeType.Property;
261 }
262 else if (ColumnCategory.Identifier == columnCategory)
263 {
264 columnModularizeType = ColumnModularizeType.Column;
265 }
266
267 columns.Add(new ColumnDefinition(columnName, columnType, length, primary, nullable, columnModularizeType, (ColumnType.Localized == columnType), minValueSet, minValue, maxValueSet, maxValue, keyTable, keyColumnSet, keyColumn, columnCategory, set, description, true, true));
268 }
269 }
270
271 TableDefinition tableDefinition = new TableDefinition(tableName, columns, false, false);
272
273 // use our table definitions if core properties are the same; this allows us to take advantage
274 // of wix concepts like localizable columns which current code assumes
275 if (this.TableDefinitions.Contains(tableName) && 0 == tableDefinition.CompareTo(this.TableDefinitions[tableName]))
276 {
277 tableDefinition = this.TableDefinitions[tableName];
278 }
279
280 Table table = new Table(null, tableDefinition);
281
282 while (true)
283 {
284 using (Record rowRecord = tableView.Fetch())
285 {
286 if (null == rowRecord)
287 {
288 break;
289 }
290
291 int recordCount = rowRecord.GetFieldCount();
292 Row row = table.CreateRow(output.SourceLineNumbers);
293
294 for (int i = 0; recordCount > i && row.Fields.Length > i; i++)
295 {
296 if (rowRecord.IsNull(i + 1))
297 {
298 if (!row.Fields[i].Column.Nullable)
299 {
300 // TODO: display an error for a null value in a non-nullable field OR
301 // display a warning and put an empty string in the value to let the compiler handle it
302 // (the second option is risky because the later code may make certain assumptions about
303 // the contents of a row value)
304 }
305 }
306 else
307 {
308 switch (row.Fields[i].Column.Type)
309 {
310 case ColumnType.Number:
311 bool success = false;
312 int intValue = rowRecord.GetInteger(i + 1);
313 if (row.Fields[i].Column.IsLocalizable)
314 {
315 success = row.BestEffortSetField(i, Convert.ToString(intValue, CultureInfo.InvariantCulture));
316 }
317 else
318 {
319 success = row.BestEffortSetField(i, intValue);
320 }
321
322 if (!success)
323 {
324 this.Messaging.OnMessage(WixWarnings.BadColumnDataIgnored(row.SourceLineNumbers, Convert.ToString(intValue, CultureInfo.InvariantCulture), tableName, row.Fields[i].Column.Name));
325 }
326 break;
327 case ColumnType.Object:
328 string sourceFile = "FILE NOT EXPORTED, USE THE dark.exe -x OPTION TO EXPORT BINARIES";
329
330 if (null != this.ExportBasePath)
331 {
332 string relativeSourceFile = Path.Combine(tableName, row.GetPrimaryKey('.'));
333 sourceFile = Path.Combine(this.ExportBasePath, relativeSourceFile);
334
335 // ensure the parent directory exists
336 System.IO.Directory.CreateDirectory(Path.Combine(this.ExportBasePath, tableName));
337
338 using (FileStream fs = System.IO.File.Create(sourceFile))
339 {
340 int bytesRead;
341 byte[] buffer = new byte[512];
342
343 while (0 != (bytesRead = rowRecord.GetStream(i + 1, buffer, buffer.Length)))
344 {
345 fs.Write(buffer, 0, bytesRead);
346 }
347 }
348 }
349
350 row[i] = sourceFile;
351 break;
352 default:
353 string value = rowRecord.GetString(i + 1);
354
355 switch (row.Fields[i].Column.Category)
356 {
357 case ColumnCategory.Guid:
358 value = value.ToUpper(CultureInfo.InvariantCulture);
359 break;
360 }
361
362 // de-modularize
363 if (!this.SuppressDemodularization && OutputType.Module == output.Type && ColumnModularizeType.None != row.Fields[i].Column.ModularizeType)
364 {
365 Regex modularization = new Regex(@"\.[0-9A-Fa-f]{8}_[0-9A-Fa-f]{4}_[0-9A-Fa-f]{4}_[0-9A-Fa-f]{4}_[0-9A-Fa-f]{12}");
366
367 if (null == modularizationGuid)
368 {
369 Match match = modularization.Match(value);
370 if (match.Success)
371 {
372 modularizationGuid = String.Concat('{', match.Value.Substring(1).Replace('_', '-'), '}');
373 }
374 }
375
376 value = modularization.Replace(value, String.Empty);
377 }
378
379 // escape "$(" for the preprocessor
380 value = value.Replace("$(", "$$(");
381
382 // escape things that look like wix variables
383 MatchCollection matches = Common.WixVariableRegex.Matches(value);
384 for (int j = matches.Count - 1; 0 <= j; j--)
385 {
386 value = value.Insert(matches[j].Index, "!");
387 }
388
389 row[i] = value;
390 break;
391 }
392 }
393 }
394 }
395 }
396
397 output.Tables.Add(table);
398 }
399
400 }
401 }
402 }
403 }
404 finally
405 {
406 if (null != validationView)
407 {
408 validationView.Close();
409 }
410 }
411
412 // set the modularization guid as the PackageCode
413 if (null != modularizationGuid)
414 {
415 Table table = output.Tables["_SummaryInformation"];
416
417 foreach (Row row in table.Rows)
418 {
419 if (9 == (int)row[0]) // PID_REVNUMBER
420 {
421 row[1] = modularizationGuid;
422 }
423 }
424 }
425
426 if (this.IsAdminImage)
427 {
428 GenerateWixFileTable(this.DatabasePath, output);
429 GenerateSectionIds(output);
430 }
431
432 return output;
433 }
434
435 /// <summary>
436 /// Generates the WixFile table based on a path to an admin image msi and an Output.
437 /// </summary>
438 /// <param name="databaseFile">The path to the msi database file in an admin image.</param>
439 /// <param name="output">The Output that represents the msi database.</param>
440 private void GenerateWixFileTable(string databaseFile, Output output)
441 {
442 string adminRootPath = Path.GetDirectoryName(databaseFile);
443
444 Hashtable componentDirectoryIndex = new Hashtable();
445 Table componentTable = output.Tables["Component"];
446 foreach (Row row in componentTable.Rows)
447 {
448 componentDirectoryIndex.Add(row[0], row[2]);
449 }
450
451 // Index full source paths for all directories
452 Hashtable directoryDirectoryParentIndex = new Hashtable();
453 Hashtable directoryFullPathIndex = new Hashtable();
454 Hashtable directorySourceNameIndex = new Hashtable();
455 Table directoryTable = output.Tables["Directory"];
456 foreach (Row row in directoryTable.Rows)
457 {
458 directoryDirectoryParentIndex.Add(row[0], row[1]);
459 if (null == row[1])
460 {
461 directoryFullPathIndex.Add(row[0], adminRootPath);
462 }
463 else
464 {
465 directorySourceNameIndex.Add(row[0], GetAdminSourceName((string)row[2]));
466 }
467 }
468
469 foreach (DictionaryEntry directoryEntry in directoryDirectoryParentIndex)
470 {
471 if (!directoryFullPathIndex.ContainsKey(directoryEntry.Key))
472 {
473 GetAdminFullPath((string)directoryEntry.Key, directoryDirectoryParentIndex, directorySourceNameIndex, directoryFullPathIndex);
474 }
475 }
476
477 Table fileTable = output.Tables["File"];
478 Table wixFileTable = output.EnsureTable(this.TableDefinitions["WixFile"]);
479 foreach (Row row in fileTable.Rows)
480 {
481 WixFileRow wixFileRow = new WixFileRow(null, this.TableDefinitions["WixFile"]);
482 wixFileRow.File = (string)row[0];
483 wixFileRow.Directory = (string)componentDirectoryIndex[(string)row[1]];
484 wixFileRow.Source = Path.Combine((string)directoryFullPathIndex[wixFileRow.Directory], GetAdminSourceName((string)row[2]));
485
486 if (!File.Exists(wixFileRow.Source))
487 {
488 throw new WixException(WixErrors.WixFileNotFound(wixFileRow.Source));
489 }
490
491 wixFileTable.Rows.Add(wixFileRow);
492 }
493 }
494
495 /// <summary>
496 /// Gets the full path of a directory. Populates the full path index with the directory's full path and all of its parent directorie's full paths.
497 /// </summary>
498 /// <param name="directory">The directory identifier.</param>
499 /// <param name="directoryDirectoryParentIndex">The Hashtable containing all the directory to directory parent mapping.</param>
500 /// <param name="directorySourceNameIndex">The Hashtable containing all the directory to source name mapping.</param>
501 /// <param name="directoryFullPathIndex">The Hashtable containing a mapping between all of the directories and their previously calculated full paths.</param>
502 /// <returns>The full path to the directory.</returns>
503 private string GetAdminFullPath(string directory, Hashtable directoryDirectoryParentIndex, Hashtable directorySourceNameIndex, Hashtable directoryFullPathIndex)
504 {
505 string parent = (string)directoryDirectoryParentIndex[directory];
506 string sourceName = (string)directorySourceNameIndex[directory];
507
508 string parentFullPath;
509 if (directoryFullPathIndex.ContainsKey(parent))
510 {
511 parentFullPath = (string)directoryFullPathIndex[parent];
512 }
513 else
514 {
515 parentFullPath = GetAdminFullPath(parent, directoryDirectoryParentIndex, directorySourceNameIndex, directoryFullPathIndex);
516 }
517
518 if (null == sourceName)
519 {
520 sourceName = String.Empty;
521 }
522
523 string fullPath = Path.Combine(parentFullPath, sourceName);
524 directoryFullPathIndex.Add(directory, fullPath);
525
526 return fullPath;
527 }
528
529 /// <summary>
530 /// Get the source name in an admin image.
531 /// </summary>
532 /// <param name="value">The Filename value.</param>
533 /// <returns>The source name of the directory in an admin image.</returns>
534 private static string GetAdminSourceName(string value)
535 {
536 string name = null;
537 string[] names;
538 string shortname = null;
539 string shortsourcename = null;
540 string sourcename = null;
541
542 names = Common.GetNames(value);
543
544 if (null != names[0] && "." != names[0])
545 {
546 if (null != names[1])
547 {
548 shortname = names[0];
549 }
550 else
551 {
552 name = names[0];
553 }
554 }
555
556 if (null != names[1])
557 {
558 name = names[1];
559 }
560
561 if (null != names[2])
562 {
563 if (null != names[3])
564 {
565 shortsourcename = names[2];
566 }
567 else
568 {
569 sourcename = names[2];
570 }
571 }
572
573 if (null != names[3])
574 {
575 sourcename = names[3];
576 }
577
578 if (null != sourcename)
579 {
580 return sourcename;
581 }
582 else if (null != shortsourcename)
583 {
584 return shortsourcename;
585 }
586 else if (null != name)
587 {
588 return name;
589 }
590 else
591 {
592 return shortname;
593 }
594 }
595
596 /// <summary>
597 /// Creates section ids on rows which form logical groupings of resources.
598 /// </summary>
599 /// <param name="output">The Output that represents the msi database.</param>
600 private void GenerateSectionIds(Output output)
601 {
602 // First assign and index section ids for the tables that are in their own sections.
603 AssignSectionIdsToTable(output.Tables["Binary"], 0);
604 Hashtable componentSectionIdIndex = AssignSectionIdsToTable(output.Tables["Component"], 0);
605 Hashtable customActionSectionIdIndex = AssignSectionIdsToTable(output.Tables["CustomAction"], 0);
606 AssignSectionIdsToTable(output.Tables["Directory"], 0);
607 Hashtable featureSectionIdIndex = AssignSectionIdsToTable(output.Tables["Feature"], 0);
608 AssignSectionIdsToTable(output.Tables["Icon"], 0);
609 Hashtable digitalCertificateSectionIdIndex = AssignSectionIdsToTable(output.Tables["MsiDigitalCertificate"], 0);
610 AssignSectionIdsToTable(output.Tables["Property"], 0);
611
612 // Now handle all the tables that rely on the first set of indexes but also produce their own indexes. Order matters here.
613 Hashtable fileSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["File"], componentSectionIdIndex, 1, 0);
614 Hashtable appIdSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["Class"], componentSectionIdIndex, 2, 5);
615 Hashtable odbcDataSourceSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["ODBCDataSource"], componentSectionIdIndex, 1, 0);
616 Hashtable odbcDriverSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["ODBCDriver"], componentSectionIdIndex, 1, 0);
617 Hashtable registrySectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["Registry"], componentSectionIdIndex, 5, 0);
618 Hashtable serviceInstallSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["ServiceInstall"], componentSectionIdIndex, 11, 0);
619
620 // Now handle all the tables which only rely on previous indexes and order does not matter.
621 foreach (Table table in output.Tables)
622 {
623 switch (table.Name)
624 {
625 case "WixFile":
626 case "MsiFileHash":
627 ConnectTableToSection(table, fileSectionIdIndex, 0);
628 break;
629 case "MsiAssembly":
630 case "MsiAssemblyName":
631 ConnectTableToSection(table, componentSectionIdIndex, 0);
632 break;
633 case "MsiPackageCertificate":
634 case "MsiPatchCertificate":
635 ConnectTableToSection(table, digitalCertificateSectionIdIndex, 1);
636 break;
637 case "CreateFolder":
638 case "FeatureComponents":
639 case "MoveFile":
640 case "ReserveCost":
641 case "ODBCTranslator":
642 ConnectTableToSection(table, componentSectionIdIndex, 1);
643 break;
644 case "TypeLib":
645 ConnectTableToSection(table, componentSectionIdIndex, 2);
646 break;
647 case "Shortcut":
648 case "Environment":
649 ConnectTableToSection(table, componentSectionIdIndex, 3);
650 break;
651 case "RemoveRegistry":
652 ConnectTableToSection(table, componentSectionIdIndex, 4);
653 break;
654 case "ServiceControl":
655 ConnectTableToSection(table, componentSectionIdIndex, 5);
656 break;
657 case "IniFile":
658 case "RemoveIniFile":
659 ConnectTableToSection(table, componentSectionIdIndex, 7);
660 break;
661 case "AppId":
662 ConnectTableToSection(table, appIdSectionIdIndex, 0);
663 break;
664 case "Condition":
665 ConnectTableToSection(table, featureSectionIdIndex, 0);
666 break;
667 case "ODBCSourceAttribute":
668 ConnectTableToSection(table, odbcDataSourceSectionIdIndex, 0);
669 break;
670 case "ODBCAttribute":
671 ConnectTableToSection(table, odbcDriverSectionIdIndex, 0);
672 break;
673 case "AdminExecuteSequence":
674 case "AdminUISequence":
675 case "AdvtExecuteSequence":
676 case "AdvtUISequence":
677 case "InstallExecuteSequence":
678 case "InstallUISequence":
679 ConnectTableToSection(table, customActionSectionIdIndex, 0);
680 break;
681 case "LockPermissions":
682 case "MsiLockPermissions":
683 foreach (Row row in table.Rows)
684 {
685 string lockObject = (string)row[0];
686 string tableName = (string)row[1];
687 switch (tableName)
688 {
689 case "File":
690 row.SectionId = (string)fileSectionIdIndex[lockObject];
691 break;
692 case "Registry":
693 row.SectionId = (string)registrySectionIdIndex[lockObject];
694 break;
695 case "ServiceInstall":
696 row.SectionId = (string)serviceInstallSectionIdIndex[lockObject];
697 break;
698 }
699 }
700 break;
701 }
702 }
703
704 // Now pass the output to each unbinder extension to allow them to analyze the output and determine thier proper section ids.
705 //foreach (IUnbinderExtension extension in this.unbinderExtensions)
706 //{
707 // extension.GenerateSectionIds(output);
708 //}
709 }
710
711 /// <summary>
712 /// Creates new section ids on all the rows in a table.
713 /// </summary>
714 /// <param name="table">The table to add sections to.</param>
715 /// <param name="rowPrimaryKeyIndex">The index of the column which is used by other tables to reference this table.</param>
716 /// <returns>A Hashtable containing the tables key for each row paired with its assigned section id.</returns>
717 private Hashtable AssignSectionIdsToTable(Table table, int rowPrimaryKeyIndex)
718 {
719 Hashtable hashtable = new Hashtable();
720 if (null != table)
721 {
722 foreach (Row row in table.Rows)
723 {
724 row.SectionId = GetNewSectionId();
725 hashtable.Add(row[rowPrimaryKeyIndex], row.SectionId);
726 }
727 }
728 return hashtable;
729 }
730
731 /// <summary>
732 /// Connects a table's rows to an already sectioned table.
733 /// </summary>
734 /// <param name="table">The table containing rows that need to be connected to sections.</param>
735 /// <param name="sectionIdIndex">A hashtable containing keys to map table to its section.</param>
736 /// <param name="rowIndex">The index of the column which is used as the foreign key in to the sectionIdIndex.</param>
737 private static void ConnectTableToSection(Table table, Hashtable sectionIdIndex, int rowIndex)
738 {
739 if (null != table)
740 {
741 foreach (Row row in table.Rows)
742 {
743 if (sectionIdIndex.ContainsKey(row[rowIndex]))
744 {
745 row.SectionId = (string)sectionIdIndex[row[rowIndex]];
746 }
747 }
748 }
749 }
750
751 /// <summary>
752 /// Connects a table's rows to an already sectioned table and produces an index for other tables to connect to it.
753 /// </summary>
754 /// <param name="table">The table containing rows that need to be connected to sections.</param>
755 /// <param name="sectionIdIndex">A hashtable containing keys to map table to its section.</param>
756 /// <param name="rowIndex">The index of the column which is used as the foreign key in to the sectionIdIndex.</param>
757 /// <param name="rowPrimaryKeyIndex">The index of the column which is used by other tables to reference this table.</param>
758 /// <returns>A Hashtable containing the tables key for each row paired with its assigned section id.</returns>
759 private static Hashtable ConnectTableToSectionAndIndex(Table table, Hashtable sectionIdIndex, int rowIndex, int rowPrimaryKeyIndex)
760 {
761 Hashtable newHashTable = new Hashtable();
762 if (null != table)
763 {
764 foreach (Row row in table.Rows)
765 {
766 if (!sectionIdIndex.ContainsKey(row[rowIndex]))
767 {
768 continue;
769 }
770
771 row.SectionId = (string)sectionIdIndex[row[rowIndex]];
772 if (null != row[rowPrimaryKeyIndex])
773 {
774 newHashTable.Add(row[rowPrimaryKeyIndex], row.SectionId);
775 }
776 }
777 }
778 return newHashTable;
779 }
780
781 /// <summary>
782 /// Creates a new section identifier to be used when adding a section to an output.
783 /// </summary>
784 /// <returns>A string representing a new section id.</returns>
785 private string GetNewSectionId()
786 {
787 this.SectionCount++;
788 return "wix.section." + this.SectionCount.ToString(CultureInfo.InvariantCulture);
789 }
790 }
791}
diff --git a/src/WixToolset.Core.WindowsInstaller/Unbind/UnbindMsiOrMsmCommand.cs b/src/WixToolset.Core.WindowsInstaller/Unbind/UnbindMsiOrMsmCommand.cs
new file mode 100644
index 00000000..f04dcefe
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Unbind/UnbindMsiOrMsmCommand.cs
@@ -0,0 +1,53 @@
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.Unbind
4{
5 using System;
6 using System.ComponentModel;
7 using WixToolset.Core.Native;
8 using WixToolset.Data;
9 using WixToolset.Extensibility;
10 using WixToolset.Msi;
11
12 internal class UnbindMsiOrMsmCommand
13 {
14 public UnbindMsiOrMsmCommand(IUnbindContext context)
15 {
16 this.Context = context;
17 }
18
19 public IUnbindContext Context { get; }
20
21 public Output Execute()
22 {
23 Output output;
24
25 try
26 {
27 using (Database database = new Database(this.Context.InputFilePath, OpenDatabase.ReadOnly))
28 {
29 var unbindCommand = new UnbindDatabaseCommand(this.Context.Messaging, database, this.Context.InputFilePath, OutputType.Product, this.Context.ExportBasePath, this.Context.IntermediateFolder, this.Context.IsAdminImage, this.Context.SuppressDemodularization, skipSummaryInfo: false);
30 output = unbindCommand.Execute();
31
32 // extract the files from the cabinets
33 if (!String.IsNullOrEmpty(this.Context.ExportBasePath) && !this.Context.SuppressExtractCabinets)
34 {
35 var extractCommand = new ExtractCabinetsCommand(output, database, this.Context.InputFilePath, this.Context.ExportBasePath, this.Context.IntermediateFolder);
36 extractCommand.Execute();
37 }
38 }
39 }
40 catch (Win32Exception e)
41 {
42 if (0x6E == e.NativeErrorCode) // ERROR_OPEN_FAILED
43 {
44 throw new WixException(WixErrors.OpenDatabaseFailed(this.Context.InputFilePath));
45 }
46
47 throw;
48 }
49
50 return output;
51 }
52 }
53}
diff --git a/src/WixToolset.Core.WindowsInstaller/Unbind/UnbindTranformCommand.cs b/src/WixToolset.Core.WindowsInstaller/Unbind/UnbindTranformCommand.cs
new file mode 100644
index 00000000..c0eda9c7
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Unbind/UnbindTranformCommand.cs
@@ -0,0 +1,317 @@
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.Unbind
4{
5 using System;
6 using System.Collections;
7 using System.Collections.Generic;
8 using System.ComponentModel;
9 using System.Globalization;
10 using System.IO;
11 using System.Linq;
12 using System.Text.RegularExpressions;
13 using WixToolset.Core.Native;
14 using WixToolset.Core.WindowsInstaller.Databases;
15 using WixToolset.Data;
16 using WixToolset.Data.Rows;
17 using WixToolset.Extensibility;
18 using WixToolset.Msi;
19
20 internal class UnbindTransformCommand
21 {
22 public UnbindTransformCommand(Messaging messaging, string transformFile, string exportBasePath, string intermediateFolder)
23 {
24 this.Messaging = messaging;
25 this.TransformFile = transformFile;
26 this.ExportBasePath = exportBasePath;
27 this.IntermediateFolder = intermediateFolder;
28
29 this.TableDefinitions = WindowsInstallerStandard.GetTableDefinitions();
30 }
31
32 private Messaging Messaging { get; }
33
34 private string TransformFile { get; }
35
36 private string ExportBasePath { get; }
37
38 private string IntermediateFolder { get; }
39
40 private TableDefinitionCollection TableDefinitions { get; }
41
42 private string EmptyFile { get; set; }
43
44 public Output Execute()
45 {
46 Output transform = new Output(new SourceLineNumber(this.TransformFile));
47 transform.Type = OutputType.Transform;
48
49 // get the summary information table
50 using (SummaryInformation summaryInformation = new SummaryInformation(this.TransformFile))
51 {
52 Table table = transform.EnsureTable(this.TableDefinitions["_SummaryInformation"]);
53
54 for (int i = 1; 19 >= i; i++)
55 {
56 string value = summaryInformation.GetProperty(i);
57
58 if (0 < value.Length)
59 {
60 Row row = table.CreateRow(transform.SourceLineNumbers);
61 row[0] = i;
62 row[1] = value;
63 }
64 }
65 }
66
67 // create a schema msi which hopefully matches the table schemas in the transform
68 Output schemaOutput = new Output(null);
69 string msiDatabaseFile = Path.Combine(this.IntermediateFolder, "schema.msi");
70 foreach (TableDefinition tableDefinition in this.TableDefinitions)
71 {
72 // skip unreal tables and the Patch table
73 if (!tableDefinition.Unreal && "Patch" != tableDefinition.Name)
74 {
75 schemaOutput.EnsureTable(tableDefinition);
76 }
77 }
78
79 Hashtable addedRows = new Hashtable();
80 Table transformViewTable;
81
82 // Bind the schema msi.
83 this.GenerateDatabase(schemaOutput, msiDatabaseFile);
84
85 // apply the transform to the database and retrieve the modifications
86 using (Database msiDatabase = new Database(msiDatabaseFile, OpenDatabase.Transact))
87 {
88 // apply the transform with the ViewTransform option to collect all the modifications
89 msiDatabase.ApplyTransform(this.TransformFile, TransformErrorConditions.All | TransformErrorConditions.ViewTransform);
90
91 // unbind the database
92 var unbindCommand = new UnbindDatabaseCommand(this.Messaging, msiDatabase, msiDatabaseFile, OutputType.Product, this.ExportBasePath, this.IntermediateFolder, false, false, skipSummaryInfo: true);
93 Output transformViewOutput = unbindCommand.Execute();
94
95 // index the added and possibly modified rows (added rows may also appears as modified rows)
96 transformViewTable = transformViewOutput.Tables["_TransformView"];
97 Hashtable modifiedRows = new Hashtable();
98 foreach (Row row in transformViewTable.Rows)
99 {
100 string tableName = (string)row[0];
101 string columnName = (string)row[1];
102 string primaryKeys = (string)row[2];
103
104 if ("INSERT" == columnName)
105 {
106 string index = String.Concat(tableName, ':', primaryKeys);
107
108 addedRows.Add(index, null);
109 }
110 else if ("CREATE" != columnName && "DELETE" != columnName && "DROP" != columnName && null != primaryKeys) // modified row
111 {
112 string index = String.Concat(tableName, ':', primaryKeys);
113
114 modifiedRows[index] = row;
115 }
116 }
117
118 // create placeholder rows for modified rows to make the transform insert the updated values when its applied
119 foreach (Row row in modifiedRows.Values)
120 {
121 string tableName = (string)row[0];
122 string columnName = (string)row[1];
123 string primaryKeys = (string)row[2];
124
125 string index = String.Concat(tableName, ':', primaryKeys);
126
127 // ignore information for added rows
128 if (!addedRows.Contains(index))
129 {
130 Table table = schemaOutput.Tables[tableName];
131 this.CreateRow(table, primaryKeys, true);
132 }
133 }
134 }
135
136 // Re-bind the schema output with the placeholder rows.
137 this.GenerateDatabase(schemaOutput, msiDatabaseFile);
138
139 // apply the transform to the database and retrieve the modifications
140 using (Database msiDatabase = new Database(msiDatabaseFile, OpenDatabase.Transact))
141 {
142 try
143 {
144 // apply the transform
145 msiDatabase.ApplyTransform(this.TransformFile, TransformErrorConditions.All);
146
147 // commit the database to guard against weird errors with streams
148 msiDatabase.Commit();
149 }
150 catch (Win32Exception ex)
151 {
152 if (0x65B == ex.NativeErrorCode)
153 {
154 // this commonly happens when the transform was built
155 // against a database schema different from the internal
156 // table definitions
157 throw new WixException(WixErrors.TransformSchemaMismatch());
158 }
159 }
160
161 // unbind the database
162 var unbindCommand = new UnbindDatabaseCommand(this.Messaging, msiDatabase, msiDatabaseFile, OutputType.Product, this.ExportBasePath, this.IntermediateFolder, false, false, skipSummaryInfo: true);
163 Output output = unbindCommand.Execute();
164
165 // index all the rows to easily find modified rows
166 Hashtable rows = new Hashtable();
167 foreach (Table table in output.Tables)
168 {
169 foreach (Row row in table.Rows)
170 {
171 rows.Add(String.Concat(table.Name, ':', row.GetPrimaryKey('\t', " ")), row);
172 }
173 }
174
175 // process the _TransformView rows into transform rows
176 foreach (Row row in transformViewTable.Rows)
177 {
178 string tableName = (string)row[0];
179 string columnName = (string)row[1];
180 string primaryKeys = (string)row[2];
181
182 Table table = transform.EnsureTable(this.TableDefinitions[tableName]);
183
184 if ("CREATE" == columnName) // added table
185 {
186 table.Operation = TableOperation.Add;
187 }
188 else if ("DELETE" == columnName) // deleted row
189 {
190 Row deletedRow = this.CreateRow(table, primaryKeys, false);
191 deletedRow.Operation = RowOperation.Delete;
192 }
193 else if ("DROP" == columnName) // dropped table
194 {
195 table.Operation = TableOperation.Drop;
196 }
197 else if ("INSERT" == columnName) // added row
198 {
199 string index = String.Concat(tableName, ':', primaryKeys);
200 Row addedRow = (Row)rows[index];
201 addedRow.Operation = RowOperation.Add;
202 table.Rows.Add(addedRow);
203 }
204 else if (null != primaryKeys) // modified row
205 {
206 string index = String.Concat(tableName, ':', primaryKeys);
207
208 // the _TransformView table includes information for added rows
209 // that looks like modified rows so it sometimes needs to be ignored
210 if (!addedRows.Contains(index))
211 {
212 Row modifiedRow = (Row)rows[index];
213
214 // mark the field as modified
215 int indexOfModifiedValue = -1;
216 for (int i = 0; i < modifiedRow.TableDefinition.Columns.Count; ++i)
217 {
218 if (columnName.Equals(modifiedRow.TableDefinition.Columns[i].Name, StringComparison.Ordinal))
219 {
220 indexOfModifiedValue = i;
221 break;
222 }
223 }
224 modifiedRow.Fields[indexOfModifiedValue].Modified = true;
225
226 // move the modified row into the transform the first time its encountered
227 if (RowOperation.None == modifiedRow.Operation)
228 {
229 modifiedRow.Operation = RowOperation.Modify;
230 table.Rows.Add(modifiedRow);
231 }
232 }
233 }
234 else // added column
235 {
236 ColumnDefinition column = table.Definition.Columns.Single(c => c.Name.Equals(columnName, StringComparison.Ordinal));
237 column.Added = true;
238 }
239 }
240 }
241
242 return transform;
243 }
244
245 private void GenerateDatabase(Output output, string databaseFile)
246 {
247 var command = new GenerateDatabaseCommand();
248 command.Extensions = Array.Empty<IBinderExtension>();
249 command.Output = output;
250 command.OutputPath = databaseFile;
251 command.KeepAddedColumns = true;
252 command.UseSubDirectory = false;
253 command.SuppressAddingValidationRows = true;
254 command.TableDefinitions = this.TableDefinitions;
255 command.TempFilesLocation = this.IntermediateFolder;
256 command.Codepage = -1;
257 command.Execute();
258 }
259
260 /// <summary>
261 /// Create a deleted or modified row.
262 /// </summary>
263 /// <param name="table">The table containing the row.</param>
264 /// <param name="primaryKeys">The primary keys of the row.</param>
265 /// <param name="setRequiredFields">Option to set all required fields with placeholder values.</param>
266 /// <returns>The new row.</returns>
267 private Row CreateRow(Table table, string primaryKeys, bool setRequiredFields)
268 {
269 Row row = table.CreateRow(null);
270
271 string[] primaryKeyParts = primaryKeys.Split('\t');
272 int primaryKeyPartIndex = 0;
273
274 for (int i = 0; i < table.Definition.Columns.Count; i++)
275 {
276 ColumnDefinition columnDefinition = table.Definition.Columns[i];
277
278 if (columnDefinition.PrimaryKey)
279 {
280 if (ColumnType.Number == columnDefinition.Type && !columnDefinition.IsLocalizable)
281 {
282 row[i] = Convert.ToInt32(primaryKeyParts[primaryKeyPartIndex++], CultureInfo.InvariantCulture);
283 }
284 else
285 {
286 row[i] = primaryKeyParts[primaryKeyPartIndex++];
287 }
288 }
289 else if (setRequiredFields)
290 {
291 if (ColumnType.Number == columnDefinition.Type && !columnDefinition.IsLocalizable)
292 {
293 row[i] = 1;
294 }
295 else if (ColumnType.Object == columnDefinition.Type)
296 {
297 if (null == this.EmptyFile)
298 {
299 this.EmptyFile = Path.GetTempFileName() + ".empty";
300 using (FileStream fileStream = File.Create(this.EmptyFile))
301 {
302 }
303 }
304
305 row[i] = this.EmptyFile;
306 }
307 else
308 {
309 row[i] = "1";
310 }
311 }
312 }
313
314 return row;
315 }
316 }
317}
diff --git a/src/WixToolset.Core.WindowsInstaller/Validator.cs b/src/WixToolset.Core.WindowsInstaller/Validator.cs
new file mode 100644
index 00000000..db66f600
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Validator.cs
@@ -0,0 +1,385 @@
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
4{
5 using System;
6 using System.Collections.Generic;
7 using System.Collections.Specialized;
8 using System.ComponentModel;
9 using System.Diagnostics.CodeAnalysis;
10 using System.Globalization;
11 using System.IO;
12 using System.Threading;
13 using WixToolset.Data;
14 using WixToolset.Extensibility;
15 using WixToolset.Core.Native;
16 using WixToolset.Msi;
17 using System.Linq;
18 using System.Reflection;
19
20 /// <summary>
21 /// Runs internal consistency evaluators (ICEs) from cub files against a database.
22 /// </summary>
23 public sealed class Validator : IMessageHandler
24 {
25 private string actionName;
26 private StringCollection cubeFiles;
27 private ValidatorExtension extension;
28 private Output output;
29 private InstallUIHandler validationUIHandler;
30 private bool validationSessionComplete;
31
32 /// <summary>
33 /// Instantiate a new Validator.
34 /// </summary>
35 public Validator()
36 {
37 this.cubeFiles = new StringCollection();
38 this.extension = new ValidatorExtension();
39 this.validationUIHandler = new InstallUIHandler(this.ValidationUIHandler);
40 }
41
42 /// <summary>
43 /// Gets or sets a <see cref="ValidatorExtension"/> that directs messages from the validator.
44 /// </summary>
45 /// <value>A <see cref="ValidatorExtension"/> that directs messages from the validator.</value>
46 public ValidatorExtension Extension
47 {
48 get { return this.extension; }
49 set { this.extension = value; }
50 }
51
52 /// <summary>
53 /// Gets or sets the list of ICEs to run.
54 /// </summary>
55 /// <value>The list of ICEs.</value>
56 [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")]
57 public ISet<string> ICEs { get; set; }
58
59 /// <summary>
60 /// Gets or sets the output used for finding source line information.
61 /// </summary>
62 /// <value>The output used for finding source line information.</value>
63 public Output Output
64 {
65 // cache Output object until validation for changes in extension
66 get { return this.output; }
67 set { this.output = value; }
68 }
69
70 /// <summary>
71 /// Gets or sets the suppressed ICEs.
72 /// </summary>
73 /// <value>The suppressed ICEs.</value>
74 [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")]
75 public ISet<string> SuppressedICEs { get; set; }
76
77 /// <summary>
78 /// Sets the temporary path for the Binder.
79 /// </summary>
80 public string IntermediateFolder { private get; set; }
81
82 /// <summary>
83 /// Add a cube file to the validation run.
84 /// </summary>
85 /// <param name="cubeFile">A cube file.</param>
86 public void AddCubeFile(string cubeFile)
87 {
88 this.cubeFiles.Add(cubeFile);
89 }
90
91 /// <summary>
92 /// Validate a database.
93 /// </summary>
94 /// <param name="databaseFile">The database to validate.</param>
95 /// <returns>true if validation succeeded; false otherwise.</returns>
96 public void Validate(string databaseFile)
97 {
98 int previousUILevel = (int)InstallUILevels.Basic;
99 IntPtr previousHwnd = IntPtr.Zero;
100 InstallUIHandler previousUIHandler = null;
101
102 if (null == databaseFile)
103 {
104 throw new ArgumentNullException("databaseFile");
105 }
106
107 // initialize the validator extension
108 this.extension.DatabaseFile = databaseFile;
109 this.extension.Output = this.output;
110 this.extension.InitializeValidator();
111
112 // Ensure the temporary files can be created.
113 Directory.CreateDirectory(this.IntermediateFolder);
114
115 // copy the database to a temporary location so it can be manipulated
116 string tempDatabaseFile = Path.Combine(this.IntermediateFolder, Path.GetFileName(databaseFile));
117 File.Copy(databaseFile, tempDatabaseFile);
118
119 // remove the read-only property from the temporary database
120 FileAttributes attributes = File.GetAttributes(tempDatabaseFile);
121 File.SetAttributes(tempDatabaseFile, attributes & ~FileAttributes.ReadOnly);
122
123 Mutex mutex = new Mutex(false, "WixValidator");
124 try
125 {
126 if (!mutex.WaitOne(0, false))
127 {
128 this.OnMessage(WixVerboses.ValidationSerialized());
129 mutex.WaitOne();
130 }
131
132 using (Database database = new Database(tempDatabaseFile, OpenDatabase.Direct))
133 {
134 bool propertyTableExists = database.TableExists("Property");
135 string productCode = null;
136
137 // remove the product code from the database before opening a session to prevent opening an installed product
138 if (propertyTableExists)
139 {
140 using (View view = database.OpenExecuteView("SELECT `Value` FROM `Property` WHERE Property = 'ProductCode'"))
141 {
142 using (Record record = view.Fetch())
143 {
144 if (null != record)
145 {
146 productCode = record.GetString(1);
147
148 using (View dropProductCodeView = database.OpenExecuteView("DELETE FROM `Property` WHERE `Property` = 'ProductCode'"))
149 {
150 }
151 }
152 }
153 }
154 }
155
156 // merge in the cube databases
157 foreach (string cubeFile in this.cubeFiles)
158 {
159 try
160 {
161 using (Database cubeDatabase = new Database(cubeFile, OpenDatabase.ReadOnly))
162 {
163 try
164 {
165 database.Merge(cubeDatabase, "MergeConflicts");
166 }
167 catch
168 {
169 // ignore merge errors since they are expected in the _Validation table
170 }
171 }
172 }
173 catch (Win32Exception e)
174 {
175 if (0x6E == e.NativeErrorCode) // ERROR_OPEN_FAILED
176 {
177 throw new WixException(WixErrors.CubeFileNotFound(cubeFile));
178 }
179
180 throw;
181 }
182 }
183
184 // commit the database before proceeding to ensure the streams don't get confused
185 database.Commit();
186
187 // the property table may have been added to the database
188 // from a cub database without the proper validation rows
189 if (!propertyTableExists)
190 {
191 using (View view = database.OpenExecuteView("DROP table `Property`"))
192 {
193 }
194 }
195
196 // get all the action names for ICEs which have not been suppressed
197 List<string> actions = new List<string>();
198 using (View view = database.OpenExecuteView("SELECT `Action` FROM `_ICESequence` ORDER BY `Sequence`"))
199 {
200 while (true)
201 {
202 using (Record record = view.Fetch())
203 {
204 if (null == record)
205 {
206 break;
207 }
208
209 string action = record.GetString(1);
210
211 if ((this.SuppressedICEs == null || !this.SuppressedICEs.Contains(action)) && (this.ICEs == null || this.ICEs.Contains(action)))
212 {
213 actions.Add(action);
214 }
215 }
216 }
217 }
218
219 // disable the internal UI handler and set an external UI handler
220 previousUILevel = Installer.SetInternalUI((int)InstallUILevels.None, ref previousHwnd);
221 previousUIHandler = Installer.SetExternalUI(this.validationUIHandler, (int)InstallLogModes.Error | (int)InstallLogModes.Warning | (int)InstallLogModes.User, IntPtr.Zero);
222
223 // create a session for running the ICEs
224 this.validationSessionComplete = false;
225 using (Session session = new Session(database))
226 {
227 // add the product code back into the database
228 if (null != productCode)
229 {
230 // some CUBs erroneously have a ProductCode property, so delete it if we just picked one up
231 using (View dropProductCodeView = database.OpenExecuteView("DELETE FROM `Property` WHERE `Property` = 'ProductCode'"))
232 {
233 }
234
235 using (View view = database.OpenExecuteView(String.Format(CultureInfo.InvariantCulture, "INSERT INTO `Property` (`Property`, `Value`) VALUES ('ProductCode', '{0}')", productCode)))
236 {
237 }
238 }
239
240 foreach (string action in actions)
241 {
242 this.actionName = action;
243 try
244 {
245 session.DoAction(action);
246 }
247 catch (Win32Exception e)
248 {
249 if (!Messaging.Instance.EncounteredError)
250 {
251 throw e;
252 }
253 // TODO: Review why this was clearing the error state when an exception had happened but an error was already encountered. That's weird.
254 //else
255 //{
256 // this.encounteredError = false;
257 //}
258 }
259 this.actionName = null;
260 }
261
262 // Mark the validation session complete so we ignore any messages that MSI may fire
263 // during session clean-up.
264 this.validationSessionComplete = true;
265 }
266 }
267 }
268 catch (Win32Exception e)
269 {
270 // avoid displaying errors twice since one may have already occurred in the UI handler
271 if (!Messaging.Instance.EncounteredError)
272 {
273 if (0x6E == e.NativeErrorCode) // ERROR_OPEN_FAILED
274 {
275 // databaseFile is not passed since during light
276 // this would be the temporary copy and there would be
277 // no final output since the error occured; during smoke
278 // they should know the path passed into smoke
279 this.OnMessage(WixErrors.ValidationFailedToOpenDatabase());
280 }
281 else if (0x64D == e.NativeErrorCode)
282 {
283 this.OnMessage(WixErrors.ValidationFailedDueToLowMsiEngine());
284 }
285 else if (0x654 == e.NativeErrorCode)
286 {
287 this.OnMessage(WixErrors.ValidationFailedDueToInvalidPackage());
288 }
289 else if (0x658 == e.NativeErrorCode)
290 {
291 this.OnMessage(WixErrors.ValidationFailedDueToMultilanguageMergeModule());
292 }
293 else if (0x659 == e.NativeErrorCode)
294 {
295 this.OnMessage(WixWarnings.ValidationFailedDueToSystemPolicy());
296 }
297 else
298 {
299 string msgTemp = e.Message;
300
301 if (null != this.actionName)
302 {
303 msgTemp = String.Concat("Action - '", this.actionName, "' ", e.Message);
304 }
305
306 this.OnMessage(WixErrors.Win32Exception(e.NativeErrorCode, msgTemp));
307 }
308 }
309 }
310 finally
311 {
312 Installer.SetExternalUI(previousUIHandler, 0, IntPtr.Zero);
313 Installer.SetInternalUI(previousUILevel, ref previousHwnd);
314
315 this.validationSessionComplete = false; // no validation session at this point, so reset the completion flag.
316
317 mutex.ReleaseMutex();
318 this.cubeFiles.Clear();
319 this.extension.FinalizeValidator();
320 }
321 }
322
323 /// <summary>
324 /// Sends a message to the message delegate if there is one.
325 /// </summary>
326 /// <param name="mea">Message event arguments.</param>
327 public void OnMessage(MessageEventArgs e)
328 {
329 Messaging.Instance.OnMessage(e);
330 this.extension.OnMessage(e);
331 }
332
333 public static Validator CreateFromContext(IBindContext context, string cubeFilename)
334 {
335 Validator validator = null;
336
337 // Tell the binder about the validator if validation isn't suppressed
338 if (!context.SuppressValidation)
339 {
340 validator = new Validator();
341 validator.IntermediateFolder = Path.Combine(context.IntermediateFolder, "validate");
342
343 // set the default cube file
344 string thisPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
345 validator.AddCubeFile(Path.Combine(thisPath, cubeFilename));
346
347 // Set the ICEs
348 validator.ICEs = new SortedSet<string>(context.Ices);
349
350 // Set the suppressed ICEs and disable ICEs that have equivalent-or-better checks in WiX.
351 validator.SuppressedICEs = new SortedSet<string>(context.SuppressIces.Union(new[] { "ICE08", "ICE33", "ICE47", "ICE66" }));
352 }
353
354 return validator;
355 }
356
357 /// <summary>
358 /// The validation external UI handler.
359 /// </summary>
360 /// <param name="context">Pointer to an application context.
361 /// This parameter can be used for error checking.</param>
362 /// <param name="messageType">Specifies a combination of one message box style,
363 /// one message box icon type, one default button, and one installation message type.</param>
364 /// <param name="message">Specifies the message text.</param>
365 /// <returns>-1 for an error, 0 if no action was taken, 1 if OK, 3 to abort.</returns>
366 private int ValidationUIHandler(IntPtr context, uint messageType, string message)
367 {
368 try
369 {
370 // If we're getting messges during the validation session, send them to
371 // the extension. Otherwise, ignore the messages.
372 if (!this.validationSessionComplete)
373 {
374 this.extension.Log(message, this.actionName);
375 }
376 }
377 catch (WixException ex)
378 {
379 this.OnMessage(ex.Error);
380 }
381
382 return 1;
383 }
384 }
385}
diff --git a/src/WixToolset.Core.WindowsInstaller/ValidatorExtension.cs b/src/WixToolset.Core.WindowsInstaller/ValidatorExtension.cs
new file mode 100644
index 00000000..44ec3106
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/ValidatorExtension.cs
@@ -0,0 +1,299 @@
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.Extensibility
4{
5 using System;
6 using System.Collections;
7 using WixToolset.Data;
8
9 /// <summary>
10 /// Base class for creating a validator extension. This default implementation
11 /// will fire and event with the ICE name and description.
12 /// </summary>
13 public class ValidatorExtension : IMessageHandler
14 {
15 private string databaseFile;
16 private Hashtable indexedSourceLineNumbers;
17 private Output output;
18 private SourceLineNumber sourceLineNumbers;
19
20 /// <summary>
21 /// Instantiate a new <see cref="ValidatorExtension"/>.
22 /// </summary>
23 public ValidatorExtension()
24 {
25 }
26
27 /// <summary>
28 /// Gets or sets the path to the database to validate.
29 /// </summary>
30 /// <value>The path to the database to validate.</value>
31 public string DatabaseFile
32 {
33 get { return this.databaseFile; }
34 set { this.databaseFile = value; }
35 }
36
37 /// <summary>
38 /// Gets or sets the <see cref="Output"/> for finding source line information.
39 /// </summary>
40 /// <value>The <see cref="Output"/> for finding source line information.</value>
41 public Output Output
42 {
43 get { return this.output; }
44 set { this.output = value; }
45 }
46
47 /// <summary>
48 /// Called at the beginning of the validation of a database file.
49 /// </summary>
50 /// <remarks>
51 /// <para>The <see cref="Validator"/> will set
52 /// <see cref="DatabaseFile"/> before calling InitializeValidator.</para>
53 /// <para><b>Notes to Inheritors:</b> When overriding
54 /// <b>InitializeValidator</b> in a derived class, be sure to call
55 /// the base class's <b>InitializeValidator</b> to thoroughly
56 /// initialize the extension.</para>
57 /// </remarks>
58 public virtual void InitializeValidator()
59 {
60 if (this.databaseFile != null)
61 {
62 this.sourceLineNumbers = new SourceLineNumber(databaseFile);
63 }
64 }
65
66 /// <summary>
67 /// Called at the end of the validation of a database file.
68 /// </summary>
69 /// <remarks>
70 /// <para>The default implementation will nullify source lines.</para>
71 /// <para><b>Notes to Inheritors:</b> When overriding
72 /// <b>FinalizeValidator</b> in a derived class, be sure to call
73 /// the base class's <b>FinalizeValidator</b> to thoroughly
74 /// finalize the extension.</para>
75 /// </remarks>
76 public virtual void FinalizeValidator()
77 {
78 this.sourceLineNumbers = null;
79 }
80
81 /// <summary>
82 /// Logs a message from the <see cref="Validator"/>.
83 /// </summary>
84 /// <param name="message">A <see cref="String"/> of tab-delmited tokens
85 /// in the validation message.</param>
86 public virtual void Log(string message)
87 {
88 this.Log(message, null);
89 }
90
91 /// <summary>
92 /// Logs a message from the <see cref="Validator"/>.
93 /// </summary>
94 /// <param name="message">A <see cref="String"/> of tab-delmited tokens
95 /// in the validation message.</param>
96 /// <param name="action">The name of the action to which the message
97 /// belongs.</param>
98 /// <exception cref="ArgumentNullException">The message cannot be null.
99 /// </exception>
100 /// <exception cref="WixException">The message does not contain four (4)
101 /// or more tab-delimited tokens.</exception>
102 /// <remarks>
103 /// <para><paramref name="message"/> a tab-delimited set of tokens,
104 /// formatted according to Windows Installer guidelines for ICE
105 /// message. The following table lists what each token by index
106 /// should mean.</para>
107 /// <para><paramref name="action"/> a name that represents the ICE
108 /// action that was executed (e.g. 'ICE08').</para>
109 /// <list type="table">
110 /// <listheader>
111 /// <term>Index</term>
112 /// <description>Description</description>
113 /// </listheader>
114 /// <item>
115 /// <term>0</term>
116 /// <description>Name of the ICE.</description>
117 /// </item>
118 /// <item>
119 /// <term>1</term>
120 /// <description>Message type. See the following list.</description>
121 /// </item>
122 /// <item>
123 /// <term>2</term>
124 /// <description>Detailed description.</description>
125 /// </item>
126 /// <item>
127 /// <term>3</term>
128 /// <description>Help URL or location.</description>
129 /// </item>
130 /// <item>
131 /// <term>4</term>
132 /// <description>Table name.</description>
133 /// </item>
134 /// <item>
135 /// <term>5</term>
136 /// <description>Column name.</description>
137 /// </item>
138 /// <item>
139 /// <term>6</term>
140 /// <description>This and remaining fields are primary keys
141 /// to identify a row.</description>
142 /// </item>
143 /// </list>
144 /// <para>The message types are one of the following value.</para>
145 /// <list type="table">
146 /// <listheader>
147 /// <term>Value</term>
148 /// <description>Message Type</description>
149 /// </listheader>
150 /// <item>
151 /// <term>0</term>
152 /// <description>Failure message reporting the failure of the
153 /// ICE custom action.</description>
154 /// </item>
155 /// <item>
156 /// <term>1</term>
157 /// <description>Error message reporting database authoring that
158 /// case incorrect behavior.</description>
159 /// </item>
160 /// <item>
161 /// <term>2</term>
162 /// <description>Warning message reporting database authoring that
163 /// causes incorrect behavior in certain cases. Warnings can also
164 /// report unexpected side-effects of database authoring.
165 /// </description>
166 /// </item>
167 /// <item>
168 /// <term>3</term>
169 /// <description>Informational message.</description>
170 /// </item>
171 /// </list>
172 /// </remarks>
173 public virtual void Log(string message, string action)
174 {
175 if (message == null)
176 {
177 throw new ArgumentNullException("message");
178 }
179
180 string[] messageParts = message.Split('\t');
181 if (3 > messageParts.Length)
182 {
183 if (null == action)
184 {
185 throw new WixException(WixErrors.UnexpectedExternalUIMessage(message));
186 }
187 else
188 {
189 throw new WixException(WixErrors.UnexpectedExternalUIMessage(message, action));
190 }
191 }
192
193 SourceLineNumber messageSourceLineNumbers = null;
194 if (6 < messageParts.Length)
195 {
196 string[] primaryKeys = new string[messageParts.Length - 6];
197
198 Array.Copy(messageParts, 6, primaryKeys, 0, primaryKeys.Length);
199
200 messageSourceLineNumbers = this.GetSourceLineNumbers(messageParts[4], primaryKeys);
201 }
202 else // use the file name as the source line information
203 {
204 messageSourceLineNumbers = this.sourceLineNumbers;
205 }
206
207 switch (messageParts[1])
208 {
209 case "0":
210 case "1":
211 this.OnMessage(WixErrors.ValidationError(messageSourceLineNumbers, messageParts[0], messageParts[2]));
212 break;
213 case "2":
214 this.OnMessage(WixWarnings.ValidationWarning(messageSourceLineNumbers, messageParts[0], messageParts[2]));
215 break;
216 case "3":
217 this.OnMessage(WixVerboses.ValidationInfo(messageParts[0], messageParts[2]));
218 break;
219 default:
220 throw new WixException(WixErrors.InvalidValidatorMessageType(messageParts[1]));
221 }
222 }
223
224 /// <summary>
225 /// Gets the source line information (if available) for a row by its table name and primary key.
226 /// </summary>
227 /// <param name="tableName">The table name of the row.</param>
228 /// <param name="primaryKeys">The primary keys of the row.</param>
229 /// <returns>The source line number information if found; null otherwise.</returns>
230 protected SourceLineNumber GetSourceLineNumbers(string tableName, string[] primaryKeys)
231 {
232 // source line information only exists if an output file was supplied
233 if (null != this.output)
234 {
235 // index the source line information if it hasn't been indexed already
236 if (null == this.indexedSourceLineNumbers)
237 {
238 this.indexedSourceLineNumbers = new Hashtable();
239
240 // index each real table
241 foreach (Table table in this.output.Tables)
242 {
243 // skip unreal tables
244 if (table.Definition.Unreal)
245 {
246 continue;
247 }
248
249 // index each row
250 foreach (Row row in table.Rows)
251 {
252 // skip rows that don't contain source line information
253 if (null == row.SourceLineNumbers)
254 {
255 continue;
256 }
257
258 // index the row using its table name and primary key
259 string primaryKey = row.GetPrimaryKey(';');
260 if (null != primaryKey)
261 {
262 string key = String.Concat(table.Name, ":", primaryKey);
263
264 if (this.indexedSourceLineNumbers.ContainsKey(key))
265 {
266 this.OnMessage(WixWarnings.DuplicatePrimaryKey(row.SourceLineNumbers, primaryKey, table.Name));
267 }
268 else
269 {
270 this.indexedSourceLineNumbers.Add(key, row.SourceLineNumbers);
271 }
272 }
273 }
274 }
275 }
276
277 return (SourceLineNumber)this.indexedSourceLineNumbers[String.Concat(tableName, ":", String.Join(";", primaryKeys))];
278 }
279
280 // use the file name as the source line information
281 return this.sourceLineNumbers;
282 }
283
284 /// <summary>
285 /// Sends a message to the <see cref="Message"/> delegate if there is one.
286 /// </summary>
287 /// <param name="e">Message event arguments.</param>
288 /// <remarks>
289 /// <para><b>Notes to Inheritors:</b> When overriding <b>OnMessage</b>
290 /// in a derived class, be sure to call the base class's
291 /// <b>OnMessage</b> method so that registered delegates recieve
292 /// the event.</para>
293 /// </remarks>
294 public virtual void OnMessage(MessageEventArgs e)
295 {
296 Messaging.Instance.OnMessage(e);
297 }
298 }
299}
diff --git a/src/WixToolset.Core.WindowsInstaller/WindowsInstallerBackendFactory.cs b/src/WixToolset.Core.WindowsInstaller/WindowsInstallerBackendFactory.cs
new file mode 100644
index 00000000..b66a4617
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/WindowsInstallerBackendFactory.cs
@@ -0,0 +1,50 @@
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
4{
5 using System;
6 using System.IO;
7 using WixToolset.Extensibility;
8
9 internal class WindowsInstallerBackendFactory : IBackendFactory
10 {
11 public bool TryCreateBackend(string outputType, string outputFile, IBindContext context, out IBackend backend)
12 {
13 if (String.IsNullOrEmpty(outputType))
14 {
15 outputType = Path.GetExtension(outputFile);
16 }
17
18 switch (outputType.ToLowerInvariant())
19 {
20 case "module":
21 case ".msm":
22 backend = new MsmBackend();
23 return true;
24
25 case "msipackage":
26 case "product":
27 case ".msi":
28 backend = new MsiBackend();
29 return true;
30
31 case "patch":
32 case ".msp":
33 backend = new MspBackend();
34 return true;
35
36 //case "patchcreation":
37 //case ".pcp":
38 // return new PatchCreationBackend();
39
40 case "transform":
41 case ".mst":
42 backend = new MstBackend();
43 return true;
44 }
45
46 backend = null;
47 return false;
48 }
49 }
50}
diff --git a/src/WixToolset.Core.WindowsInstaller/WixToolset.Core.WindowsInstaller.csproj b/src/WixToolset.Core.WindowsInstaller/WixToolset.Core.WindowsInstaller.csproj
new file mode 100644
index 00000000..d74cb1e8
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/WixToolset.Core.WindowsInstaller.csproj
@@ -0,0 +1,36 @@
1<?xml version="1.0" encoding="utf-8"?>
2<!-- 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. -->
3
4<Project Sdk="Microsoft.NET.Sdk">
5 <PropertyGroup>
6 <TargetFramework>netstandard2.0</TargetFramework>
7 <Description>Core Windows Installer</Description>
8 <Title>WiX Toolset Core Windows Installer</Title>
9 </PropertyGroup>
10
11 <PropertyGroup>
12 <NoWarn>NU1701</NoWarn>
13 </PropertyGroup>
14
15 <ItemGroup>
16 <ProjectReference Include="$(WixToolsetRootFolder)\Data\src\WixToolset.Data\WixToolset.Data.csproj" Condition=" '$(Configuration)' == 'Debug' And Exists('$(WixToolsetRootFolder)\Data\src\WixToolset.Data\WixToolset.Data.csproj') " />
17 <PackageReference Include="WixToolset.Data" Version="4.0.*" Condition=" '$(Configuration)' == 'Release' Or !Exists('$(WixToolsetRootFolder)\Data\src\WixToolset.Data\WixToolset.Data.csproj') " />
18
19 <ProjectReference Include="$(WixToolsetRootFolder)\Extensibility\src\WixToolset.Extensibility\WixToolset.Extensibility.csproj" Condition=" '$(Configuration)' == 'Debug' And Exists('$(WixToolsetRootFolder)\Extensibility\src\WixToolset.Extensibility\WixToolset.Extensibility.csproj') " />
20 <PackageReference Include="WixToolset.Extensibility" Version="4.0.*" Condition=" '$(Configuration)' == 'Release' Or !Exists('$(WixToolsetRootFolder)\Extensibility\src\WixToolset.Extensibility\WixToolset.Extensibility.csproj') " />
21
22 <ProjectReference Include="$(WixToolsetRootFolder)\Core.Native\src\WixToolset.Core.Native\WixToolset.Core.Native.csproj" Condition=" '$(Configuration)' == 'Debug' And Exists('$(WixToolsetRootFolder)\Core.Native\src\WixToolset.Core.Native\WixToolset.Core.Native.csproj') " />
23
24 <ProjectReference Include="..\WixToolset.Core\WixToolset.Core.csproj" />
25 <PackageReference Include="WixToolset.Core.Native" Version="4.0.*" Condition=" '$(Configuration)' == 'Release' Or !Exists('$(WixToolsetRootFolder)\Core.Native\src\WixToolset.Core.Native\WixToolset.Core.Native.csproj') " />
26 </ItemGroup>
27
28 <ItemGroup>
29 <PackageReference Include="WixToolset.Dtf.Resources" Version="4.0.*" />
30 <PackageReference Include="WixToolset.Dtf.WindowsInstaller" Version="4.0.*" />
31 </ItemGroup>
32
33 <ItemGroup>
34 <PackageReference Include="Nerdbank.GitVersioning" Version="2.0.41" PrivateAssets="all" />
35 </ItemGroup>
36</Project>