From d3d3649a68cb1fa589fdd987a6690dbd5d671f0d Mon Sep 17 00:00:00 2001 From: Rob Mensching Date: Sun, 17 Sep 2017 15:35:20 -0700 Subject: Initial code commit --- src/WixToolset.Core/Bind/BindBundleCommand.cs | 905 ++++++++++++++ src/WixToolset.Core/Bind/BindDatabaseCommand.cs | 1311 ++++++++++++++++++++ src/WixToolset.Core/Bind/BindTransformCommand.cs | 473 +++++++ .../AutomaticallySlipstreamPatchesCommand.cs | 112 ++ src/WixToolset.Core/Bind/Bundles/BurnCommon.cs | 378 ++++++ src/WixToolset.Core/Bind/Bundles/BurnReader.cs | 210 ++++ src/WixToolset.Core/Bind/Bundles/BurnWriter.cs | 239 ++++ ...CreateBootstrapperApplicationManifestCommand.cs | 241 ++++ .../Bind/Bundles/CreateBurnManifestCommand.cs | 686 ++++++++++ .../Bind/Bundles/CreateContainerCommand.cs | 68 + .../Bind/Bundles/GetPackageFacadesCommand.cs | 62 + .../OrderPackagesAndRollbackBoundariesCommand.cs | 145 +++ src/WixToolset.Core/Bind/Bundles/PackageFacade.cs | 58 + .../Bind/Bundles/ProcessExePackageCommand.cs | 33 + .../Bind/Bundles/ProcessMsiPackageCommand.cs | 560 +++++++++ .../Bind/Bundles/ProcessMspPackageCommand.cs | 189 +++ .../Bind/Bundles/ProcessMsuPackageCommand.cs | 30 + .../Bind/Bundles/ProcessPayloadsCommand.cs | 159 +++ .../Bundles/VerifyPayloadsWithCatalogCommand.cs | 148 +++ .../Bind/Databases/AssignMediaCommand.cs | 314 +++++ .../Bind/Databases/BindSummaryInfoCommand.cs | 135 ++ .../Bind/Databases/CabinetBuilder.cs | 176 +++ .../Bind/Databases/CabinetWorkItem.cs | 78 ++ .../Bind/Databases/ConfigurationCallback.cs | 91 ++ .../Bind/Databases/CopyTransformDataCommand.cs | 606 +++++++++ .../Bind/Databases/CreateCabinetsCommand.cs | 489 ++++++++ .../Bind/Databases/CreateDeltaPatchesCommand.cs | 86 ++ .../Databases/CreateSpecialPropertiesCommand.cs | 68 + .../Databases/ExtractMergeModuleFilesCommand.cs | 225 ++++ src/WixToolset.Core/Bind/Databases/FileFacade.cs | 44 + .../Bind/Databases/GetFileFacadesCommand.cs | 148 +++ .../Bind/Databases/MergeModulesCommand.cs | 350 ++++++ .../Databases/ProcessUncompressedFilesCommand.cs | 115 ++ .../Bind/Databases/UpdateControlTextCommand.cs | 80 ++ .../Bind/Databases/UpdateFileFacadesCommand.cs | 532 ++++++++ src/WixToolset.Core/Bind/DelayedField.cs | 38 + src/WixToolset.Core/Bind/ExtractEmbeddedFiles.cs | 83 ++ .../Bind/ExtractEmbeddedFilesCommand.cs | 53 + src/WixToolset.Core/Bind/FileTransfer.cs | 113 ++ .../Bind/GenerateDatabaseCommand.cs | 335 +++++ .../Bind/ResolveDelayedFieldsCommand.cs | 121 ++ src/WixToolset.Core/Bind/ResolveFieldsCommand.cs | 215 ++++ src/WixToolset.Core/Bind/ResolvedDirectory.cs | 31 + src/WixToolset.Core/Bind/TransferFilesCommand.cs | 214 ++++ 44 files changed, 10747 insertions(+) create mode 100644 src/WixToolset.Core/Bind/BindBundleCommand.cs create mode 100644 src/WixToolset.Core/Bind/BindDatabaseCommand.cs create mode 100644 src/WixToolset.Core/Bind/BindTransformCommand.cs create mode 100644 src/WixToolset.Core/Bind/Bundles/AutomaticallySlipstreamPatchesCommand.cs create mode 100644 src/WixToolset.Core/Bind/Bundles/BurnCommon.cs create mode 100644 src/WixToolset.Core/Bind/Bundles/BurnReader.cs create mode 100644 src/WixToolset.Core/Bind/Bundles/BurnWriter.cs create mode 100644 src/WixToolset.Core/Bind/Bundles/CreateBootstrapperApplicationManifestCommand.cs create mode 100644 src/WixToolset.Core/Bind/Bundles/CreateBurnManifestCommand.cs create mode 100644 src/WixToolset.Core/Bind/Bundles/CreateContainerCommand.cs create mode 100644 src/WixToolset.Core/Bind/Bundles/GetPackageFacadesCommand.cs create mode 100644 src/WixToolset.Core/Bind/Bundles/OrderPackagesAndRollbackBoundariesCommand.cs create mode 100644 src/WixToolset.Core/Bind/Bundles/PackageFacade.cs create mode 100644 src/WixToolset.Core/Bind/Bundles/ProcessExePackageCommand.cs create mode 100644 src/WixToolset.Core/Bind/Bundles/ProcessMsiPackageCommand.cs create mode 100644 src/WixToolset.Core/Bind/Bundles/ProcessMspPackageCommand.cs create mode 100644 src/WixToolset.Core/Bind/Bundles/ProcessMsuPackageCommand.cs create mode 100644 src/WixToolset.Core/Bind/Bundles/ProcessPayloadsCommand.cs create mode 100644 src/WixToolset.Core/Bind/Bundles/VerifyPayloadsWithCatalogCommand.cs create mode 100644 src/WixToolset.Core/Bind/Databases/AssignMediaCommand.cs create mode 100644 src/WixToolset.Core/Bind/Databases/BindSummaryInfoCommand.cs create mode 100644 src/WixToolset.Core/Bind/Databases/CabinetBuilder.cs create mode 100644 src/WixToolset.Core/Bind/Databases/CabinetWorkItem.cs create mode 100644 src/WixToolset.Core/Bind/Databases/ConfigurationCallback.cs create mode 100644 src/WixToolset.Core/Bind/Databases/CopyTransformDataCommand.cs create mode 100644 src/WixToolset.Core/Bind/Databases/CreateCabinetsCommand.cs create mode 100644 src/WixToolset.Core/Bind/Databases/CreateDeltaPatchesCommand.cs create mode 100644 src/WixToolset.Core/Bind/Databases/CreateSpecialPropertiesCommand.cs create mode 100644 src/WixToolset.Core/Bind/Databases/ExtractMergeModuleFilesCommand.cs create mode 100644 src/WixToolset.Core/Bind/Databases/FileFacade.cs create mode 100644 src/WixToolset.Core/Bind/Databases/GetFileFacadesCommand.cs create mode 100644 src/WixToolset.Core/Bind/Databases/MergeModulesCommand.cs create mode 100644 src/WixToolset.Core/Bind/Databases/ProcessUncompressedFilesCommand.cs create mode 100644 src/WixToolset.Core/Bind/Databases/UpdateControlTextCommand.cs create mode 100644 src/WixToolset.Core/Bind/Databases/UpdateFileFacadesCommand.cs create mode 100644 src/WixToolset.Core/Bind/DelayedField.cs create mode 100644 src/WixToolset.Core/Bind/ExtractEmbeddedFiles.cs create mode 100644 src/WixToolset.Core/Bind/ExtractEmbeddedFilesCommand.cs create mode 100644 src/WixToolset.Core/Bind/FileTransfer.cs create mode 100644 src/WixToolset.Core/Bind/GenerateDatabaseCommand.cs create mode 100644 src/WixToolset.Core/Bind/ResolveDelayedFieldsCommand.cs create mode 100644 src/WixToolset.Core/Bind/ResolveFieldsCommand.cs create mode 100644 src/WixToolset.Core/Bind/ResolvedDirectory.cs create mode 100644 src/WixToolset.Core/Bind/TransferFilesCommand.cs (limited to 'src/WixToolset.Core/Bind') diff --git a/src/WixToolset.Core/Bind/BindBundleCommand.cs b/src/WixToolset.Core/Bind/BindBundleCommand.cs new file mode 100644 index 00000000..7ea0c830 --- /dev/null +++ b/src/WixToolset.Core/Bind/BindBundleCommand.cs @@ -0,0 +1,905 @@ +// 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. + +namespace WixToolset.Bind +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Reflection; + using WixToolset.Bind.Bundles; + using WixToolset.Data; + using WixToolset.Data.Rows; + using WixToolset.Extensibility; + + /// + /// Binds a this.bundle. + /// + internal class BindBundleCommand : ICommand + { + public CompressionLevel DefaultCompressionLevel { private get; set; } + + public IEnumerable Extensions { private get; set; } + + public BinderFileManagerCore FileManagerCore { private get; set; } + + public IEnumerable FileManagers { private get; set; } + + public Output Output { private get; set; } + + public string OutputPath { private get; set; } + + public string PdbFile { private get; set; } + + public TableDefinitionCollection TableDefinitions { private get; set; } + + public string TempFilesLocation { private get; set; } + + public WixVariableResolver WixVariableResolver { private get; set; } + + public IEnumerable FileTransfers { get; private set; } + + public IEnumerable ContentFilePaths { get; private set; } + + public void Execute() + { + this.FileTransfers = Enumerable.Empty(); + this.ContentFilePaths = Enumerable.Empty(); + + // First look for data we expect to find... Chain, WixGroups, etc. + + // We shouldn't really get past the linker phase if there are + // no group items... that means that there's no UX, no Chain, + // *and* no Containers! + Table chainPackageTable = this.GetRequiredTable("WixBundlePackage"); + + Table wixGroupTable = this.GetRequiredTable("WixGroup"); + + // Ensure there is one and only one row in the WixBundle table. + // The compiler and linker behavior should have colluded to get + // this behavior. + WixBundleRow bundleRow = (WixBundleRow)this.GetSingleRowTable("WixBundle"); + + bundleRow.PerMachine = true; // default to per-machine but the first-per user package wil flip the bundle per-user. + + // Ensure there is one and only one row in the WixBootstrapperApplication table. + // The compiler and linker behavior should have colluded to get + // this behavior. + Row baRow = this.GetSingleRowTable("WixBootstrapperApplication"); + + // Ensure there is one and only one row in the WixChain table. + // The compiler and linker behavior should have colluded to get + // this behavior. + WixChainRow chainRow = (WixChainRow)this.GetSingleRowTable("WixChain"); + + if (Messaging.Instance.EncounteredError) + { + return; + } + + // Localize fields, resolve wix variables, and resolve file paths. + ExtractEmbeddedFiles filesWithEmbeddedFiles = new ExtractEmbeddedFiles(); + + IEnumerable delayedFields; + { + ResolveFieldsCommand command = new ResolveFieldsCommand(); + command.Tables = this.Output.Tables; + command.FilesWithEmbeddedFiles = filesWithEmbeddedFiles; + command.FileManagerCore = this.FileManagerCore; + command.FileManagers = this.FileManagers; + command.SupportDelayedResolution = true; + command.TempFilesLocation = this.TempFilesLocation; + command.WixVariableResolver = this.WixVariableResolver; + command.Execute(); + + delayedFields = command.DelayedFields; + } + + if (Messaging.Instance.EncounteredError) + { + return; + } + + // If there are any fields to resolve later, create the cache to populate during bind. + IDictionary variableCache = null; + if (delayedFields.Any()) + { + variableCache = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + } + + // TODO: Although the WixSearch tables are defined in the Util extension, + // the Bundle Binder has to know all about them. We hope to revisit all + // of this in the 4.0 timeframe. + IEnumerable orderedSearches = this.OrderSearches(); + + // Extract files that come from cabinet files (this does not extract files from merge modules). + { + ExtractEmbeddedFilesCommand extractEmbeddedFilesCommand = new ExtractEmbeddedFilesCommand(); + extractEmbeddedFilesCommand.FilesWithEmbeddedFiles = filesWithEmbeddedFiles; + extractEmbeddedFilesCommand.Execute(); + } + + // Get the explicit payloads. + RowDictionary payloads = new RowDictionary(this.Output.Tables["WixBundlePayload"]); + + // Update explicitly authored payloads with their parent package and container (as appropriate) + // to make it easier to gather the payloads later. + foreach (WixGroupRow row in wixGroupTable.RowsAs()) + { + if (ComplexReferenceChildType.Payload == row.ChildType) + { + WixBundlePayloadRow payload = payloads.Get(row.ChildId); + + if (ComplexReferenceParentType.Package == row.ParentType) + { + Debug.Assert(String.IsNullOrEmpty(payload.Package)); + payload.Package = row.ParentId; + } + else if (ComplexReferenceParentType.Container == row.ParentType) + { + Debug.Assert(String.IsNullOrEmpty(payload.Container)); + payload.Container = row.ParentId; + } + else if (ComplexReferenceParentType.Layout == row.ParentType) + { + payload.LayoutOnly = true; + } + } + } + + List fileTransfers = new List(); + string layoutDirectory = Path.GetDirectoryName(this.OutputPath); + + // Process the explicitly authored payloads. + ISet processedPayloads; + { + ProcessPayloadsCommand command = new ProcessPayloadsCommand(); + command.Payloads = payloads.Values; + command.DefaultPackaging = bundleRow.DefaultPackagingType; + command.LayoutDirectory = layoutDirectory; + command.Execute(); + + fileTransfers.AddRange(command.FileTransfers); + + processedPayloads = new HashSet(payloads.Keys); + } + + IDictionary facades; + { + GetPackageFacadesCommand command = new GetPackageFacadesCommand(); + command.PackageTable = chainPackageTable; + command.ExePackageTable = this.Output.Tables["WixBundleExePackage"]; + command.MsiPackageTable = this.Output.Tables["WixBundleMsiPackage"]; + command.MspPackageTable = this.Output.Tables["WixBundleMspPackage"]; + command.MsuPackageTable = this.Output.Tables["WixBundleMsuPackage"]; + command.Execute(); + + facades = command.PackageFacades; + } + + // Process each package facade. Note this is likely to add payloads and other rows to tables so + // note that any indexes created above may be out of date now. + foreach (PackageFacade facade in facades.Values) + { + switch (facade.Package.Type) + { + case WixBundlePackageType.Exe: + { + ProcessExePackageCommand command = new ProcessExePackageCommand(); + command.AuthoredPayloads = payloads; + command.Facade = facade; + command.Execute(); + + // ? variableCache.Add(String.Concat("packageManufacturer.", facade.Package.WixChainItemId), facade.ExePackage.Manufacturer); + } + break; + + case WixBundlePackageType.Msi: + { + ProcessMsiPackageCommand command = new ProcessMsiPackageCommand(); + command.AuthoredPayloads = payloads; + command.Facade = facade; + command.FileManager = this.FileManagers.First(); + command.MsiFeatureTable = this.Output.EnsureTable(this.TableDefinitions["WixBundleMsiFeature"]); + command.MsiPropertyTable = this.Output.EnsureTable(this.TableDefinitions["WixBundleMsiProperty"]); + command.PayloadTable = this.Output.Tables["WixBundlePayload"]; + command.RelatedPackageTable = this.Output.EnsureTable(this.TableDefinitions["WixBundleRelatedPackage"]); + command.Execute(); + + if (null != variableCache) + { + variableCache.Add(String.Concat("packageLanguage.", facade.Package.WixChainItemId), facade.MsiPackage.ProductLanguage.ToString()); + + if (null != facade.MsiPackage.Manufacturer) + { + variableCache.Add(String.Concat("packageManufacturer.", facade.Package.WixChainItemId), facade.MsiPackage.Manufacturer); + } + } + + } + break; + + case WixBundlePackageType.Msp: + { + ProcessMspPackageCommand command = new ProcessMspPackageCommand(); + command.AuthoredPayloads = payloads; + command.Facade = facade; + command.WixBundlePatchTargetCodeTable = this.Output.EnsureTable(this.TableDefinitions["WixBundlePatchTargetCode"]); + command.Execute(); + } + break; + + case WixBundlePackageType.Msu: + { + ProcessMsuPackageCommand command = new ProcessMsuPackageCommand(); + command.Facade = facade; + command.Execute(); + } + break; + } + + if (null != variableCache) + { + BindBundleCommand.PopulatePackageVariableCache(facade.Package, variableCache); + } + } + + // Reindex the payloads now that all the payloads (minus the manifest payloads that will be created later) + // are present. + payloads = new RowDictionary(this.Output.Tables["WixBundlePayload"]); + + // Process the payloads that were added by processing the packages. + { + ProcessPayloadsCommand command = new ProcessPayloadsCommand(); + command.Payloads = payloads.Values.Where(r => !processedPayloads.Contains(r.Id)).ToList(); + command.DefaultPackaging = bundleRow.DefaultPackagingType; + command.LayoutDirectory = layoutDirectory; + command.Execute(); + + fileTransfers.AddRange(command.FileTransfers); + + processedPayloads = null; + } + + // Set the package metadata from the payloads now that we have the complete payload information. + ILookup payloadsByPackage = payloads.Values.ToLookup(p => p.Package); + + { + foreach (PackageFacade facade in facades.Values) + { + facade.Package.Size = 0; + + IEnumerable packagePayloads = payloadsByPackage[facade.Package.WixChainItemId]; + + foreach (WixBundlePayloadRow payload in packagePayloads) + { + facade.Package.Size += payload.FileSize; + } + + if (!facade.Package.InstallSize.HasValue) + { + facade.Package.InstallSize = facade.Package.Size; + + } + + WixBundlePayloadRow packagePayload = payloads[facade.Package.PackagePayload]; + + if (String.IsNullOrEmpty(facade.Package.Description)) + { + facade.Package.Description = packagePayload.Description; + } + + if (String.IsNullOrEmpty(facade.Package.DisplayName)) + { + facade.Package.DisplayName = packagePayload.DisplayName; + } + } + } + + + // Give the UX payloads their embedded IDs... + int uxPayloadIndex = 0; + { + foreach (WixBundlePayloadRow payload in payloads.Values.Where(p => Compiler.BurnUXContainerId == p.Container)) + { + // In theory, UX payloads could be embedded in the UX CAB, external to the bundle EXE, or even + // downloaded. The current engine requires the UX to be fully present before any downloading starts, + // so that rules out downloading. Also, the burn engine does not currently copy external UX payloads + // into the temporary UX directory correctly, so we don't allow external either. + if (PackagingType.Embedded != payload.Packaging) + { + Messaging.Instance.OnMessage(WixWarnings.UxPayloadsOnlySupportEmbedding(payload.SourceLineNumbers, payload.FullFileName)); + payload.Packaging = PackagingType.Embedded; + } + + payload.EmbeddedId = String.Format(CultureInfo.InvariantCulture, BurnCommon.BurnUXContainerEmbeddedIdFormat, uxPayloadIndex); + ++uxPayloadIndex; + } + + if (0 == uxPayloadIndex) + { + // If we didn't get any UX payloads, it's an error! + throw new WixException(WixErrors.MissingBundleInformation("BootstrapperApplication")); + } + + // Give the embedded payloads without an embedded id yet an embedded id. + int payloadIndex = 0; + foreach (WixBundlePayloadRow payload in payloads.Values) + { + Debug.Assert(PackagingType.Unknown != payload.Packaging); + + if (PackagingType.Embedded == payload.Packaging && String.IsNullOrEmpty(payload.EmbeddedId)) + { + payload.EmbeddedId = String.Format(CultureInfo.InvariantCulture, BurnCommon.BurnAttachedContainerEmbeddedIdFormat, payloadIndex); + ++payloadIndex; + } + } + } + + // Determine patches to automatically slipstream. + { + AutomaticallySlipstreamPatchesCommand command = new AutomaticallySlipstreamPatchesCommand(); + command.PackageFacades = facades.Values; + command.SlipstreamMspTable = this.Output.EnsureTable(this.TableDefinitions["WixBundleSlipstreamMsp"]); + command.WixBundlePatchTargetCodeTable = this.Output.EnsureTable(this.TableDefinitions["WixBundlePatchTargetCode"]); + command.Execute(); + } + + // If catalog files exist, non-embedded payloads should validate with the catalogs. + IEnumerable catalogs = this.Output.Tables["WixBundleCatalog"].RowsAs(); + + if (catalogs.Any()) + { + VerifyPayloadsWithCatalogCommand command = new VerifyPayloadsWithCatalogCommand(); + command.Catalogs = catalogs; + command.Payloads = payloads.Values; + command.Execute(); + } + + if (Messaging.Instance.EncounteredError) + { + return; + } + + IEnumerable orderedFacades; + IEnumerable boundaries; + { + OrderPackagesAndRollbackBoundariesCommand command = new OrderPackagesAndRollbackBoundariesCommand(); + command.Boundaries = new RowDictionary(this.Output.Tables["WixBundleRollbackBoundary"]); + command.PackageFacades = facades; + command.WixGroupTable = wixGroupTable; + command.Execute(); + + orderedFacades = command.OrderedPackageFacades; + boundaries = command.UsedRollbackBoundaries; + } + + // Resolve any delayed fields before generating the manifest. + if (delayedFields.Any()) + { + ResolveDelayedFieldsCommand resolveDelayedFieldsCommand = new ResolveDelayedFieldsCommand(); + resolveDelayedFieldsCommand.OutputType = this.Output.Type; + resolveDelayedFieldsCommand.DelayedFields = delayedFields; + resolveDelayedFieldsCommand.ModularizationGuid = null; + resolveDelayedFieldsCommand.VariableCache = variableCache; + resolveDelayedFieldsCommand.Execute(); + } + + // Set the overridable bundle provider key. + this.SetBundleProviderKey(this.Output, bundleRow); + + // Import or generate dependency providers for packages in the manifest. + this.ProcessDependencyProviders(this.Output, facades); + + // Update the bundle per-machine/per-user scope based on the chained packages. + this.ResolveBundleInstallScope(bundleRow, orderedFacades); + + // Generate the core-defined BA manifest tables... + { + CreateBootstrapperApplicationManifestCommand command = new CreateBootstrapperApplicationManifestCommand(); + command.BundleRow = bundleRow; + command.ChainPackages = orderedFacades; + command.LastUXPayloadIndex = uxPayloadIndex; + command.MsiFeatures = this.Output.Tables["WixBundleMsiFeature"].RowsAs(); + command.Output = this.Output; + command.Payloads = payloads; + command.TableDefinitions = this.TableDefinitions; + command.TempFilesLocation = this.TempFilesLocation; + command.Execute(); + + WixBundlePayloadRow baManifestPayload = command.BootstrapperApplicationManifestPayloadRow; + payloads.Add(baManifestPayload); + } + + foreach (BinderExtension extension in this.Extensions) + { + extension.Finish(Output); + } + + // Create all the containers except the UX container first so the manifest (that goes in the UX container) + // can contain all size and hash information about the non-UX containers. + RowDictionary containers = new RowDictionary(this.Output.Tables["WixBundleContainer"]); + + ILookup payloadsByContainer = payloads.Values.ToLookup(p => p.Container); + + int attachedContainerIndex = 1; // count starts at one because UX container is "0". + + IEnumerable uxContainerPayloads = Enumerable.Empty(); + + foreach (WixBundleContainerRow container in containers.Values) + { + IEnumerable containerPayloads = payloadsByContainer[container.Id]; + + if (!containerPayloads.Any()) + { + if (container.Id != Compiler.BurnDefaultAttachedContainerId) + { + // TODO: display warning that we're ignoring container that ended up with no paylods in it. + } + } + else if (Compiler.BurnUXContainerId == container.Id) + { + container.WorkingPath = Path.Combine(this.TempFilesLocation, container.Name); + container.AttachedContainerIndex = 0; + + // Gather the list of UX payloads but ensure the BootstrapperApplication Payload is the first + // in the list since that is the Payload that Burn attempts to load. + List uxPayloads = new List(); + + string baPayloadId = baRow.FieldAsString(0); + + foreach (WixBundlePayloadRow uxPayload in containerPayloads) + { + if (uxPayload.Id == baPayloadId) + { + uxPayloads.Insert(0, uxPayload); + } + else + { + uxPayloads.Add(uxPayload); + } + } + + uxContainerPayloads = uxPayloads; + } + else + { + container.WorkingPath = Path.Combine(this.TempFilesLocation, container.Name); + + // Add detached containers to the list of file transfers. + if (ContainerType.Detached == container.Type) + { + FileTransfer transfer; + if (FileTransfer.TryCreate(container.WorkingPath, Path.Combine(layoutDirectory, container.Name), true, "Container", container.SourceLineNumbers, out transfer)) + { + transfer.Built = true; + fileTransfers.Add(transfer); + } + } + else // update the attached container index. + { + Debug.Assert(ContainerType.Attached == container.Type); + + container.AttachedContainerIndex = attachedContainerIndex; + ++attachedContainerIndex; + } + + this.CreateContainer(container, containerPayloads, null); + } + } + + // Create the bundle manifest then UX container. + string manifestPath = Path.Combine(this.TempFilesLocation, "bundle-manifest.xml"); + { + CreateBurnManifestCommand command = new CreateBurnManifestCommand(); + command.FileManagers = this.FileManagers; + command.Output = this.Output; + + command.BundleInfo = bundleRow; + command.Chain = chainRow; + command.Containers = containers; + command.Catalogs = catalogs; + command.ExecutableName = Path.GetFileName(this.OutputPath); + command.OrderedPackages = orderedFacades; + command.OutputPath = manifestPath; + command.RollbackBoundaries = boundaries; + command.OrderedSearches = orderedSearches; + command.Payloads = payloads; + command.UXContainerPayloads = uxContainerPayloads; + command.Execute(); + } + + WixBundleContainerRow uxContainer = containers[Compiler.BurnUXContainerId]; + this.CreateContainer(uxContainer, uxContainerPayloads, manifestPath); + + // Copy the burn.exe to a writable location then mark it to be moved to its final build location. Note + // that today, the x64 Burn uses the x86 stub. + string stubPlatform = (Platform.X64 == bundleRow.Platform) ? "x86" : bundleRow.Platform.ToString(); + + string stubFile = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), stubPlatform, "burn.exe"); + string bundleTempPath = Path.Combine(this.TempFilesLocation, Path.GetFileName(this.OutputPath)); + + Messaging.Instance.OnMessage(WixVerboses.GeneratingBundle(bundleTempPath, stubFile)); + + string bundleFilename = Path.GetFileName(this.OutputPath); + if ("setup.exe".Equals(bundleFilename, StringComparison.OrdinalIgnoreCase)) + { + Messaging.Instance.OnMessage(WixErrors.InsecureBundleFilename(bundleFilename)); + } + + FileTransfer bundleTransfer; + if (FileTransfer.TryCreate(bundleTempPath, this.OutputPath, true, "Bundle", bundleRow.SourceLineNumbers, out bundleTransfer)) + { + bundleTransfer.Built = true; + fileTransfers.Add(bundleTransfer); + } + + File.Copy(stubFile, bundleTempPath, true); + File.SetAttributes(bundleTempPath, FileAttributes.Normal); + + this.UpdateBurnResources(bundleTempPath, this.OutputPath, bundleRow); + + // Update the .wixburn section to point to at the UX and attached container(s) then attach the containers + // if they should be attached. + using (BurnWriter writer = BurnWriter.Open(bundleTempPath)) + { + FileInfo burnStubFile = new FileInfo(bundleTempPath); + writer.InitializeBundleSectionData(burnStubFile.Length, bundleRow.BundleId); + + // Always attach the UX container first + writer.AppendContainer(uxContainer.WorkingPath, BurnWriter.Container.UX); + + // Now append all other attached containers + foreach (WixBundleContainerRow container in containers.Values) + { + if (ContainerType.Attached == container.Type) + { + // The container was only created if it had payloads. + if (!String.IsNullOrEmpty(container.WorkingPath) && Compiler.BurnUXContainerId != container.Id) + { + writer.AppendContainer(container.WorkingPath, BurnWriter.Container.Attached); + } + } + } + } + + if (null != this.PdbFile) + { + Pdb pdb = new Pdb(); + pdb.Output = Output; + pdb.Save(this.PdbFile); + } + + this.FileTransfers = fileTransfers; + this.ContentFilePaths = payloads.Values.Where(p => p.ContentFile).Select(p => p.FullFileName).ToList(); + } + + private Table GetRequiredTable(string tableName) + { + Table table = this.Output.Tables[tableName]; + if (null == table || 0 == table.Rows.Count) + { + throw new WixException(WixErrors.MissingBundleInformation(tableName)); + } + + return table; + } + + private Row GetSingleRowTable(string tableName) + { + Table table = this.Output.Tables[tableName]; + if (null == table || 1 != table.Rows.Count) + { + throw new WixException(WixErrors.MissingBundleInformation(tableName)); + } + + return table.Rows[0]; + } + + private List OrderSearches() + { + Dictionary allSearches = new Dictionary(); + Table wixFileSearchTable = this.Output.Tables["WixFileSearch"]; + if (null != wixFileSearchTable && 0 < wixFileSearchTable.Rows.Count) + { + foreach (Row row in wixFileSearchTable.Rows) + { + WixFileSearchInfo fileSearchInfo = new WixFileSearchInfo(row); + allSearches.Add(fileSearchInfo.Id, fileSearchInfo); + } + } + + Table wixRegistrySearchTable = this.Output.Tables["WixRegistrySearch"]; + if (null != wixRegistrySearchTable && 0 < wixRegistrySearchTable.Rows.Count) + { + foreach (Row row in wixRegistrySearchTable.Rows) + { + WixRegistrySearchInfo registrySearchInfo = new WixRegistrySearchInfo(row); + allSearches.Add(registrySearchInfo.Id, registrySearchInfo); + } + } + + Table wixComponentSearchTable = this.Output.Tables["WixComponentSearch"]; + if (null != wixComponentSearchTable && 0 < wixComponentSearchTable.Rows.Count) + { + foreach (Row row in wixComponentSearchTable.Rows) + { + WixComponentSearchInfo componentSearchInfo = new WixComponentSearchInfo(row); + allSearches.Add(componentSearchInfo.Id, componentSearchInfo); + } + } + + Table wixProductSearchTable = this.Output.Tables["WixProductSearch"]; + if (null != wixProductSearchTable && 0 < wixProductSearchTable.Rows.Count) + { + foreach (Row row in wixProductSearchTable.Rows) + { + WixProductSearchInfo productSearchInfo = new WixProductSearchInfo(row); + allSearches.Add(productSearchInfo.Id, productSearchInfo); + } + } + + // Merge in the variable/condition info and get the canonical ordering for + // the searches. + List orderedSearches = new List(); + Table wixSearchTable = this.Output.Tables["WixSearch"]; + if (null != wixSearchTable && 0 < wixSearchTable.Rows.Count) + { + orderedSearches.Capacity = wixSearchTable.Rows.Count; + foreach (Row row in wixSearchTable.Rows) + { + WixSearchInfo searchInfo = allSearches[(string)row[0]]; + searchInfo.AddWixSearchRowInfo(row); + orderedSearches.Add(searchInfo); + } + } + + return orderedSearches; + } + + /// + /// Populates the variable cache with specific package properties. + /// + /// The package with properties to cache. + /// The property cache. + private static void PopulatePackageVariableCache(WixBundlePackageRow package, IDictionary variableCache) + { + string id = package.WixChainItemId; + + variableCache.Add(String.Concat("packageDescription.", id), package.Description); + //variableCache.Add(String.Concat("packageLanguage.", id), package.Language); + //variableCache.Add(String.Concat("packageManufacturer.", id), package.Manufacturer); + variableCache.Add(String.Concat("packageName.", id), package.DisplayName); + variableCache.Add(String.Concat("packageVersion.", id), package.Version); + } + + private void CreateContainer(WixBundleContainerRow container, IEnumerable containerPayloads, string manifestFile) + { + CreateContainerCommand command = new CreateContainerCommand(); + command.DefaultCompressionLevel = this.DefaultCompressionLevel; + command.Payloads = containerPayloads; + command.ManifestFile = manifestFile; + command.OutputPath = container.WorkingPath; + command.Execute(); + + container.Hash = command.Hash; + container.Size = command.Size; + } + + private void ResolveBundleInstallScope(WixBundleRow bundleInfo, IEnumerable facades) + { + foreach (PackageFacade facade in facades) + { + if (bundleInfo.PerMachine && YesNoDefaultType.No == facade.Package.PerMachine) + { + Messaging.Instance.OnMessage(WixVerboses.SwitchingToPerUserPackage(facade.Package.SourceLineNumbers, facade.Package.WixChainItemId)); + + bundleInfo.PerMachine = false; + break; + } + } + + foreach (PackageFacade facade in facades) + { + // Update package scope from bundle scope if default. + if (YesNoDefaultType.Default == facade.Package.PerMachine) + { + facade.Package.PerMachine = bundleInfo.PerMachine ? YesNoDefaultType.Yes : YesNoDefaultType.No; + } + + // We will only register packages in the same scope as the bundle. Warn if any packages with providers + // are in a different scope and not permanent (permanents typically don't need a ref-count). + if (!bundleInfo.PerMachine && YesNoDefaultType.Yes == facade.Package.PerMachine && !facade.Package.Permanent && 0 < facade.Provides.Count) + { + Messaging.Instance.OnMessage(WixWarnings.NoPerMachineDependencies(facade.Package.SourceLineNumbers, facade.Package.WixChainItemId)); + } + } + } + + private void UpdateBurnResources(string bundleTempPath, string outputPath, WixBundleRow bundleInfo) + { + WixToolset.Dtf.Resources.ResourceCollection resources = new WixToolset.Dtf.Resources.ResourceCollection(); + WixToolset.Dtf.Resources.VersionResource version = new WixToolset.Dtf.Resources.VersionResource("#1", 1033); + + version.Load(bundleTempPath); + resources.Add(version); + + // Ensure the bundle info provides a full four part version. + Version fourPartVersion = new Version(bundleInfo.Version); + int major = (fourPartVersion.Major < 0) ? 0 : fourPartVersion.Major; + int minor = (fourPartVersion.Minor < 0) ? 0 : fourPartVersion.Minor; + int build = (fourPartVersion.Build < 0) ? 0 : fourPartVersion.Build; + int revision = (fourPartVersion.Revision < 0) ? 0 : fourPartVersion.Revision; + + if (UInt16.MaxValue < major || UInt16.MaxValue < minor || UInt16.MaxValue < build || UInt16.MaxValue < revision) + { + throw new WixException(WixErrors.InvalidModuleOrBundleVersion(bundleInfo.SourceLineNumbers, "Bundle", bundleInfo.Version)); + } + + fourPartVersion = new Version(major, minor, build, revision); + version.FileVersion = fourPartVersion; + version.ProductVersion = fourPartVersion; + + WixToolset.Dtf.Resources.VersionStringTable strings = version[1033]; + strings["LegalCopyright"] = bundleInfo.Copyright; + strings["OriginalFilename"] = Path.GetFileName(outputPath); + strings["FileVersion"] = bundleInfo.Version; // string versions do not have to be four parts. + strings["ProductVersion"] = bundleInfo.Version; // string versions do not have to be four parts. + + if (!String.IsNullOrEmpty(bundleInfo.Name)) + { + strings["ProductName"] = bundleInfo.Name; + strings["FileDescription"] = bundleInfo.Name; + } + + if (!String.IsNullOrEmpty(bundleInfo.Publisher)) + { + strings["CompanyName"] = bundleInfo.Publisher; + } + else + { + strings["CompanyName"] = String.Empty; + } + + if (!String.IsNullOrEmpty(bundleInfo.IconPath)) + { + Dtf.Resources.GroupIconResource iconGroup = new Dtf.Resources.GroupIconResource("#1", 1033); + iconGroup.ReadFromFile(bundleInfo.IconPath); + resources.Add(iconGroup); + + foreach (Dtf.Resources.Resource icon in iconGroup.Icons) + { + resources.Add(icon); + } + } + + if (!String.IsNullOrEmpty(bundleInfo.SplashScreenBitmapPath)) + { + Dtf.Resources.BitmapResource bitmap = new Dtf.Resources.BitmapResource("#1", 1033); + bitmap.ReadFromFile(bundleInfo.SplashScreenBitmapPath); + resources.Add(bitmap); + } + + resources.Save(bundleTempPath); + } + + #region DependencyExtension + /// + /// Imports authored dependency providers for each package in the manifest, + /// and generates dependency providers for certain package types that do not + /// have a provider defined. + /// + /// The object for the bundle. + /// An indexed collection of chained packages. + private void ProcessDependencyProviders(Output bundle, IDictionary facades) + { + // First import any authored dependencies. These may merge with imported provides from MSI packages. + Table wixDependencyProviderTable = bundle.Tables["WixDependencyProvider"]; + if (null != wixDependencyProviderTable && 0 < wixDependencyProviderTable.Rows.Count) + { + // Add package information for each dependency provider authored into the manifest. + foreach (Row wixDependencyProviderRow in wixDependencyProviderTable.Rows) + { + string packageId = (string)wixDependencyProviderRow[1]; + + PackageFacade facade = null; + if (facades.TryGetValue(packageId, out facade)) + { + ProvidesDependency dependency = new ProvidesDependency(wixDependencyProviderRow); + + if (String.IsNullOrEmpty(dependency.Key)) + { + switch (facade.Package.Type) + { + // The WixDependencyExtension allows an empty Key for MSIs and MSPs. + case WixBundlePackageType.Msi: + dependency.Key = facade.MsiPackage.ProductCode; + break; + case WixBundlePackageType.Msp: + dependency.Key = facade.MspPackage.PatchCode; + break; + } + } + + if (String.IsNullOrEmpty(dependency.Version)) + { + dependency.Version = facade.Package.Version; + } + + // If the version is still missing, a version could not be harvested from the package and was not authored. + if (String.IsNullOrEmpty(dependency.Version)) + { + Messaging.Instance.OnMessage(WixErrors.MissingDependencyVersion(facade.Package.WixChainItemId)); + } + + if (String.IsNullOrEmpty(dependency.DisplayName)) + { + dependency.DisplayName = facade.Package.DisplayName; + } + + if (!facade.Provides.Merge(dependency)) + { + Messaging.Instance.OnMessage(WixErrors.DuplicateProviderDependencyKey(dependency.Key, facade.Package.WixChainItemId)); + } + } + } + } + + // Generate providers for MSI packages that still do not have providers. + foreach (PackageFacade facade in facades.Values) + { + string key = null; + + if (WixBundlePackageType.Msi == facade.Package.Type && 0 == facade.Provides.Count) + { + key = facade.MsiPackage.ProductCode; + } + else if (WixBundlePackageType.Msp == facade.Package.Type && 0 == facade.Provides.Count) + { + key = facade.MspPackage.PatchCode; + } + + if (!String.IsNullOrEmpty(key)) + { + ProvidesDependency dependency = new ProvidesDependency(key, facade.Package.Version, facade.Package.DisplayName, 0); + + if (!facade.Provides.Merge(dependency)) + { + Messaging.Instance.OnMessage(WixErrors.DuplicateProviderDependencyKey(dependency.Key, facade.Package.WixChainItemId)); + } + } + } + } + + /// + /// Sets the provider key for the bundle. + /// + /// The object for the bundle. + /// The containing the provider key and other information for the bundle. + private void SetBundleProviderKey(Output bundle, WixBundleRow bundleInfo) + { + // From DependencyCommon.cs in the WixDependencyExtension. + const int ProvidesAttributesBundle = 0x10000; + + Table wixDependencyProviderTable = bundle.Tables["WixDependencyProvider"]; + if (null != wixDependencyProviderTable && 0 < wixDependencyProviderTable.Rows.Count) + { + // Search the WixDependencyProvider table for the single bundle provider key. + foreach (Row wixDependencyProviderRow in wixDependencyProviderTable.Rows) + { + object attributes = wixDependencyProviderRow[5]; + if (null != attributes && 0 != (ProvidesAttributesBundle & (int)attributes)) + { + bundleInfo.ProviderKey = (string)wixDependencyProviderRow[2]; + break; + } + } + } + + // Defaults to the bundle ID as the provider key. + } + #endregion + } +} diff --git a/src/WixToolset.Core/Bind/BindDatabaseCommand.cs b/src/WixToolset.Core/Bind/BindDatabaseCommand.cs new file mode 100644 index 00000000..93af2e9a --- /dev/null +++ b/src/WixToolset.Core/Bind/BindDatabaseCommand.cs @@ -0,0 +1,1311 @@ +// 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. + +namespace WixToolset.Bind +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Diagnostics; + using System.Globalization; + using System.IO; + using System.Linq; + using WixToolset.Bind.Databases; + using WixToolset.Data; + using WixToolset.Data.Rows; + using WixToolset.Extensibility; + using WixToolset.Msi; + + /// + /// Binds a databse. + /// + internal class BindDatabaseCommand : ICommand + { + // As outlined in RFC 4122, this is our namespace for generating name-based (version 3) UUIDs. + private static readonly Guid WixComponentGuidNamespace = new Guid("{3064E5C6-FB63-4FE9-AC49-E446A792EFA5}"); + + public int Codepage { private get; set; } + + public int CabbingThreadCount { private get; set; } + + public CompressionLevel DefaultCompressionLevel { private get; set; } + + public bool DeltaBinaryPatch { get; set; } + + public IEnumerable Extensions { private get; set; } + + public BinderFileManagerCore FileManagerCore { private get; set; } + + public IEnumerable FileManagers { private get; set; } + + public IEnumerable InspectorExtensions { private get; set; } + + public Localizer Localizer { private get; set; } + + public string PdbFile { private get; set; } + + public Output Output { private get; set; } + + public string OutputPath { private get; set; } + + public bool SuppressAddingValidationRows { private get; set; } + + public bool SuppressLayout { private get; set; } + + public TableDefinitionCollection TableDefinitions { private get; set; } + + public string TempFilesLocation { private get; set; } + + public Validator Validator { private get; set; } + + public WixVariableResolver WixVariableResolver { private get; set; } + + public IEnumerable FileTransfers { get; private set; } + + public IEnumerable ContentFilePaths { get; private set; } + + public void Execute() + { + List fileTransfers = new List(); + + HashSet suppressedTableNames = new HashSet(); + + // Localize fields, resolve wix variables, and resolve file paths. + ExtractEmbeddedFiles filesWithEmbeddedFiles = new ExtractEmbeddedFiles(); + + IEnumerable delayedFields; + { + ResolveFieldsCommand command = new ResolveFieldsCommand(); + command.Tables = this.Output.Tables; + command.FilesWithEmbeddedFiles = filesWithEmbeddedFiles; + command.FileManagerCore = this.FileManagerCore; + command.FileManagers = this.FileManagers; + command.SupportDelayedResolution = true; + command.TempFilesLocation = this.TempFilesLocation; + command.WixVariableResolver = this.WixVariableResolver; + command.Execute(); + + delayedFields = command.DelayedFields; + } + + if (OutputType.Patch == this.Output.Type) + { + foreach (SubStorage transform in this.Output.SubStorages) + { + ResolveFieldsCommand command = new ResolveFieldsCommand(); + command.Tables = transform.Data.Tables; + command.FilesWithEmbeddedFiles = filesWithEmbeddedFiles; + command.FileManagerCore = this.FileManagerCore; + command.FileManagers = this.FileManagers; + command.SupportDelayedResolution = false; + command.TempFilesLocation = this.TempFilesLocation; + command.WixVariableResolver = this.WixVariableResolver; + command.Execute(); + } + } + + // If there are any fields to resolve later, create the cache to populate during bind. + IDictionary variableCache = null; + if (delayedFields.Any()) + { + variableCache = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + } + + this.LocalizeUI(this.Output.Tables); + + // Process the summary information table before the other tables. + bool compressed; + bool longNames; + int installerVersion; + string modularizationGuid; + { + BindSummaryInfoCommand command = new BindSummaryInfoCommand(); + command.Output = this.Output; + command.Execute(); + + compressed = command.Compressed; + longNames = command.LongNames; + installerVersion = command.InstallerVersion; + modularizationGuid = command.ModularizationGuid; + } + + // Stop processing if an error previously occurred. + if (Messaging.Instance.EncounteredError) + { + return; + } + + // Modularize identifiers and add tables with real streams to the import tables. + if (OutputType.Module == this.Output.Type) + { + // Gather all the suppress modularization identifiers + HashSet suppressModularizationIdentifiers = null; + Table wixSuppressModularizationTable = this.Output.Tables["WixSuppressModularization"]; + if (null != wixSuppressModularizationTable) + { + suppressModularizationIdentifiers = new HashSet(wixSuppressModularizationTable.Rows.Select(row => (string)row[0])); + } + + foreach (Table table in this.Output.Tables) + { + table.Modularize(modularizationGuid, suppressModularizationIdentifiers); + } + } + + // This must occur after all variables and source paths have been resolved and after modularization. + List fileFacades; + { + GetFileFacadesCommand command = new GetFileFacadesCommand(); + command.FileTable = this.Output.Tables["File"]; + command.WixFileTable = this.Output.Tables["WixFile"]; + command.WixDeltaPatchFileTable = this.Output.Tables["WixDeltaPatchFile"]; + command.WixDeltaPatchSymbolPathsTable = this.Output.Tables["WixDeltaPatchSymbolPaths"]; + command.Execute(); + + fileFacades = command.FileFacades; + } + + ////if (OutputType.Patch == this.Output.Type) + ////{ + //// foreach (SubStorage substorage in this.Output.SubStorages) + //// { + //// Output transform = substorage.Data; + + //// ResolveFieldsCommand command = new ResolveFieldsCommand(); + //// command.Tables = transform.Tables; + //// command.FilesWithEmbeddedFiles = filesWithEmbeddedFiles; + //// command.FileManagerCore = this.FileManagerCore; + //// command.FileManagers = this.FileManagers; + //// command.SupportDelayedResolution = false; + //// command.TempFilesLocation = this.TempFilesLocation; + //// command.WixVariableResolver = this.WixVariableResolver; + //// command.Execute(); + + //// this.MergeUnrealTables(transform.Tables); + //// } + ////} + + { + CreateSpecialPropertiesCommand command = new CreateSpecialPropertiesCommand(); + command.PropertyTable = this.Output.Tables["Property"]; + command.WixPropertyTable = this.Output.Tables["WixProperty"]; + command.Execute(); + } + + if (Messaging.Instance.EncounteredError) + { + return; + } + + // Add binder variables for all properties. + Table propertyTable = this.Output.Tables["Property"]; + if (null != propertyTable) + { + foreach (PropertyRow propertyRow in propertyTable.Rows) + { + // Set the ProductCode if it is to be generated. + if (OutputType.Product == this.Output.Type && "ProductCode".Equals(propertyRow.Property, StringComparison.Ordinal) && "*".Equals(propertyRow.Value, StringComparison.Ordinal)) + { + propertyRow.Value = Common.GenerateGuid(); + + // Update the target ProductCode in any instance transforms. + foreach (SubStorage subStorage in this.Output.SubStorages) + { + Output subStorageOutput = subStorage.Data; + if (OutputType.Transform != subStorageOutput.Type) + { + continue; + } + + Table instanceSummaryInformationTable = subStorageOutput.Tables["_SummaryInformation"]; + foreach (Row row in instanceSummaryInformationTable.Rows) + { + if ((int)SummaryInformation.Transform.ProductCodes == row.FieldAsInteger(0)) + { + row[1] = row.FieldAsString(1).Replace("*", propertyRow.Value); + break; + } + } + } + } + + // Add the property name and value to the variableCache. + if (null != variableCache) + { + string key = String.Concat("property.", Demodularize(this.Output.Type, modularizationGuid, propertyRow.Property)); + variableCache[key] = propertyRow.Value; + } + } + } + + // Extract files that come from cabinet files (this does not extract files from merge modules). + { + ExtractEmbeddedFilesCommand command = new ExtractEmbeddedFilesCommand(); + command.FilesWithEmbeddedFiles = filesWithEmbeddedFiles; + command.Execute(); + } + + if (OutputType.Product == this.Output.Type) + { + // Retrieve files and their information from merge modules. + Table wixMergeTable = this.Output.Tables["WixMerge"]; + + if (null != wixMergeTable) + { + ExtractMergeModuleFilesCommand command = new ExtractMergeModuleFilesCommand(); + command.FileFacades = fileFacades; + command.FileTable = this.Output.Tables["File"]; + command.WixFileTable = this.Output.Tables["WixFile"]; + command.WixMergeTable = wixMergeTable; + command.OutputInstallerVersion = installerVersion; + command.SuppressLayout = this.SuppressLayout; + command.TempFilesLocation = this.TempFilesLocation; + command.Execute(); + + fileFacades.AddRange(command.MergeModulesFileFacades); + } + } + else if (OutputType.Patch == this.Output.Type) + { + // Merge transform data into the output object. + IEnumerable filesFromTransform = this.CopyFromTransformData(this.Output); + + fileFacades.AddRange(filesFromTransform); + } + + // stop processing if an error previously occurred + if (Messaging.Instance.EncounteredError) + { + return; + } + + Messaging.Instance.OnMessage(WixVerboses.UpdatingFileInformation()); + + // Gather information about files that did not come from merge modules (i.e. rows with a reference to the File table). + { + UpdateFileFacadesCommand command = new UpdateFileFacadesCommand(); + command.FileFacades = fileFacades; + command.UpdateFileFacades = fileFacades.Where(f => !f.FromModule); + command.ModularizationGuid = modularizationGuid; + command.Output = this.Output; + command.OverwriteHash = true; + command.TableDefinitions = this.TableDefinitions; + command.VariableCache = variableCache; + command.Execute(); + } + + // Set generated component guids. + this.SetComponentGuids(this.Output); + + // With the Component Guids set now we can create instance transforms. + this.CreateInstanceTransforms(this.Output); + + this.ValidateComponentGuids(this.Output); + + this.UpdateControlText(this.Output); + + if (delayedFields.Any()) + { + ResolveDelayedFieldsCommand command = new ResolveDelayedFieldsCommand(); + command.OutputType = this.Output.Type; + command.DelayedFields = delayedFields; + command.ModularizationGuid = null; + command.VariableCache = variableCache; + command.Execute(); + } + + // Assign files to media. + RowDictionary assignedMediaRows; + Dictionary> filesByCabinetMedia; + IEnumerable uncompressedFiles; + { + AssignMediaCommand command = new AssignMediaCommand(); + command.FilesCompressed = compressed; + command.FileFacades = fileFacades; + command.Output = this.Output; + command.TableDefinitions = this.TableDefinitions; + command.Execute(); + + assignedMediaRows = command.MediaRows; + filesByCabinetMedia = command.FileFacadesByCabinetMedia; + uncompressedFiles = command.UncompressedFileFacades; + } + + // Update file sequence. + this.UpdateMediaSequences(this.Output.Type, fileFacades, assignedMediaRows); + + // stop processing if an error previously occurred + if (Messaging.Instance.EncounteredError) + { + return; + } + + // Extended binder extensions can be called now that fields are resolved. + { + Table updatedFiles = this.Output.EnsureTable(this.TableDefinitions["WixBindUpdatedFiles"]); + + foreach (BinderExtension extension in this.Extensions) + { + extension.AfterResolvedFields(this.Output); + } + + List updatedFileFacades = new List(); + + foreach (Row updatedFile in updatedFiles.Rows) + { + string updatedId = updatedFile.FieldAsString(0); + + FileFacade updatedFacade = fileFacades.First(f => f.File.File.Equals(updatedId)); + + updatedFileFacades.Add(updatedFacade); + } + + if (updatedFileFacades.Any()) + { + UpdateFileFacadesCommand command = new UpdateFileFacadesCommand(); + command.FileFacades = fileFacades; + command.UpdateFileFacades = updatedFileFacades; + command.ModularizationGuid = modularizationGuid; + command.Output = this.Output; + command.OverwriteHash = true; + command.TableDefinitions = this.TableDefinitions; + command.VariableCache = variableCache; + command.Execute(); + } + } + + // stop processing if an error previously occurred + if (Messaging.Instance.EncounteredError) + { + return; + } + + Directory.CreateDirectory(this.TempFilesLocation); + + if (OutputType.Patch == this.Output.Type && this.DeltaBinaryPatch) + { + CreateDeltaPatchesCommand command = new CreateDeltaPatchesCommand(); + command.FileFacades = fileFacades; + command.WixPatchIdTable = this.Output.Tables["WixPatchId"]; + command.TempFilesLocation = this.TempFilesLocation; + command.Execute(); + } + + // create cabinet files and process uncompressed files + string layoutDirectory = Path.GetDirectoryName(this.OutputPath); + if (!this.SuppressLayout || OutputType.Module == this.Output.Type) + { + Messaging.Instance.OnMessage(WixVerboses.CreatingCabinetFiles()); + + CreateCabinetsCommand command = new CreateCabinetsCommand(); + command.CabbingThreadCount = this.CabbingThreadCount; + command.DefaultCompressionLevel = this.DefaultCompressionLevel; + command.Output = this.Output; + command.FileManagers = this.FileManagers; + command.LayoutDirectory = layoutDirectory; + command.Compressed = compressed; + command.FileRowsByCabinet = filesByCabinetMedia; + command.ResolveMedia = this.ResolveMedia; + command.TableDefinitions = this.TableDefinitions; + command.TempFilesLocation = this.TempFilesLocation; + command.WixMediaTable = this.Output.Tables["WixMedia"]; + command.Execute(); + + fileTransfers.AddRange(command.FileTransfers); + } + + if (OutputType.Patch == this.Output.Type) + { + // copy output data back into the transforms + this.CopyToTransformData(this.Output); + } + + // stop processing if an error previously occurred + if (Messaging.Instance.EncounteredError) + { + return; + } + + // add back suppressed tables which must be present prior to merging in modules + if (OutputType.Product == this.Output.Type) + { + Table wixMergeTable = this.Output.Tables["WixMerge"]; + + if (null != wixMergeTable && 0 < wixMergeTable.Rows.Count) + { + foreach (SequenceTable sequence in Enum.GetValues(typeof(SequenceTable))) + { + string sequenceTableName = sequence.ToString(); + Table sequenceTable = this.Output.Tables[sequenceTableName]; + + if (null == sequenceTable) + { + sequenceTable = this.Output.EnsureTable(this.TableDefinitions[sequenceTableName]); + } + + if (0 == sequenceTable.Rows.Count) + { + suppressedTableNames.Add(sequenceTableName); + } + } + } + } + + foreach (BinderExtension extension in this.Extensions) + { + extension.Finish(this.Output); + } + + // generate database file + Messaging.Instance.OnMessage(WixVerboses.GeneratingDatabase()); + string tempDatabaseFile = Path.Combine(this.TempFilesLocation, Path.GetFileName(this.OutputPath)); + this.GenerateDatabase(this.Output, tempDatabaseFile, false, false); + + FileTransfer transfer; + if (FileTransfer.TryCreate(tempDatabaseFile, this.OutputPath, true, this.Output.Type.ToString(), null, out transfer)) // note where this database needs to move in the future + { + transfer.Built = true; + fileTransfers.Add(transfer); + } + + // stop processing if an error previously occurred + if (Messaging.Instance.EncounteredError) + { + return; + } + + // Output the output to a file + Pdb pdb = new Pdb(); + pdb.Output = this.Output; + if (!String.IsNullOrEmpty(this.PdbFile)) + { + pdb.Save(this.PdbFile); + } + + // Merge modules. + if (OutputType.Product == this.Output.Type) + { + Messaging.Instance.OnMessage(WixVerboses.MergingModules()); + + MergeModulesCommand command = new MergeModulesCommand(); + command.FileFacades = fileFacades; + command.Output = this.Output; + command.OutputPath = tempDatabaseFile; + command.SuppressedTableNames = suppressedTableNames; + command.Execute(); + + // stop processing if an error previously occurred + if (Messaging.Instance.EncounteredError) + { + return; + } + } + + // inspect the MSI prior to running ICEs + InspectorCore inspectorCore = new InspectorCore(); + foreach (InspectorExtension inspectorExtension in this.InspectorExtensions) + { + inspectorExtension.Core = inspectorCore; + inspectorExtension.InspectDatabase(tempDatabaseFile, pdb); + + inspectorExtension.Core = null; // reset. + } + + if (Messaging.Instance.EncounteredError) + { + return; + } + + // validate the output if there is an MSI validator + if (null != this.Validator) + { + Stopwatch stopwatch = Stopwatch.StartNew(); + + // set the output file for source line information + this.Validator.Output = this.Output; + + Messaging.Instance.OnMessage(WixVerboses.ValidatingDatabase()); + + this.Validator.Validate(tempDatabaseFile); + + stopwatch.Stop(); + Messaging.Instance.OnMessage(WixVerboses.ValidatedDatabase(stopwatch.ElapsedMilliseconds)); + + // Stop processing if an error occurred. + if (Messaging.Instance.EncounteredError) + { + return; + } + } + + // Process uncompressed files. + if (!Messaging.Instance.EncounteredError && !this.SuppressLayout && uncompressedFiles.Any()) + { + ProcessUncompressedFilesCommand command = new ProcessUncompressedFilesCommand(); + command.Compressed = compressed; + command.FileFacades = uncompressedFiles; + command.LayoutDirectory = layoutDirectory; + command.LongNamesInImage = longNames; + command.MediaRows = assignedMediaRows; + command.ResolveMedia = this.ResolveMedia; + command.DatabasePath = tempDatabaseFile; + command.WixMediaTable = this.Output.Tables["WixMedia"]; + command.Execute(); + + fileTransfers.AddRange(command.FileTransfers); + } + + this.FileTransfers = fileTransfers; + this.ContentFilePaths = fileFacades.Select(r => r.WixFile.Source).ToList(); + } + + /// + /// Localize dialogs and controls. + /// + /// The tables to localize. + private void LocalizeUI(TableIndexedCollection tables) + { + Table dialogTable = tables["Dialog"]; + if (null != dialogTable) + { + foreach (Row row in dialogTable.Rows) + { + string dialog = (string)row[0]; + LocalizedControl localizedControl = this.Localizer.GetLocalizedControl(dialog, null); + if (null != localizedControl) + { + if (CompilerConstants.IntegerNotSet != localizedControl.X) + { + row[1] = localizedControl.X; + } + + if (CompilerConstants.IntegerNotSet != localizedControl.Y) + { + row[2] = localizedControl.Y; + } + + if (CompilerConstants.IntegerNotSet != localizedControl.Width) + { + row[3] = localizedControl.Width; + } + + if (CompilerConstants.IntegerNotSet != localizedControl.Height) + { + row[4] = localizedControl.Height; + } + + row[5] = (int)row[5] | localizedControl.Attributes; + + if (!String.IsNullOrEmpty(localizedControl.Text)) + { + row[6] = localizedControl.Text; + } + } + } + } + + Table controlTable = tables["Control"]; + if (null != controlTable) + { + foreach (Row row in controlTable.Rows) + { + string dialog = (string)row[0]; + string control = (string)row[1]; + LocalizedControl localizedControl = this.Localizer.GetLocalizedControl(dialog, control); + if (null != localizedControl) + { + if (CompilerConstants.IntegerNotSet != localizedControl.X) + { + row[3] = localizedControl.X.ToString(); + } + + if (CompilerConstants.IntegerNotSet != localizedControl.Y) + { + row[4] = localizedControl.Y.ToString(); + } + + if (CompilerConstants.IntegerNotSet != localizedControl.Width) + { + row[5] = localizedControl.Width.ToString(); + } + + if (CompilerConstants.IntegerNotSet != localizedControl.Height) + { + row[6] = localizedControl.Height.ToString(); + } + + row[7] = (int)row[7] | localizedControl.Attributes; + + if (!String.IsNullOrEmpty(localizedControl.Text)) + { + row[9] = localizedControl.Text; + } + } + } + } + } + + /// + /// Copy file data between transform substorages and the patch output object + /// + /// The output to bind. + /// True if copying from transform to patch, false the other way. + private IEnumerable CopyFromTransformData(Output output) + { + CopyTransformDataCommand command = new CopyTransformDataCommand(); + command.CopyOutFileRows = true; + command.FileManagerCore = this.FileManagerCore; + command.FileManagers = this.FileManagers; + command.Output = output; + command.TableDefinitions = this.TableDefinitions; + command.Execute(); + + return command.FileFacades; + } + + /// + /// Copy file data between transform substorages and the patch output object + /// + /// The output to bind. + /// True if copying from transform to patch, false the other way. + private void CopyToTransformData(Output output) + { + CopyTransformDataCommand command = new CopyTransformDataCommand(); + command.CopyOutFileRows = false; + command.FileManagerCore = this.FileManagerCore; + command.FileManagers = this.FileManagers; + command.Output = output; + command.TableDefinitions = this.TableDefinitions; + command.Execute(); + } + + /// + /// Takes an id, and demodularizes it (if possible). + /// + /// + /// If the output type is a module, returns a demodularized version of an id. Otherwise, returns the id. + /// + /// The type of the output to bind. + /// The modularization GUID. + /// The id to demodularize. + /// The demodularized id. + internal static string Demodularize(OutputType outputType, string modularizationGuid, string id) + { + if (OutputType.Module == outputType && id.EndsWith(String.Concat(".", modularizationGuid), StringComparison.Ordinal)) + { + id = id.Substring(0, id.Length - 37); + } + + return id; + } + + private void UpdateMediaSequences(OutputType outputType, IEnumerable fileFacades, RowDictionary mediaRows) + { + // Calculate sequence numbers and media disk id layout for all file media information objects. + if (OutputType.Module == outputType) + { + int lastSequence = 0; + 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. + { + facade.File.Sequence = ++lastSequence; + } + } + else + { + int lastSequence = 0; + MediaRow mediaRow = null; + Dictionary> patchGroups = new Dictionary>(); + + // sequence the non-patch-added files + 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. + { + if (null == mediaRow) + { + mediaRow = mediaRows.Get(facade.WixFile.DiskId); + if (OutputType.Patch == outputType) + { + // patch Media cannot start at zero + lastSequence = mediaRow.LastSequence; + } + } + else if (mediaRow.DiskId != facade.WixFile.DiskId) + { + mediaRow.LastSequence = lastSequence; + mediaRow = mediaRows.Get(facade.WixFile.DiskId); + } + + if (0 < facade.WixFile.PatchGroup) + { + List patchGroup = patchGroups[facade.WixFile.PatchGroup]; + + if (null == patchGroup) + { + patchGroup = new List(); + patchGroups.Add(facade.WixFile.PatchGroup, patchGroup); + } + + patchGroup.Add(facade); + } + else + { + facade.File.Sequence = ++lastSequence; + } + } + + if (null != mediaRow) + { + mediaRow.LastSequence = lastSequence; + mediaRow = null; + } + + // sequence the patch-added files + foreach (List patchGroup in patchGroups.Values) + { + foreach (FileFacade facade in patchGroup) + { + if (null == mediaRow) + { + mediaRow = mediaRows.Get(facade.WixFile.DiskId); + } + else if (mediaRow.DiskId != facade.WixFile.DiskId) + { + mediaRow.LastSequence = lastSequence; + mediaRow = mediaRows.Get(facade.WixFile.DiskId); + } + + facade.File.Sequence = ++lastSequence; + } + } + + if (null != mediaRow) + { + mediaRow.LastSequence = lastSequence; + } + } + } + + /// + /// Set the guids for components with generatable guids. + /// + /// Internal representation of the database to operate on. + private void SetComponentGuids(Output output) + { + Table componentTable = output.Tables["Component"]; + if (null != componentTable) + { + Hashtable registryKeyRows = null; + Hashtable directories = null; + Hashtable componentIdGenSeeds = null; + Dictionary> fileRows = null; + + // find components with generatable guids + foreach (ComponentRow componentRow in componentTable.Rows) + { + // component guid will be generated + if ("*" == componentRow.Guid) + { + if (null == componentRow.KeyPath || componentRow.IsOdbcDataSourceKeyPath) + { + Messaging.Instance.OnMessage(WixErrors.IllegalComponentWithAutoGeneratedGuid(componentRow.SourceLineNumbers)); + } + else if (componentRow.IsRegistryKeyPath) + { + if (null == registryKeyRows) + { + Table registryTable = output.Tables["Registry"]; + + registryKeyRows = new Hashtable(registryTable.Rows.Count); + + foreach (Row registryRow in registryTable.Rows) + { + registryKeyRows.Add((string)registryRow[0], registryRow); + } + } + + Row foundRow = registryKeyRows[componentRow.KeyPath] as Row; + + string bitness = componentRow.Is64Bit ? "64" : String.Empty; + if (null != foundRow) + { + string regkey = String.Concat(bitness, foundRow[1], "\\", foundRow[2], "\\", foundRow[3]); + componentRow.Guid = Uuid.NewUuid(BindDatabaseCommand.WixComponentGuidNamespace, regkey.ToLowerInvariant()).ToString("B").ToUpperInvariant(); + } + } + else // must be a File KeyPath + { + // if the directory table hasn't been loaded into an indexed hash + // of directory ids to target names do that now. + if (null == directories) + { + Table directoryTable = output.Tables["Directory"]; + + int numDirectoryTableRows = (null != directoryTable) ? directoryTable.Rows.Count : 0; + + directories = new Hashtable(numDirectoryTableRows); + + // get the target paths for all directories + if (null != directoryTable) + { + foreach (Row row in directoryTable.Rows) + { + // if the directory Id already exists, we will skip it here since + // checking for duplicate primary keys is done later when importing tables + // into database + if (directories.ContainsKey(row[0])) + { + continue; + } + + string targetName = Installer.GetName((string)row[2], false, true); + directories.Add(row[0], new ResolvedDirectory((string)row[1], targetName)); + } + } + } + + // if the component id generation seeds have not been indexed + // from the WixDirectory table do that now. + if (null == componentIdGenSeeds) + { + Table wixDirectoryTable = output.Tables["WixDirectory"]; + + int numWixDirectoryRows = (null != wixDirectoryTable) ? wixDirectoryTable.Rows.Count : 0; + + componentIdGenSeeds = new Hashtable(numWixDirectoryRows); + + // if there are any WixDirectory rows, build up the Component Guid + // generation seeds indexed by Directory/@Id. + if (null != wixDirectoryTable) + { + foreach (Row row in wixDirectoryTable.Rows) + { + componentIdGenSeeds.Add(row[0], (string)row[1]); + } + } + } + + // if the file rows have not been indexed by File.Component yet + // then do that now + if (null == fileRows) + { + Table fileTable = output.Tables["File"]; + + int numFileRows = (null != fileTable) ? fileTable.Rows.Count : 0; + + fileRows = new Dictionary>(numFileRows); + + if (null != fileTable) + { + foreach (FileRow file in fileTable.Rows) + { + List files; + if (!fileRows.TryGetValue(file.Component, out files)) + { + files = new List(); + fileRows.Add(file.Component, files); + } + + files.Add(file); + } + } + } + + // validate component meets all the conditions to have a generated guid + List currentComponentFiles = fileRows[componentRow.Component]; + int numFilesInComponent = currentComponentFiles.Count; + string path = null; + + foreach (FileRow fileRow in currentComponentFiles) + { + if (fileRow.File == componentRow.KeyPath) + { + // calculate the key file's canonical target path + string directoryPath = Binder.GetDirectoryPath(directories, componentIdGenSeeds, componentRow.Directory, true); + string fileName = Installer.GetName(fileRow.FileName, false, true).ToLower(CultureInfo.InvariantCulture); + path = Path.Combine(directoryPath, fileName); + + // find paths that are not canonicalized + if (path.StartsWith(@"PersonalFolder\my pictures", StringComparison.Ordinal) || + path.StartsWith(@"ProgramFilesFolder\common files", StringComparison.Ordinal) || + path.StartsWith(@"ProgramMenuFolder\startup", StringComparison.Ordinal) || + path.StartsWith("TARGETDIR", StringComparison.Ordinal) || + path.StartsWith(@"StartMenuFolder\programs", StringComparison.Ordinal) || + path.StartsWith(@"WindowsFolder\fonts", StringComparison.Ordinal)) + { + Messaging.Instance.OnMessage(WixErrors.IllegalPathForGeneratedComponentGuid(componentRow.SourceLineNumbers, fileRow.Component, path)); + } + + // if component has more than one file, the key path must be versioned + if (1 < numFilesInComponent && String.IsNullOrEmpty(fileRow.Version)) + { + Messaging.Instance.OnMessage(WixErrors.IllegalGeneratedGuidComponentUnversionedKeypath(componentRow.SourceLineNumbers)); + } + } + else + { + // not a key path, so it must be an unversioned file if component has more than one file + if (1 < numFilesInComponent && !String.IsNullOrEmpty(fileRow.Version)) + { + Messaging.Instance.OnMessage(WixErrors.IllegalGeneratedGuidComponentVersionedNonkeypath(componentRow.SourceLineNumbers)); + } + } + } + + // if the rules were followed, reward with a generated guid + if (!Messaging.Instance.EncounteredError) + { + componentRow.Guid = Uuid.NewUuid(BindDatabaseCommand.WixComponentGuidNamespace, path).ToString("B").ToUpperInvariant(); + } + } + } + } + } + } + + /// + /// Creates instance transform substorages in the output. + /// + /// Output containing instance transform definitions. + private void CreateInstanceTransforms(Output output) + { + // Create and add substorages for instance transforms. + Table wixInstanceTransformsTable = output.Tables["WixInstanceTransforms"]; + if (null != wixInstanceTransformsTable && 0 <= wixInstanceTransformsTable.Rows.Count) + { + string targetProductCode = null; + string targetUpgradeCode = null; + string targetProductVersion = null; + + Table targetSummaryInformationTable = output.Tables["_SummaryInformation"]; + Table targetPropertyTable = output.Tables["Property"]; + + // Get the data from target database + foreach (Row propertyRow in targetPropertyTable.Rows) + { + if ("ProductCode" == (string)propertyRow[0]) + { + targetProductCode = (string)propertyRow[1]; + } + else if ("ProductVersion" == (string)propertyRow[0]) + { + targetProductVersion = (string)propertyRow[1]; + } + else if ("UpgradeCode" == (string)propertyRow[0]) + { + targetUpgradeCode = (string)propertyRow[1]; + } + } + + // Index the Instance Component Rows. + Dictionary instanceComponentGuids = new Dictionary(); + Table targetInstanceComponentTable = output.Tables["WixInstanceComponent"]; + if (null != targetInstanceComponentTable && 0 < targetInstanceComponentTable.Rows.Count) + { + foreach (Row row in targetInstanceComponentTable.Rows) + { + // Build up all the instances, we'll get the Components rows from the real Component table. + instanceComponentGuids.Add((string)row[0], null); + } + + Table targetComponentTable = output.Tables["Component"]; + foreach (ComponentRow componentRow in targetComponentTable.Rows) + { + string component = (string)componentRow[0]; + if (instanceComponentGuids.ContainsKey(component)) + { + instanceComponentGuids[component] = componentRow; + } + } + } + + // Generate the instance transforms + foreach (Row instanceRow in wixInstanceTransformsTable.Rows) + { + string instanceId = (string)instanceRow[0]; + + Output instanceTransform = new Output(instanceRow.SourceLineNumbers); + instanceTransform.Type = OutputType.Transform; + instanceTransform.Codepage = output.Codepage; + + Table instanceSummaryInformationTable = instanceTransform.EnsureTable(this.TableDefinitions["_SummaryInformation"]); + string targetPlatformAndLanguage = null; + + foreach (Row summaryInformationRow in targetSummaryInformationTable.Rows) + { + if (7 == (int)summaryInformationRow[0]) // PID_TEMPLATE + { + targetPlatformAndLanguage = (string)summaryInformationRow[1]; + } + + // Copy the row's data to the transform. + Row copyOfSummaryRow = instanceSummaryInformationTable.CreateRow(null); + copyOfSummaryRow[0] = summaryInformationRow[0]; + copyOfSummaryRow[1] = summaryInformationRow[1]; + } + + // Modify the appropriate properties. + Table propertyTable = instanceTransform.EnsureTable(this.TableDefinitions["Property"]); + + // Change the ProductCode property + string productCode = (string)instanceRow[2]; + if ("*" == productCode) + { + productCode = Common.GenerateGuid(); + } + + Row productCodeRow = propertyTable.CreateRow(instanceRow.SourceLineNumbers); + productCodeRow.Operation = RowOperation.Modify; + productCodeRow.Fields[1].Modified = true; + productCodeRow[0] = "ProductCode"; + productCodeRow[1] = productCode; + + // Change the instance property + Row instanceIdRow = propertyTable.CreateRow(instanceRow.SourceLineNumbers); + instanceIdRow.Operation = RowOperation.Modify; + instanceIdRow.Fields[1].Modified = true; + instanceIdRow[0] = (string)instanceRow[1]; + instanceIdRow[1] = instanceId; + + if (null != instanceRow[3]) + { + // Change the ProductName property + Row productNameRow = propertyTable.CreateRow(instanceRow.SourceLineNumbers); + productNameRow.Operation = RowOperation.Modify; + productNameRow.Fields[1].Modified = true; + productNameRow[0] = "ProductName"; + productNameRow[1] = (string)instanceRow[3]; + } + + if (null != instanceRow[4]) + { + // Change the UpgradeCode property + Row upgradeCodeRow = propertyTable.CreateRow(instanceRow.SourceLineNumbers); + upgradeCodeRow.Operation = RowOperation.Modify; + upgradeCodeRow.Fields[1].Modified = true; + upgradeCodeRow[0] = "UpgradeCode"; + upgradeCodeRow[1] = instanceRow[4]; + + // Change the Upgrade table + Table targetUpgradeTable = output.Tables["Upgrade"]; + if (null != targetUpgradeTable && 0 <= targetUpgradeTable.Rows.Count) + { + string upgradeId = (string)instanceRow[4]; + Table upgradeTable = instanceTransform.EnsureTable(this.TableDefinitions["Upgrade"]); + foreach (Row row in targetUpgradeTable.Rows) + { + // In case they are upgrading other codes to this new product, leave the ones that don't match the + // Product.UpgradeCode intact. + if (targetUpgradeCode == (string)row[0]) + { + Row upgradeRow = upgradeTable.CreateRow(null); + upgradeRow.Operation = RowOperation.Add; + upgradeRow.Fields[0].Modified = true; + // I was hoping to be able to RowOperation.Modify, but that didn't appear to function. + // upgradeRow.Fields[0].PreviousData = (string)row[0]; + + // Inserting a new Upgrade record with the updated UpgradeCode + upgradeRow[0] = upgradeId; + upgradeRow[1] = row[1]; + upgradeRow[2] = row[2]; + upgradeRow[3] = row[3]; + upgradeRow[4] = row[4]; + upgradeRow[5] = row[5]; + upgradeRow[6] = row[6]; + + // Delete the old row + Row upgradeRemoveRow = upgradeTable.CreateRow(null); + upgradeRemoveRow.Operation = RowOperation.Delete; + upgradeRemoveRow[0] = row[0]; + upgradeRemoveRow[1] = row[1]; + upgradeRemoveRow[2] = row[2]; + upgradeRemoveRow[3] = row[3]; + upgradeRemoveRow[4] = row[4]; + upgradeRemoveRow[5] = row[5]; + upgradeRemoveRow[6] = row[6]; + } + } + } + } + + // If there are instance Components generate new GUIDs for them. + if (0 < instanceComponentGuids.Count) + { + Table componentTable = instanceTransform.EnsureTable(this.TableDefinitions["Component"]); + foreach (ComponentRow targetComponentRow in instanceComponentGuids.Values) + { + string guid = targetComponentRow.Guid; + if (!String.IsNullOrEmpty(guid)) + { + Row instanceComponentRow = componentTable.CreateRow(targetComponentRow.SourceLineNumbers); + instanceComponentRow.Operation = RowOperation.Modify; + instanceComponentRow.Fields[1].Modified = true; + instanceComponentRow[0] = targetComponentRow[0]; + instanceComponentRow[1] = Uuid.NewUuid(BindDatabaseCommand.WixComponentGuidNamespace, String.Concat(guid, instanceId)).ToString("B").ToUpper(CultureInfo.InvariantCulture); + instanceComponentRow[2] = targetComponentRow[2]; + instanceComponentRow[3] = targetComponentRow[3]; + instanceComponentRow[4] = targetComponentRow[4]; + instanceComponentRow[5] = targetComponentRow[5]; + } + } + } + + // Update the summary information + Hashtable summaryRows = new Hashtable(instanceSummaryInformationTable.Rows.Count); + foreach (Row row in instanceSummaryInformationTable.Rows) + { + summaryRows[row[0]] = row; + + if ((int)SummaryInformation.Transform.UpdatedPlatformAndLanguage == (int)row[0]) + { + row[1] = targetPlatformAndLanguage; + } + else if ((int)SummaryInformation.Transform.ProductCodes == (int)row[0]) + { + row[1] = String.Concat(targetProductCode, targetProductVersion, ';', productCode, targetProductVersion, ';', targetUpgradeCode); + } + else if ((int)SummaryInformation.Transform.ValidationFlags == (int)row[0]) + { + row[1] = 0; + } + else if ((int)SummaryInformation.Transform.Security == (int)row[0]) + { + row[1] = "4"; + } + } + + if (!summaryRows.Contains((int)SummaryInformation.Transform.UpdatedPlatformAndLanguage)) + { + Row summaryRow = instanceSummaryInformationTable.CreateRow(null); + summaryRow[0] = (int)SummaryInformation.Transform.UpdatedPlatformAndLanguage; + summaryRow[1] = targetPlatformAndLanguage; + } + else if (!summaryRows.Contains((int)SummaryInformation.Transform.ValidationFlags)) + { + Row summaryRow = instanceSummaryInformationTable.CreateRow(null); + summaryRow[0] = (int)SummaryInformation.Transform.ValidationFlags; + summaryRow[1] = "0"; + } + else if (!summaryRows.Contains((int)SummaryInformation.Transform.Security)) + { + Row summaryRow = instanceSummaryInformationTable.CreateRow(null); + summaryRow[0] = (int)SummaryInformation.Transform.Security; + summaryRow[1] = "4"; + } + + output.SubStorages.Add(new SubStorage(instanceId, instanceTransform)); + } + } + } + + /// + /// Validate that there are no duplicate GUIDs in the output. + /// + /// + /// Duplicate GUIDs without conditions are an error condition; with conditions, it's a + /// warning, as the conditions might be mutually exclusive. + /// + private void ValidateComponentGuids(Output output) + { + Table componentTable = output.Tables["Component"]; + if (null != componentTable) + { + Dictionary componentGuidConditions = new Dictionary(componentTable.Rows.Count); + + foreach (ComponentRow row in componentTable.Rows) + { + // we don't care about unmanaged components and if there's a * GUID remaining, + // there's already an error that prevented it from being replaced with a real GUID. + if (!String.IsNullOrEmpty(row.Guid) && "*" != row.Guid) + { + bool thisComponentHasCondition = !String.IsNullOrEmpty(row.Condition); + bool allComponentsHaveConditions = thisComponentHasCondition; + + if (componentGuidConditions.ContainsKey(row.Guid)) + { + allComponentsHaveConditions = componentGuidConditions[row.Guid] && thisComponentHasCondition; + + if (allComponentsHaveConditions) + { + Messaging.Instance.OnMessage(WixWarnings.DuplicateComponentGuidsMustHaveMutuallyExclusiveConditions(row.SourceLineNumbers, row.Component, row.Guid)); + } + else + { + Messaging.Instance.OnMessage(WixErrors.DuplicateComponentGuids(row.SourceLineNumbers, row.Component, row.Guid)); + } + } + + componentGuidConditions[row.Guid] = allComponentsHaveConditions; + } + } + } + } + + /// + /// Update Control and BBControl text by reading from files when necessary. + /// + /// Internal representation of the msi database to operate upon. + private void UpdateControlText(Output output) + { + UpdateControlTextCommand command = new UpdateControlTextCommand(); + command.BBControlTable = output.Tables["BBControl"]; + command.WixBBControlTable = output.Tables["WixBBControl"]; + command.ControlTable = output.Tables["Control"]; + command.WixControlTable = output.Tables["WixControl"]; + command.Execute(); + } + + private string ResolveMedia(MediaRow mediaRow, string mediaLayoutDirectory, string layoutDirectory) + { + string layout = null; + + foreach (IBinderFileManager fileManager in this.FileManagers) + { + layout = fileManager.ResolveMedia(mediaRow, mediaLayoutDirectory, layoutDirectory); + if (!String.IsNullOrEmpty(layout)) + { + break; + } + } + + // If no binder file manager resolved the layout, do the default behavior. + if (String.IsNullOrEmpty(layout)) + { + if (String.IsNullOrEmpty(mediaLayoutDirectory)) + { + layout = layoutDirectory; + } + else if (Path.IsPathRooted(mediaLayoutDirectory)) + { + layout = mediaLayoutDirectory; + } + else + { + layout = Path.Combine(layoutDirectory, mediaLayoutDirectory); + } + } + + return layout; + } + + /// + /// Creates the MSI/MSM/PCP database. + /// + /// Output to create database for. + /// The database file to create. + /// Whether to keep columns added in a transform. + /// Whether to use a subdirectory based on the file name for intermediate files. + private void GenerateDatabase(Output output, string databaseFile, bool keepAddedColumns, bool useSubdirectory) + { + GenerateDatabaseCommand command = new GenerateDatabaseCommand(); + command.Extensions = this.Extensions; + command.FileManagers = this.FileManagers; + command.Output = output; + command.OutputPath = databaseFile; + command.KeepAddedColumns = keepAddedColumns; + command.UseSubDirectory = useSubdirectory; + command.SuppressAddingValidationRows = this.SuppressAddingValidationRows; + command.TableDefinitions = this.TableDefinitions; + command.TempFilesLocation = this.TempFilesLocation; + command.Codepage = this.Codepage; + command.Execute(); + } + } +} diff --git a/src/WixToolset.Core/Bind/BindTransformCommand.cs b/src/WixToolset.Core/Bind/BindTransformCommand.cs new file mode 100644 index 00000000..e909f191 --- /dev/null +++ b/src/WixToolset.Core/Bind/BindTransformCommand.cs @@ -0,0 +1,473 @@ +// 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. + +namespace WixToolset.Bind +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using WixToolset.Data; + using WixToolset.Extensibility; + using WixToolset.Msi; + using WixToolset.Core.Native; + + internal class BindTransformCommand : ICommand + { + public IEnumerable Extensions { private get; set; } + + public IEnumerable FileManagers { private get; set; } + + public TableDefinitionCollection TableDefinitions { private get; set; } + + public string TempFilesLocation { private get; set; } + + public Output Transform { private get; set; } + + public string OutputPath { private get; set; } + + public void Execute() + { + int transformFlags = 0; + + Output targetOutput = new Output(null); + Output updatedOutput = new Output(null); + + // TODO: handle added columns + + // to generate a localized transform, both the target and updated + // databases need to have the same code page. the only reason to + // set different code pages is to support localized primary key + // columns, but that would only support deleting rows. if this + // becomes necessary, define a PreviousCodepage property on the + // Output class and persist this throughout transform generation. + targetOutput.Codepage = this.Transform.Codepage; + updatedOutput.Codepage = this.Transform.Codepage; + + // remove certain Property rows which will be populated from summary information values + string targetUpgradeCode = null; + string updatedUpgradeCode = null; + + Table propertyTable = this.Transform.Tables["Property"]; + if (null != propertyTable) + { + for (int i = propertyTable.Rows.Count - 1; i >= 0; i--) + { + Row row = propertyTable.Rows[i]; + + if ("ProductCode" == (string)row[0] || "ProductLanguage" == (string)row[0] || "ProductVersion" == (string)row[0] || "UpgradeCode" == (string)row[0]) + { + propertyTable.Rows.RemoveAt(i); + + if ("UpgradeCode" == (string)row[0]) + { + updatedUpgradeCode = (string)row[1]; + } + } + } + } + + Table targetSummaryInfo = targetOutput.EnsureTable(this.TableDefinitions["_SummaryInformation"]); + Table updatedSummaryInfo = updatedOutput.EnsureTable(this.TableDefinitions["_SummaryInformation"]); + Table targetPropertyTable = targetOutput.EnsureTable(this.TableDefinitions["Property"]); + Table updatedPropertyTable = updatedOutput.EnsureTable(this.TableDefinitions["Property"]); + + // process special summary information values + foreach (Row row in this.Transform.Tables["_SummaryInformation"].Rows) + { + if ((int)SummaryInformation.Transform.CodePage == (int)row[0]) + { + // convert from a web name if provided + string codePage = (string)row.Fields[1].Data; + if (null == codePage) + { + codePage = "0"; + } + else + { + codePage = Common.GetValidCodePage(codePage).ToString(CultureInfo.InvariantCulture); + } + + string previousCodePage = (string)row.Fields[1].PreviousData; + if (null == previousCodePage) + { + previousCodePage = "0"; + } + else + { + previousCodePage = Common.GetValidCodePage(previousCodePage).ToString(CultureInfo.InvariantCulture); + } + + Row targetCodePageRow = targetSummaryInfo.CreateRow(null); + targetCodePageRow[0] = 1; // PID_CODEPAGE + targetCodePageRow[1] = previousCodePage; + + Row updatedCodePageRow = updatedSummaryInfo.CreateRow(null); + updatedCodePageRow[0] = 1; // PID_CODEPAGE + updatedCodePageRow[1] = codePage; + } + else if ((int)SummaryInformation.Transform.TargetPlatformAndLanguage == (int)row[0] || + (int)SummaryInformation.Transform.UpdatedPlatformAndLanguage == (int)row[0]) + { + // the target language + string[] propertyData = ((string)row[1]).Split(';'); + string lang = 2 == propertyData.Length ? propertyData[1] : "0"; + + Table tempSummaryInfo = (int)SummaryInformation.Transform.TargetPlatformAndLanguage == (int)row[0] ? targetSummaryInfo : updatedSummaryInfo; + Table tempPropertyTable = (int)SummaryInformation.Transform.TargetPlatformAndLanguage == (int)row[0] ? targetPropertyTable : updatedPropertyTable; + + Row productLanguageRow = tempPropertyTable.CreateRow(null); + productLanguageRow[0] = "ProductLanguage"; + productLanguageRow[1] = lang; + + // set the platform;language on the MSI to be generated + Row templateRow = tempSummaryInfo.CreateRow(null); + templateRow[0] = 7; // PID_TEMPLATE + templateRow[1] = (string)row[1]; + } + else if ((int)SummaryInformation.Transform.ProductCodes == (int)row[0]) + { + string[] propertyData = ((string)row[1]).Split(';'); + + Row targetProductCodeRow = targetPropertyTable.CreateRow(null); + targetProductCodeRow[0] = "ProductCode"; + targetProductCodeRow[1] = propertyData[0].Substring(0, 38); + + Row targetProductVersionRow = targetPropertyTable.CreateRow(null); + targetProductVersionRow[0] = "ProductVersion"; + targetProductVersionRow[1] = propertyData[0].Substring(38); + + Row updatedProductCodeRow = updatedPropertyTable.CreateRow(null); + updatedProductCodeRow[0] = "ProductCode"; + updatedProductCodeRow[1] = propertyData[1].Substring(0, 38); + + Row updatedProductVersionRow = updatedPropertyTable.CreateRow(null); + updatedProductVersionRow[0] = "ProductVersion"; + updatedProductVersionRow[1] = propertyData[1].Substring(38); + + // UpgradeCode is optional and may not exists in the target + // or upgraded databases, so do not include a null-valued + // UpgradeCode property. + + targetUpgradeCode = propertyData[2]; + if (!String.IsNullOrEmpty(targetUpgradeCode)) + { + Row targetUpgradeCodeRow = targetPropertyTable.CreateRow(null); + targetUpgradeCodeRow[0] = "UpgradeCode"; + targetUpgradeCodeRow[1] = targetUpgradeCode; + + // If the target UpgradeCode is specified, an updated + // UpgradeCode is required. + if (String.IsNullOrEmpty(updatedUpgradeCode)) + { + updatedUpgradeCode = targetUpgradeCode; + } + } + + if (!String.IsNullOrEmpty(updatedUpgradeCode)) + { + Row updatedUpgradeCodeRow = updatedPropertyTable.CreateRow(null); + updatedUpgradeCodeRow[0] = "UpgradeCode"; + updatedUpgradeCodeRow[1] = updatedUpgradeCode; + } + } + else if ((int)SummaryInformation.Transform.ValidationFlags == (int)row[0]) + { + transformFlags = Convert.ToInt32(row[1], CultureInfo.InvariantCulture); + } + else if ((int)SummaryInformation.Transform.Reserved11 == (int)row[0]) + { + // PID_LASTPRINTED should be null for transforms + row.Operation = RowOperation.None; + } + else + { + // add everything else as is + Row targetRow = targetSummaryInfo.CreateRow(null); + targetRow[0] = row[0]; + targetRow[1] = row[1]; + + Row updatedRow = updatedSummaryInfo.CreateRow(null); + updatedRow[0] = row[0]; + updatedRow[1] = row[1]; + } + } + + // Validate that both databases have an UpgradeCode if the + // authoring transform will validate the UpgradeCode; otherwise, + // MsiCreateTransformSummaryinfo() will fail with 1620. + if (((int)TransformFlags.ValidateUpgradeCode & transformFlags) != 0 && + (String.IsNullOrEmpty(targetUpgradeCode) || String.IsNullOrEmpty(updatedUpgradeCode))) + { + Messaging.Instance.OnMessage(WixErrors.BothUpgradeCodesRequired()); + } + + string emptyFile = null; + + foreach (Table table in this.Transform.Tables) + { + // Ignore unreal tables when building transforms except the _Stream table. + // These tables are ignored when generating the database so there is no reason + // to process them here. + if (table.Definition.Unreal && "_Streams" != table.Name) + { + continue; + } + + // process table operations + switch (table.Operation) + { + case TableOperation.Add: + updatedOutput.EnsureTable(table.Definition); + break; + case TableOperation.Drop: + targetOutput.EnsureTable(table.Definition); + continue; + default: + targetOutput.EnsureTable(table.Definition); + updatedOutput.EnsureTable(table.Definition); + break; + } + + // process row operations + foreach (Row row in table.Rows) + { + switch (row.Operation) + { + case RowOperation.Add: + Table updatedTable = updatedOutput.EnsureTable(table.Definition); + updatedTable.Rows.Add(row); + continue; + case RowOperation.Delete: + Table targetTable = targetOutput.EnsureTable(table.Definition); + targetTable.Rows.Add(row); + + // fill-in non-primary key values + foreach (Field field in row.Fields) + { + if (!field.Column.PrimaryKey) + { + if (ColumnType.Number == field.Column.Type && !field.Column.IsLocalizable) + { + field.Data = field.Column.MinValue; + } + else if (ColumnType.Object == field.Column.Type) + { + if (null == emptyFile) + { + emptyFile = Path.Combine(this.TempFilesLocation, "empty"); + } + + field.Data = emptyFile; + } + else + { + field.Data = "0"; + } + } + } + continue; + } + + // Assure that the file table's sequence is populated + if ("File" == table.Name) + { + foreach (Row fileRow in table.Rows) + { + if (null == fileRow[7]) + { + if (RowOperation.Add == fileRow.Operation) + { + Messaging.Instance.OnMessage(WixErrors.InvalidAddedFileRowWithoutSequence(fileRow.SourceLineNumbers, (string)fileRow[0])); + break; + } + + // Set to 1 to prevent invalid IDT file from being generated + fileRow[7] = 1; + } + } + } + + // process modified and unmodified rows + bool modifiedRow = false; + Row targetRow = new Row(null, table.Definition); + Row updatedRow = row; + for (int i = 0; i < row.Fields.Length; i++) + { + Field updatedField = row.Fields[i]; + + if (updatedField.Modified) + { + // set a different value in the target row to ensure this value will be modified during transform generation + if (ColumnType.Number == updatedField.Column.Type && !updatedField.Column.IsLocalizable) + { + if (null == updatedField.Data || 1 != (int)updatedField.Data) + { + targetRow[i] = 1; + } + else + { + targetRow[i] = 2; + } + } + else if (ColumnType.Object == updatedField.Column.Type) + { + if (null == emptyFile) + { + emptyFile = Path.Combine(this.TempFilesLocation, "empty"); + } + + targetRow[i] = emptyFile; + } + else + { + if ("0" != (string)updatedField.Data) + { + targetRow[i] = "0"; + } + else + { + targetRow[i] = "1"; + } + } + + modifiedRow = true; + } + else if (ColumnType.Object == updatedField.Column.Type) + { + ObjectField objectField = (ObjectField)updatedField; + + // create an empty file for comparing against + if (null == objectField.PreviousData) + { + if (null == emptyFile) + { + emptyFile = Path.Combine(this.TempFilesLocation, "empty"); + } + + targetRow[i] = emptyFile; + modifiedRow = true; + } + else if (!this.CompareFiles(objectField.PreviousData, (string)objectField.Data)) + { + targetRow[i] = objectField.PreviousData; + modifiedRow = true; + } + } + else // unmodified + { + if (null != updatedField.Data) + { + targetRow[i] = updatedField.Data; + } + } + } + + // modified rows and certain special rows go in the target and updated msi databases + if (modifiedRow || + ("Property" == table.Name && + ("ProductCode" == (string)row[0] || + "ProductLanguage" == (string)row[0] || + "ProductVersion" == (string)row[0] || + "UpgradeCode" == (string)row[0]))) + { + Table targetTable = targetOutput.EnsureTable(table.Definition); + targetTable.Rows.Add(targetRow); + + Table updatedTable = updatedOutput.EnsureTable(table.Definition); + updatedTable.Rows.Add(updatedRow); + } + } + } + + foreach (BinderExtension extension in this.Extensions) + { + extension.Finish(this.Transform); + } + + // Any errors encountered up to this point can cause errors during generation. + if (Messaging.Instance.EncounteredError) + { + return; + } + + string transformFileName = Path.GetFileNameWithoutExtension(this.OutputPath); + string targetDatabaseFile = Path.Combine(this.TempFilesLocation, String.Concat(transformFileName, "_target.msi")); + string updatedDatabaseFile = Path.Combine(this.TempFilesLocation, String.Concat(transformFileName, "_updated.msi")); + + try + { + if (!String.IsNullOrEmpty(emptyFile)) + { + using (FileStream fileStream = File.Create(emptyFile)) + { + } + } + + this.GenerateDatabase(targetOutput, targetDatabaseFile, false); + this.GenerateDatabase(updatedOutput, updatedDatabaseFile, true); + + // make sure the directory exists + Directory.CreateDirectory(Path.GetDirectoryName(this.OutputPath)); + + // create the transform file + using (Database targetDatabase = new Database(targetDatabaseFile, OpenDatabase.ReadOnly)) + { + using (Database updatedDatabase = new Database(updatedDatabaseFile, OpenDatabase.ReadOnly)) + { + if (updatedDatabase.GenerateTransform(targetDatabase, this.OutputPath)) + { + updatedDatabase.CreateTransformSummaryInfo(targetDatabase, this.OutputPath, (TransformErrorConditions)(transformFlags & 0xFFFF), (TransformValidations)((transformFlags >> 16) & 0xFFFF)); + } + else + { + Messaging.Instance.OnMessage(WixErrors.NoDifferencesInTransform(this.Transform.SourceLineNumbers)); + } + } + } + } + finally + { + if (!String.IsNullOrEmpty(emptyFile)) + { + File.Delete(emptyFile); + } + } + } + + private bool CompareFiles(string targetFile, string updatedFile) + { + bool? compared = null; + foreach (IBinderFileManager fileManager in this.FileManagers) + { + compared = fileManager.CompareFiles(targetFile, updatedFile); + if (compared.HasValue) + { + break; + } + } + + if (!compared.HasValue) + { + throw new InvalidOperationException(); // TODO: something needs to be said here that none of the binder file managers returned a result. + } + + return compared.Value; + } + + private void GenerateDatabase(Output output, string outputPath, bool keepAddedColumns) + { + GenerateDatabaseCommand command = new GenerateDatabaseCommand(); + command.Codepage = output.Codepage; + command.Extensions = this.Extensions; + command.FileManagers = this.FileManagers; + command.KeepAddedColumns = keepAddedColumns; + command.Output = output; + command.OutputPath = outputPath; + command.TableDefinitions = this.TableDefinitions; + command.TempFilesLocation = this.TempFilesLocation; + command.SuppressAddingValidationRows = true; + command.UseSubDirectory = true; + command.Execute(); + } + } +} diff --git a/src/WixToolset.Core/Bind/Bundles/AutomaticallySlipstreamPatchesCommand.cs b/src/WixToolset.Core/Bind/Bundles/AutomaticallySlipstreamPatchesCommand.cs new file mode 100644 index 00000000..eb02a983 --- /dev/null +++ b/src/WixToolset.Core/Bind/Bundles/AutomaticallySlipstreamPatchesCommand.cs @@ -0,0 +1,112 @@ +// 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. + +namespace WixToolset.Bind.Bundles +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using WixToolset.Data; + using WixToolset.Data.Rows; + + internal class AutomaticallySlipstreamPatchesCommand : ICommand + { + public IEnumerable PackageFacades { private get; set; } + + public Table WixBundlePatchTargetCodeTable { private get; set; } + + public Table SlipstreamMspTable { private get; set; } + + public void Execute() + { + List msiPackages = new List(); + Dictionary> targetsProductCode = new Dictionary>(); + Dictionary> targetsUpgradeCode = new Dictionary>(); + + foreach (PackageFacade facade in this.PackageFacades) + { + if (WixBundlePackageType.Msi == facade.Package.Type) + { + // Keep track of all MSI packages. + msiPackages.Add(facade.MsiPackage); + } + else if (WixBundlePackageType.Msp == facade.Package.Type && facade.MspPackage.Slipstream) + { + IEnumerable patchTargetCodeRows = this.WixBundlePatchTargetCodeTable.RowsAs().Where(r => r.MspPackageId == facade.Package.WixChainItemId); + + // Index target ProductCodes and UpgradeCodes for slipstreamed MSPs. + foreach (WixBundlePatchTargetCodeRow row in patchTargetCodeRows) + { + if (row.TargetsProductCode) + { + List rows; + if (!targetsProductCode.TryGetValue(row.TargetCode, out rows)) + { + rows = new List(); + targetsProductCode.Add(row.TargetCode, rows); + } + + rows.Add(row); + } + else if (row.TargetsUpgradeCode) + { + List rows; + if (!targetsUpgradeCode.TryGetValue(row.TargetCode, out rows)) + { + rows = new List(); + targetsUpgradeCode.Add(row.TargetCode, rows); + } + } + } + } + } + + RowIndexedList slipstreamMspRows = new RowIndexedList(SlipstreamMspTable); + + // Loop through the MSI and slipstream patches targeting it. + foreach (WixBundleMsiPackageRow msi in msiPackages) + { + List rows; + if (targetsProductCode.TryGetValue(msi.ProductCode, out rows)) + { + foreach (WixBundlePatchTargetCodeRow row in rows) + { + Debug.Assert(row.TargetsProductCode); + Debug.Assert(!row.TargetsUpgradeCode); + + Row slipstreamMspRow = SlipstreamMspTable.CreateRow(row.SourceLineNumbers, false); + slipstreamMspRow[0] = msi.ChainPackageId; + slipstreamMspRow[1] = row.MspPackageId; + + if (slipstreamMspRows.TryAdd(slipstreamMspRow)) + { + SlipstreamMspTable.Rows.Add(slipstreamMspRow); + } + } + + rows = null; + } + + if (!String.IsNullOrEmpty(msi.UpgradeCode) && targetsUpgradeCode.TryGetValue(msi.UpgradeCode, out rows)) + { + foreach (WixBundlePatchTargetCodeRow row in rows) + { + Debug.Assert(!row.TargetsProductCode); + Debug.Assert(row.TargetsUpgradeCode); + + Row slipstreamMspRow = SlipstreamMspTable.CreateRow(row.SourceLineNumbers, false); + slipstreamMspRow[0] = msi.ChainPackageId; + slipstreamMspRow[1] = row.MspPackageId; + + if (slipstreamMspRows.TryAdd(slipstreamMspRow)) + { + SlipstreamMspTable.Rows.Add(slipstreamMspRow); + } + } + + rows = null; + } + } + } + } +} diff --git a/src/WixToolset.Core/Bind/Bundles/BurnCommon.cs b/src/WixToolset.Core/Bind/Bundles/BurnCommon.cs new file mode 100644 index 00000000..8cb07791 --- /dev/null +++ b/src/WixToolset.Core/Bind/Bundles/BurnCommon.cs @@ -0,0 +1,378 @@ +// 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. + +namespace WixToolset.Bind.Bundles +{ + using System; + using System.Diagnostics; + using System.IO; + using WixToolset.Data; + + /// + /// Common functionality for Burn PE Writer & Reader for the WiX toolset. + /// + /// This class encapsulates common functionality related to + /// bundled/chained setup packages. + /// + /// + internal abstract class BurnCommon : IDisposable + { + public const string BurnNamespace = "http://wixtoolset.org/schemas/v4/2008/Burn"; + public const string BurnUXContainerEmbeddedIdFormat = "u{0}"; + public const string BurnUXContainerPayloadIdFormat = "p{0}"; + public const string BurnAttachedContainerEmbeddedIdFormat = "a{0}"; + + // See WinNT.h for details about the PE format, including the + // structure and offsets for IMAGE_DOS_HEADER, IMAGE_NT_HEADERS32, + // IMAGE_FILE_HEADER, etc. + protected const UInt32 IMAGE_DOS_HEADER_SIZE = 64; + protected const UInt32 IMAGE_DOS_HEADER_OFFSET_MAGIC = 0; + protected const UInt32 IMAGE_DOS_HEADER_OFFSET_NTHEADER = 60; + + protected const UInt32 IMAGE_NT_HEADER_SIZE = 24; // signature DWORD (4) + IMAGE_FILE_HEADER (20) + protected const UInt32 IMAGE_NT_HEADER_OFFSET_SIGNATURE = 0; + protected const UInt32 IMAGE_NT_HEADER_OFFSET_NUMBEROFSECTIONS = 6; + protected const UInt32 IMAGE_NT_HEADER_OFFSET_SIZEOFOPTIONALHEADER = 20; + + protected const UInt32 IMAGE_OPTIONAL_OFFSET_CHECKSUM = 4 * 16; // checksum is 16 DWORDs into IMAGE_OPTIONAL_HEADER which is right after the IMAGE_NT_HEADER. + protected const UInt32 IMAGE_OPTIONAL_NEGATIVE_OFFSET_CERTIFICATETABLE = (IMAGE_DATA_DIRECTORY_SIZE * (IMAGE_NUMBEROF_DIRECTORY_ENTRIES - IMAGE_DIRECTORY_ENTRY_SECURITY)); + + protected const UInt32 IMAGE_SECTION_HEADER_SIZE = 40; + protected const UInt32 IMAGE_SECTION_HEADER_OFFSET_NAME = 0; + protected const UInt32 IMAGE_SECTION_HEADER_OFFSET_VIRTUALSIZE = 8; + protected const UInt32 IMAGE_SECTION_HEADER_OFFSET_SIZEOFRAWDATA = 16; + protected const UInt32 IMAGE_SECTION_HEADER_OFFSET_POINTERTORAWDATA = 20; + + protected const UInt32 IMAGE_DATA_DIRECTORY_SIZE = 8; // struct of two DWORDs. + protected const UInt32 IMAGE_DIRECTORY_ENTRY_SECURITY = 4; + protected const UInt32 IMAGE_NUMBEROF_DIRECTORY_ENTRIES = 16; + + protected const UInt16 IMAGE_DOS_SIGNATURE = 0x5A4D; + protected const UInt32 IMAGE_NT_SIGNATURE = 0x00004550; + protected const UInt64 IMAGE_SECTION_WIXBURN_NAME = 0x6E7275627869772E; // ".wixburn", as a qword. + + // The ".wixburn" section contains: + // 0- 3: magic number + // 4- 7: version + // 8-23: bundle GUID + // 24-27: engine (stub) size + // 28-31: original checksum + // 32-35: original signature offset + // 36-39: original signature size + // 40-43: container type (1 = CAB) + // 44-47: container count + // 48-51: byte count of manifest + UX container + // 52-55: byte count of attached container + protected const UInt32 BURN_SECTION_OFFSET_MAGIC = 0; + protected const UInt32 BURN_SECTION_OFFSET_VERSION = 4; + protected const UInt32 BURN_SECTION_OFFSET_BUNDLEGUID = 8; + protected const UInt32 BURN_SECTION_OFFSET_STUBSIZE = 24; + protected const UInt32 BURN_SECTION_OFFSET_ORIGINALCHECKSUM = 28; + protected const UInt32 BURN_SECTION_OFFSET_ORIGINALSIGNATUREOFFSET = 32; + protected const UInt32 BURN_SECTION_OFFSET_ORIGINALSIGNATURESIZE = 36; + protected const UInt32 BURN_SECTION_OFFSET_FORMAT = 40; + protected const UInt32 BURN_SECTION_OFFSET_COUNT = 44; + protected const UInt32 BURN_SECTION_OFFSET_UXSIZE = 48; + protected const UInt32 BURN_SECTION_OFFSET_ATTACHEDCONTAINERSIZE = 52; + protected const UInt32 BURN_SECTION_SIZE = BURN_SECTION_OFFSET_ATTACHEDCONTAINERSIZE + 4; // last field + sizeof(DWORD) + + protected const UInt32 BURN_SECTION_MAGIC = 0x00f14300; + protected const UInt32 BURN_SECTION_VERSION = 0x00000002; + + protected string fileExe; + protected UInt32 peOffset = UInt32.MaxValue; + protected UInt16 sections = UInt16.MaxValue; + protected UInt32 firstSectionOffset = UInt32.MaxValue; + protected UInt32 checksumOffset; + protected UInt32 certificateTableSignatureOffset; + protected UInt32 certificateTableSignatureSize; + protected UInt32 wixburnDataOffset = UInt32.MaxValue; + + // TODO: does this enum exist in another form somewhere? + /// + /// The types of attached containers that BurnWriter supports. + /// + public enum Container + { + Nothing = 0, + UX, + Attached + } + + /// + /// Creates a BurnCommon for re-writing a PE file. + /// + /// File to modify in-place. + /// GUID for the bundle. + public BurnCommon(string fileExe) + { + this.fileExe = fileExe; + } + + public UInt32 Checksum { get; protected set; } + public UInt32 SignatureOffset { get; protected set; } + public UInt32 SignatureSize { get; protected set; } + public UInt32 Version { get; protected set; } + public UInt32 StubSize { get; protected set; } + public UInt32 OriginalChecksum { get; protected set; } + public UInt32 OriginalSignatureOffset { get; protected set; } + public UInt32 OriginalSignatureSize { get; protected set; } + public UInt32 EngineSize { get; protected set; } + public UInt32 ContainerCount { get; protected set; } + public UInt32 UXAddress { get; protected set; } + public UInt32 UXSize { get; protected set; } + public UInt32 AttachedContainerAddress { get; protected set; } + public UInt32 AttachedContainerSize { get; protected set; } + + public void Dispose() + { + Dispose(true); + + GC.SuppressFinalize(this); + } + + /// + /// Copies one stream to another. + /// + /// Input stream. + /// Output stream. + /// Optional count of bytes to copy. 0 indicates whole input stream from current should be copied. + protected static int CopyStream(Stream input, Stream output, int size) + { + byte[] bytes = new byte[4096]; + int total = 0; + int read = 0; + do + { + read = Math.Min(bytes.Length, size - total); + read = input.Read(bytes, 0, read); + if (0 == read) + { + break; + } + + output.Write(bytes, 0, read); + total += read; + } while (0 == size || total < size); + + return total; + } + + /// + /// Initialize the common information about a Burn engine. + /// + /// Binary reader open against a Burn engine. + /// True if initialized. + protected bool Initialize(BinaryReader reader) + { + if (!GetWixburnSectionInfo(reader)) + { + return false; + } + + reader.BaseStream.Seek(this.wixburnDataOffset, SeekOrigin.Begin); + byte[] bytes = reader.ReadBytes((int)BURN_SECTION_SIZE); + UInt32 uint32 = 0; + + uint32 = BurnCommon.ReadUInt32(bytes, BURN_SECTION_OFFSET_MAGIC); + if (BURN_SECTION_MAGIC != uint32) + { + Messaging.Instance.OnMessage(WixErrors.InvalidBundle(this.fileExe)); + return false; + } + + this.Version = BurnCommon.ReadUInt32(bytes, BURN_SECTION_OFFSET_VERSION); + if (BURN_SECTION_VERSION != this.Version) + { + Messaging.Instance.OnMessage(WixErrors.BundleTooNew(this.fileExe, this.Version)); + return false; + } + + uint32 = BurnCommon.ReadUInt32(bytes, BURN_SECTION_OFFSET_FORMAT); // We only know how to deal with CABs right now + if (1 != uint32) + { + Messaging.Instance.OnMessage(WixErrors.InvalidBundle(this.fileExe)); + return false; + } + + this.StubSize = BurnCommon.ReadUInt32(bytes, BURN_SECTION_OFFSET_STUBSIZE); + this.OriginalChecksum = BurnCommon.ReadUInt32(bytes, BURN_SECTION_OFFSET_ORIGINALCHECKSUM); + this.OriginalSignatureOffset = BurnCommon.ReadUInt32(bytes, BURN_SECTION_OFFSET_ORIGINALSIGNATUREOFFSET); + this.OriginalSignatureSize = BurnCommon.ReadUInt32(bytes, BURN_SECTION_OFFSET_ORIGINALSIGNATURESIZE); + + this.ContainerCount = BurnCommon.ReadUInt32(bytes, BURN_SECTION_OFFSET_COUNT); + this.UXAddress = this.StubSize; + this.UXSize = BurnCommon.ReadUInt32(bytes, BURN_SECTION_OFFSET_UXSIZE); + + // If there is an original signature use that to determine the engine size. + if (0 < this.OriginalSignatureOffset) + { + this.EngineSize = this.OriginalSignatureOffset + this.OriginalSignatureSize; + } + else if (0 < this.SignatureOffset && 2 > this.ContainerCount) // if there is a signature and no attached containers, use the current signature. + { + this.EngineSize = this.SignatureOffset + this.SignatureSize; + } + else // just use the stub and UX container as the size of the engine. + { + this.EngineSize = this.StubSize + this.UXSize; + } + + this.AttachedContainerAddress = this.ContainerCount > 1 ? this.EngineSize : 0; + this.AttachedContainerSize = this.ContainerCount > 1 ? BurnCommon.ReadUInt32(bytes, BURN_SECTION_OFFSET_ATTACHEDCONTAINERSIZE) : 0; + + return true; + } + + protected virtual void Dispose(bool disposing) + { + } + + /// + /// Finds the ".wixburn" section in the current exe. + /// + /// true if the ".wixburn" section is successfully found; false otherwise + private bool GetWixburnSectionInfo(BinaryReader reader) + { + if (UInt32.MaxValue == this.wixburnDataOffset) + { + if (!EnsureNTHeader(reader)) + { + return false; + } + + UInt32 wixburnSectionOffset = UInt32.MaxValue; + byte[] bytes = new byte[IMAGE_SECTION_HEADER_SIZE]; + + reader.BaseStream.Seek(this.firstSectionOffset, SeekOrigin.Begin); + for (UInt16 sectionIndex = 0; sectionIndex < this.sections; ++sectionIndex) + { + reader.Read(bytes, 0, bytes.Length); + + if (IMAGE_SECTION_WIXBURN_NAME == BurnCommon.ReadUInt64(bytes, IMAGE_SECTION_HEADER_OFFSET_NAME)) + { + wixburnSectionOffset = this.firstSectionOffset + (IMAGE_SECTION_HEADER_SIZE * sectionIndex); + break; + } + } + + if (UInt32.MaxValue == wixburnSectionOffset) + { + Messaging.Instance.OnMessage(WixErrors.StubMissingWixburnSection(this.fileExe)); + return false; + } + + // we need 56 bytes for the manifest header, which is always going to fit in + // the smallest alignment (512 bytes), but just to be paranoid... + if (BURN_SECTION_SIZE > BurnCommon.ReadUInt32(bytes, IMAGE_SECTION_HEADER_OFFSET_SIZEOFRAWDATA)) + { + Messaging.Instance.OnMessage(WixErrors.StubWixburnSectionTooSmall(this.fileExe)); + return false; + } + + this.wixburnDataOffset = BurnCommon.ReadUInt32(bytes, IMAGE_SECTION_HEADER_OFFSET_POINTERTORAWDATA); + } + + return true; + } + + /// + /// Checks for a valid Windows PE signature (IMAGE_NT_SIGNATURE) in the current exe. + /// + /// true if the exe is a Windows executable; false otherwise + private bool EnsureNTHeader(BinaryReader reader) + { + if (UInt32.MaxValue == this.firstSectionOffset) + { + if (!EnsureDosHeader(reader)) + { + return false; + } + + reader.BaseStream.Seek(this.peOffset, SeekOrigin.Begin); + byte[] bytes = reader.ReadBytes((int)IMAGE_NT_HEADER_SIZE); + + // Verify the NT signature... + if (IMAGE_NT_SIGNATURE != BurnCommon.ReadUInt32(bytes, IMAGE_NT_HEADER_OFFSET_SIGNATURE)) + { + Messaging.Instance.OnMessage(WixErrors.InvalidStubExe(this.fileExe)); + return false; + } + + ushort sizeOptionalHeader = BurnCommon.ReadUInt16(bytes, IMAGE_NT_HEADER_OFFSET_SIZEOFOPTIONALHEADER); + + this.sections = BurnCommon.ReadUInt16(bytes, IMAGE_NT_HEADER_OFFSET_NUMBEROFSECTIONS); + this.firstSectionOffset = this.peOffset + IMAGE_NT_HEADER_SIZE + sizeOptionalHeader; + + this.checksumOffset = this.peOffset + IMAGE_NT_HEADER_SIZE + IMAGE_OPTIONAL_OFFSET_CHECKSUM; + this.certificateTableSignatureOffset = this.peOffset + IMAGE_NT_HEADER_SIZE + sizeOptionalHeader - IMAGE_OPTIONAL_NEGATIVE_OFFSET_CERTIFICATETABLE; + this.certificateTableSignatureSize = this.certificateTableSignatureOffset + 4; // size is in the DWORD after the offset. + + bytes = reader.ReadBytes(sizeOptionalHeader); + this.Checksum = BurnCommon.ReadUInt32(bytes, IMAGE_OPTIONAL_OFFSET_CHECKSUM); + this.SignatureOffset = BurnCommon.ReadUInt32(bytes, sizeOptionalHeader - IMAGE_OPTIONAL_NEGATIVE_OFFSET_CERTIFICATETABLE); + this.SignatureSize = BurnCommon.ReadUInt32(bytes, sizeOptionalHeader - IMAGE_OPTIONAL_NEGATIVE_OFFSET_CERTIFICATETABLE + 4); + } + + return true; + } + + /// + /// Checks for a valid DOS header in the current exe. + /// + /// true if the exe starts with a DOS stub; false otherwise + private bool EnsureDosHeader(BinaryReader reader) + { + if (UInt32.MaxValue == this.peOffset) + { + byte[] bytes = reader.ReadBytes((int)IMAGE_DOS_HEADER_SIZE); + + // Verify the DOS 'MZ' signature. + if (IMAGE_DOS_SIGNATURE != BurnCommon.ReadUInt16(bytes, IMAGE_DOS_HEADER_OFFSET_MAGIC)) + { + Messaging.Instance.OnMessage(WixErrors.InvalidStubExe(this.fileExe)); + return false; + } + + this.peOffset = BurnCommon.ReadUInt32(bytes, IMAGE_DOS_HEADER_OFFSET_NTHEADER); + } + + return true; + } + + /// + /// Reads a UInt16 value in little-endian format from an offset in an array of bytes. + /// + /// Array from which to read. + /// Beginning offset from which to read. + /// value at offset + private static UInt16 ReadUInt16(byte[] bytes, UInt32 offset) + { + Debug.Assert(offset + 2 <= bytes.Length); + return (UInt16)(bytes[offset] + (bytes[offset + 1] << 8)); + } + + /// + /// Reads a UInt32 value in little-endian format from an offset in an array of bytes. + /// + /// Array from which to read. + /// Beginning offset from which to read. + /// value at offset + private static UInt32 ReadUInt32(byte[] bytes, UInt32 offset) + { + Debug.Assert(offset + 4 <= bytes.Length); + return (UInt32)(bytes[offset] + (bytes[offset + 1] << 8) + (bytes[offset + 2] << 16) + (bytes[offset + 3] << 24)); + } + + /// + /// Reads a UInt64 value in little-endian format from an offset in an array of bytes. + /// + /// Array from which to read. + /// Beginning offset from which to read. + /// value at offset + private static UInt64 ReadUInt64(byte[] bytes, UInt32 offset) + { + Debug.Assert(offset + 8 <= bytes.Length); + return BurnCommon.ReadUInt32(bytes, offset) + ((UInt64)(BurnCommon.ReadUInt32(bytes, offset + 4)) << 32); + } + } +} diff --git a/src/WixToolset.Core/Bind/Bundles/BurnReader.cs b/src/WixToolset.Core/Bind/Bundles/BurnReader.cs new file mode 100644 index 00000000..f6d7a197 --- /dev/null +++ b/src/WixToolset.Core/Bind/Bundles/BurnReader.cs @@ -0,0 +1,210 @@ +// 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. + +namespace WixToolset.Bind.Bundles +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.IO; + using System.Xml; + using WixToolset.Cab; + + /// + /// Burn PE reader for the WiX toolset. + /// + /// This class encapsulates reading from a stub EXE with containers attached + /// for dissecting bundled/chained setup packages. + /// + /// using (BurnReader reader = BurnReader.Open(fileExe, this.core, guid)) + /// { + /// reader.ExtractUXContainer(file1, tempFolder); + /// } + /// + internal class BurnReader : BurnCommon + { + private bool disposed; + + private bool invalidBundle; + private BinaryReader binaryReader; + private List attachedContainerPayloadNames; + + /// + /// Creates a BurnReader for reading a PE file. + /// + /// File to read. + private BurnReader(string fileExe) + : base(fileExe) + { + this.attachedContainerPayloadNames = new List(); + } + + /// + /// Gets the underlying stream. + /// + public Stream Stream + { + get + { + return (null != this.binaryReader) ? this.binaryReader.BaseStream : null; + } + } + + /// + /// Opens a Burn reader. + /// + /// Path to file. + /// Burn reader. + public static BurnReader Open(string fileExe) + { + BurnReader reader = new BurnReader(fileExe); + + reader.binaryReader = new BinaryReader(File.Open(fileExe, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete)); + if (!reader.Initialize(reader.binaryReader)) + { + reader.invalidBundle = true; + } + + return reader; + } + + /// + /// Gets the UX container from the exe and extracts its contents to the output directory. + /// + /// Directory to write extracted files to. + /// True if successful, false otherwise + public bool ExtractUXContainer(string outputDirectory, string tempDirectory) + { + // No UX container to extract + if (this.UXAddress == 0 || this.UXSize == 0) + { + return false; + } + + if (this.invalidBundle) + { + return false; + } + + Directory.CreateDirectory(outputDirectory); + string tempCabPath = Path.Combine(tempDirectory, "ux.cab"); + string manifestOriginalPath = Path.Combine(outputDirectory, "0"); + string manifestPath = Path.Combine(outputDirectory, "manifest.xml"); + + this.binaryReader.BaseStream.Seek(this.UXAddress, SeekOrigin.Begin); + using (Stream tempCab = File.Open(tempCabPath, FileMode.Create, FileAccess.Write)) + { + BurnCommon.CopyStream(this.binaryReader.BaseStream, tempCab, (int)this.UXSize); + } + + using (WixExtractCab extract = new WixExtractCab()) + { + extract.Extract(tempCabPath, outputDirectory); + } + + Directory.CreateDirectory(Path.GetDirectoryName(manifestPath)); + File.Delete(manifestPath); + File.Move(manifestOriginalPath, manifestPath); + + XmlDocument document = new XmlDocument(); + document.Load(manifestPath); + XmlNamespaceManager namespaceManager = new XmlNamespaceManager(document.NameTable); + namespaceManager.AddNamespace("burn", BurnCommon.BurnNamespace); + XmlNodeList uxPayloads = document.SelectNodes("/burn:BurnManifest/burn:UX/burn:Payload", namespaceManager); + XmlNodeList payloads = document.SelectNodes("/burn:BurnManifest/burn:Payload", namespaceManager); + + foreach (XmlNode uxPayload in uxPayloads) + { + XmlNode sourcePathNode = uxPayload.Attributes.GetNamedItem("SourcePath"); + XmlNode filePathNode = uxPayload.Attributes.GetNamedItem("FilePath"); + + string sourcePath = Path.Combine(outputDirectory, sourcePathNode.Value); + string destinationPath = Path.Combine(outputDirectory, filePathNode.Value); + + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)); + File.Delete(destinationPath); + File.Move(sourcePath, destinationPath); + } + + foreach (XmlNode payload in payloads) + { + XmlNode sourcePathNode = payload.Attributes.GetNamedItem("SourcePath"); + XmlNode filePathNode = payload.Attributes.GetNamedItem("FilePath"); + XmlNode packagingNode = payload.Attributes.GetNamedItem("Packaging"); + + string sourcePath = sourcePathNode.Value; + string destinationPath = filePathNode.Value; + string packaging = packagingNode.Value; + + if (packaging.Equals("embedded", StringComparison.OrdinalIgnoreCase)) + { + this.attachedContainerPayloadNames.Add(new DictionaryEntry(sourcePath, destinationPath)); + } + } + + return true; + } + + /// + /// Gets the attached container from the exe and extracts its contents to the output directory. + /// + /// Directory to write extracted files to. + /// True if successful, false otherwise + public bool ExtractAttachedContainer(string outputDirectory, string tempDirectory) + { + // No attached container to extract + if (this.AttachedContainerAddress == 0 || this.AttachedContainerSize == 0) + { + return false; + } + + if (this.invalidBundle) + { + return false; + } + + Directory.CreateDirectory(outputDirectory); + string tempCabPath = Path.Combine(tempDirectory, "attached.cab"); + + this.binaryReader.BaseStream.Seek(this.AttachedContainerAddress, SeekOrigin.Begin); + using (Stream tempCab = File.Open(tempCabPath, FileMode.Create, FileAccess.Write)) + { + BurnCommon.CopyStream(this.binaryReader.BaseStream, tempCab, (int)this.AttachedContainerSize); + } + + using (WixExtractCab extract = new WixExtractCab()) + { + extract.Extract(tempCabPath, outputDirectory); + } + + foreach (DictionaryEntry entry in this.attachedContainerPayloadNames) + { + string sourcePath = Path.Combine(outputDirectory, (string)entry.Key); + string destinationPath = Path.Combine(outputDirectory, (string)entry.Value); + + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)); + File.Delete(destinationPath); + File.Move(sourcePath, destinationPath); + } + + return true; + } + + /// + /// Dispose object. + /// + /// True when releasing managed objects. + protected override void Dispose(bool disposing) + { + if (!this.disposed) + { + if (disposing && this.binaryReader != null) + { + this.binaryReader.Close(); + this.binaryReader = null; + } + + this.disposed = true; + } + } + } +} diff --git a/src/WixToolset.Core/Bind/Bundles/BurnWriter.cs b/src/WixToolset.Core/Bind/Bundles/BurnWriter.cs new file mode 100644 index 00000000..bc0baf46 --- /dev/null +++ b/src/WixToolset.Core/Bind/Bundles/BurnWriter.cs @@ -0,0 +1,239 @@ +// 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. + +namespace WixToolset.Bind.Bundles +{ + using System; + using System.Diagnostics; + using System.IO; + using WixToolset.Data; + + /// + /// Burn PE writer for the WiX toolset. + /// + /// This class encapsulates reading/writing to a stub EXE for + /// creating bundled/chained setup packages. + /// + /// using (BurnWriter writer = new BurnWriter(fileExe, this.core, guid)) + /// { + /// writer.AppendContainer(file1, BurnWriter.Container.UX); + /// writer.AppendContainer(file2, BurnWriter.Container.Attached); + /// } + /// + internal class BurnWriter : BurnCommon + { + private bool disposed; + private bool invalidBundle; + private BinaryWriter binaryWriter; + + /// + /// Creates a BurnWriter for re-writing a PE file. + /// + /// File to modify in-place. + /// GUID for the bundle. + private BurnWriter(string fileExe) + : base(fileExe) + { + } + + /// + /// Opens a Burn writer. + /// + /// Path to file. + /// Burn writer. + public static BurnWriter Open(string fileExe) + { + BurnWriter writer = new BurnWriter(fileExe); + + using (BinaryReader binaryReader = new BinaryReader(File.Open(fileExe, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete))) + { + if (!writer.Initialize(binaryReader)) + { + writer.invalidBundle = true; + } + } + + if (!writer.invalidBundle) + { + writer.binaryWriter = new BinaryWriter(File.Open(fileExe, FileMode.Open, FileAccess.ReadWrite, FileShare.Read | FileShare.Delete)); + } + + return writer; + } + + /// + /// Update the ".wixburn" section data. + /// + /// Size of the stub engine "burn.exe". + /// Unique identifier for this bundle. + /// + public bool InitializeBundleSectionData(long stubSize, Guid bundleId) + { + if (this.invalidBundle) + { + return false; + } + + this.WriteToBurnSectionOffset(BURN_SECTION_OFFSET_MAGIC, BURN_SECTION_MAGIC); + this.WriteToBurnSectionOffset(BURN_SECTION_OFFSET_VERSION, BURN_SECTION_VERSION); + + Messaging.Instance.OnMessage(WixVerboses.BundleGuid(bundleId.ToString("B"))); + this.binaryWriter.BaseStream.Seek(this.wixburnDataOffset + BURN_SECTION_OFFSET_BUNDLEGUID, SeekOrigin.Begin); + this.binaryWriter.Write(bundleId.ToByteArray()); + + this.StubSize = (uint)stubSize; + + this.WriteToBurnSectionOffset(BURN_SECTION_OFFSET_STUBSIZE, this.StubSize); + this.WriteToBurnSectionOffset(BURN_SECTION_OFFSET_ORIGINALCHECKSUM, 0); + this.WriteToBurnSectionOffset(BURN_SECTION_OFFSET_ORIGINALSIGNATUREOFFSET, 0); + this.WriteToBurnSectionOffset(BURN_SECTION_OFFSET_ORIGINALSIGNATURESIZE, 0); + this.WriteToBurnSectionOffset(BURN_SECTION_OFFSET_FORMAT, 1); // Hard-coded to CAB for now. + this.WriteToBurnSectionOffset(BURN_SECTION_OFFSET_COUNT, 0); + this.WriteToBurnSectionOffset(BURN_SECTION_OFFSET_UXSIZE, 0); + this.WriteToBurnSectionOffset(BURN_SECTION_OFFSET_ATTACHEDCONTAINERSIZE, 0); + this.binaryWriter.BaseStream.Flush(); + + this.EngineSize = this.StubSize; + + return true; + } + + /// + /// Appends a UX or Attached container to the exe and updates the ".wixburn" section data to point to it. + /// + /// File path to append to the current exe. + /// Container section represented by the fileContainer. + /// true if the container data is successfully appended; false otherwise + public bool AppendContainer(string fileContainer, BurnCommon.Container container) + { + using (FileStream reader = File.OpenRead(fileContainer)) + { + return this.AppendContainer(reader, reader.Length, container); + } + } + + /// + /// Appends a UX or Attached container to the exe and updates the ".wixburn" section data to point to it. + /// + /// File stream to append to the current exe. + /// Size of container to append. + /// Container section represented by the fileContainer. + /// true if the container data is successfully appended; false otherwise + public bool AppendContainer(Stream containerStream, long containerSize, BurnCommon.Container container) + { + UInt32 burnSectionCount = 0; + UInt32 burnSectionOffsetSize = 0; + + switch (container) + { + case Container.UX: + burnSectionCount = 1; + burnSectionOffsetSize = BURN_SECTION_OFFSET_UXSIZE; + // TODO: verify that the size in the section data is 0 or the same size. + this.EngineSize += (uint)containerSize; + this.UXSize = (uint)containerSize; + break; + + case Container.Attached: + burnSectionCount = 2; + burnSectionOffsetSize = BURN_SECTION_OFFSET_ATTACHEDCONTAINERSIZE; + // TODO: verify that the size in the section data is 0 or the same size. + this.AttachedContainerSize = (uint)containerSize; + break; + + default: + Debug.Assert(false); + return false; + } + + return AppendContainer(containerStream, (UInt32)containerSize, burnSectionOffsetSize, burnSectionCount); + } + + public void RememberThenResetSignature() + { + if (this.invalidBundle) + { + return; + } + + this.OriginalChecksum = this.Checksum; + this.OriginalSignatureOffset = this.SignatureOffset; + this.OriginalSignatureSize = this.SignatureSize; + + this.WriteToBurnSectionOffset(BURN_SECTION_OFFSET_ORIGINALCHECKSUM, this.OriginalChecksum); + this.WriteToBurnSectionOffset(BURN_SECTION_OFFSET_ORIGINALSIGNATUREOFFSET, this.OriginalSignatureOffset); + this.WriteToBurnSectionOffset(BURN_SECTION_OFFSET_ORIGINALSIGNATURESIZE, this.OriginalSignatureSize); + + this.Checksum = 0; + this.SignatureOffset = 0; + this.SignatureSize = 0; + + this.WriteToOffset(this.checksumOffset, this.Checksum); + this.WriteToOffset(this.certificateTableSignatureOffset, this.SignatureOffset); + this.WriteToOffset(this.certificateTableSignatureSize, this.SignatureSize); + } + + /// + /// Dispose object. + /// + /// True when releasing managed objects. + protected override void Dispose(bool disposing) + { + if (!this.disposed) + { + if (disposing && this.binaryWriter != null) + { + this.binaryWriter.Close(); + this.binaryWriter = null; + } + + this.disposed = true; + } + } + + /// + /// Appends a container to the exe and updates the ".wixburn" section data to point to it. + /// + /// File stream to append to the current exe. + /// Offset of size field for this container in ".wixburn" section data. + /// true if the container data is successfully appended; false otherwise + private bool AppendContainer(Stream containerStream, UInt32 containerSize, UInt32 burnSectionOffsetSize, UInt32 burnSectionCount) + { + if (this.invalidBundle) + { + return false; + } + + // Update the ".wixburn" section data + this.WriteToBurnSectionOffset(BURN_SECTION_OFFSET_COUNT, burnSectionCount); + this.WriteToBurnSectionOffset(burnSectionOffsetSize, containerSize); + + // Append the container to the end of the existing bits. + this.binaryWriter.BaseStream.Seek(0, SeekOrigin.End); + BurnCommon.CopyStream(containerStream, this.binaryWriter.BaseStream, (int)containerSize); + this.binaryWriter.BaseStream.Flush(); + + return true; + } + + /// + /// Writes the value to an offset in the Burn section data. + /// + /// Offset in to the Burn section data. + /// Value to write. + private void WriteToBurnSectionOffset(uint offset, uint value) + { + this.WriteToOffset(this.wixburnDataOffset + offset, value); + } + + /// + /// Writes the value to an offset in the Burn stub. + /// + /// Offset in to the Burn stub. + /// Value to write. + private void WriteToOffset(uint offset, uint value) + { + this.binaryWriter.BaseStream.Seek((int)offset, SeekOrigin.Begin); + this.binaryWriter.Write(value); + } + } +} diff --git a/src/WixToolset.Core/Bind/Bundles/CreateBootstrapperApplicationManifestCommand.cs b/src/WixToolset.Core/Bind/Bundles/CreateBootstrapperApplicationManifestCommand.cs new file mode 100644 index 00000000..1040b394 --- /dev/null +++ b/src/WixToolset.Core/Bind/Bundles/CreateBootstrapperApplicationManifestCommand.cs @@ -0,0 +1,241 @@ +// 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. + +namespace WixToolset.Bind.Bundles +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Globalization; + using System.IO; + using System.Text; + using System.Xml; + using WixToolset.Data; + using WixToolset.Data.Rows; + + internal class CreateBootstrapperApplicationManifestCommand : ICommand + { + public WixBundleRow BundleRow { private get; set; } + + public IEnumerable ChainPackages { private get; set; } + + public int LastUXPayloadIndex { private get; set; } + + public IEnumerable MsiFeatures { private get; set; } + + public Output Output { private get; set; } + + public RowDictionary Payloads { private get; set; } + + public TableDefinitionCollection TableDefinitions { private get; set; } + + public string TempFilesLocation { private get; set; } + + public WixBundlePayloadRow BootstrapperApplicationManifestPayloadRow { get; private set; } + + public void Execute() + { + this.GenerateBAManifestBundleTables(); + + this.GenerateBAManifestMsiFeatureTables(); + + this.GenerateBAManifestPackageTables(); + + this.GenerateBAManifestPayloadTables(); + + string baManifestPath = Path.Combine(this.TempFilesLocation, "wix-badata.xml"); + + this.CreateBootstrapperApplicationManifest(baManifestPath); + + this.BootstrapperApplicationManifestPayloadRow = this.CreateBootstrapperApplicationManifestPayloadRow(baManifestPath); + } + + private void GenerateBAManifestBundleTables() + { + Table wixBundlePropertiesTable = this.Output.EnsureTable(this.TableDefinitions["WixBundleProperties"]); + + Row row = wixBundlePropertiesTable.CreateRow(this.BundleRow.SourceLineNumbers); + row[0] = this.BundleRow.Name; + row[1] = this.BundleRow.LogPathVariable; + row[2] = (YesNoDefaultType.Yes == this.BundleRow.Compressed) ? "yes" : "no"; + row[3] = this.BundleRow.BundleId.ToString("B"); + row[4] = this.BundleRow.UpgradeCode; + row[5] = this.BundleRow.PerMachine ? "yes" : "no"; + } + + private void GenerateBAManifestPackageTables() + { + Table wixPackagePropertiesTable = this.Output.EnsureTable(this.TableDefinitions["WixPackageProperties"]); + + foreach (PackageFacade package in this.ChainPackages) + { + WixBundlePayloadRow packagePayload = this.Payloads[package.Package.PackagePayload]; + + Row row = wixPackagePropertiesTable.CreateRow(package.Package.SourceLineNumbers); + row[0] = package.Package.WixChainItemId; + row[1] = (YesNoType.Yes == package.Package.Vital) ? "yes" : "no"; + row[2] = package.Package.DisplayName; + row[3] = package.Package.Description; + row[4] = package.Package.Size.ToString(CultureInfo.InvariantCulture); // TODO: DownloadSize (compressed) (what does this mean when it's embedded?) + row[5] = package.Package.Size.ToString(CultureInfo.InvariantCulture); // Package.Size (uncompressed) + row[6] = package.Package.InstallSize.Value.ToString(CultureInfo.InvariantCulture); // InstallSize (required disk space) + row[7] = package.Package.Type.ToString(); + row[8] = package.Package.Permanent ? "yes" : "no"; + row[9] = package.Package.LogPathVariable; + row[10] = package.Package.RollbackLogPathVariable; + row[11] = (PackagingType.Embedded == packagePayload.Packaging) ? "yes" : "no"; + + if (WixBundlePackageType.Msi == package.Package.Type) + { + row[12] = package.MsiPackage.DisplayInternalUI ? "yes" : "no"; + + if (!String.IsNullOrEmpty(package.MsiPackage.ProductCode)) + { + row[13] = package.MsiPackage.ProductCode; + } + + if (!String.IsNullOrEmpty(package.MsiPackage.UpgradeCode)) + { + row[14] = package.MsiPackage.UpgradeCode; + } + } + else if (WixBundlePackageType.Msp == package.Package.Type) + { + row[12] = package.MspPackage.DisplayInternalUI ? "yes" : "no"; + + if (!String.IsNullOrEmpty(package.MspPackage.PatchCode)) + { + row[13] = package.MspPackage.PatchCode; + } + } + + if (!String.IsNullOrEmpty(package.Package.Version)) + { + row[15] = package.Package.Version; + } + + if (!String.IsNullOrEmpty(package.Package.InstallCondition)) + { + row[16] = package.Package.InstallCondition; + } + + switch (package.Package.Cache) + { + case YesNoAlwaysType.No: + row[17] = "no"; + break; + case YesNoAlwaysType.Yes: + row[17] = "yes"; + break; + case YesNoAlwaysType.Always: + row[17] = "always"; + break; + } + } + } + + private void GenerateBAManifestMsiFeatureTables() + { + Table wixPackageFeatureInfoTable = this.Output.EnsureTable(this.TableDefinitions["WixPackageFeatureInfo"]); + + foreach (WixBundleMsiFeatureRow feature in this.MsiFeatures) + { + Row row = wixPackageFeatureInfoTable.CreateRow(feature.SourceLineNumbers); + row[0] = feature.ChainPackageId; + row[1] = feature.Name; + row[2] = Convert.ToString(feature.Size, CultureInfo.InvariantCulture); + row[3] = feature.Parent; + row[4] = feature.Title; + row[5] = feature.Description; + row[6] = Convert.ToString(feature.Display, CultureInfo.InvariantCulture); + row[7] = Convert.ToString(feature.Level, CultureInfo.InvariantCulture); + row[8] = feature.Directory; + row[9] = Convert.ToString(feature.Attributes, CultureInfo.InvariantCulture); + } + + } + + private void GenerateBAManifestPayloadTables() + { + Table wixPayloadPropertiesTable = this.Output.EnsureTable(this.TableDefinitions["WixPayloadProperties"]); + + foreach (WixBundlePayloadRow payload in this.Payloads.Values) + { + WixPayloadPropertiesRow row = (WixPayloadPropertiesRow)wixPayloadPropertiesTable.CreateRow(payload.SourceLineNumbers); + row.Id = payload.Id; + row.Package = payload.Package; + row.Container = payload.Container; + row.Name = payload.Name; + row.Size = payload.FileSize.ToString(); + row.DownloadUrl = payload.DownloadUrl; + row.LayoutOnly = payload.LayoutOnly ? "yes" : "no"; + } + } + + private void CreateBootstrapperApplicationManifest(string path) + { + using (XmlTextWriter writer = new XmlTextWriter(path, Encoding.Unicode)) + { + writer.Formatting = Formatting.Indented; + writer.WriteStartDocument(); + writer.WriteStartElement("BootstrapperApplicationData", "http://wixtoolset.org/schemas/v4/2010/BootstrapperApplicationData"); + + foreach (Table table in this.Output.Tables) + { + if (table.Definition.BootstrapperApplicationData) + { + // We simply assert that the table (and field) name is valid, because + // this is up to the extension developer to get right. An author will + // only affect the attribute value, and that will get properly escaped. +#if DEBUG + Debug.Assert(Common.IsIdentifier(table.Name)); + foreach (ColumnDefinition column in table.Definition.Columns) + { + Debug.Assert(Common.IsIdentifier(column.Name)); + } +#endif // DEBUG + + foreach (Row row in table.Rows) + { + writer.WriteStartElement(table.Name); + + foreach (Field field in row.Fields) + { + if (null != field.Data) + { + writer.WriteAttributeString(field.Column.Name, field.Data.ToString()); + } + } + + writer.WriteEndElement(); + } + } + } + + writer.WriteEndElement(); + writer.WriteEndDocument(); + } + } + + private WixBundlePayloadRow CreateBootstrapperApplicationManifestPayloadRow(string baManifestPath) + { + Table payloadTable = this.Output.EnsureTable(this.TableDefinitions["WixBundlePayload"]); + WixBundlePayloadRow row = (WixBundlePayloadRow)payloadTable.CreateRow(this.BundleRow.SourceLineNumbers); + row.Id = Common.GenerateIdentifier("ux", "BootstrapperApplicationData.xml"); + row.Name = "BootstrapperApplicationData.xml"; + row.SourceFile = baManifestPath; + row.Compressed = YesNoDefaultType.Yes; + row.UnresolvedSourceFile = baManifestPath; + row.Container = Compiler.BurnUXContainerId; + row.EmbeddedId = String.Format(CultureInfo.InvariantCulture, BurnCommon.BurnUXContainerEmbeddedIdFormat, this.LastUXPayloadIndex); + row.Packaging = PackagingType.Embedded; + + FileInfo fileInfo = new FileInfo(row.SourceFile); + + row.FileSize = (int)fileInfo.Length; + + row.Hash = Common.GetFileHash(fileInfo.FullName); + + return row; + } + } +} diff --git a/src/WixToolset.Core/Bind/Bundles/CreateBurnManifestCommand.cs b/src/WixToolset.Core/Bind/Bundles/CreateBurnManifestCommand.cs new file mode 100644 index 00000000..7bc708a3 --- /dev/null +++ b/src/WixToolset.Core/Bind/Bundles/CreateBurnManifestCommand.cs @@ -0,0 +1,686 @@ +// 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. + +namespace WixToolset.Bind.Bundles +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Globalization; + using System.Linq; + using System.Text; + using System.Xml; + using WixToolset.Data; + using WixToolset.Data.Rows; + using WixToolset.Extensibility; + + internal class CreateBurnManifestCommand : ICommand + { + public IEnumerable FileManagers { private get; set; } + + public Output Output { private get; set; } + + public string ExecutableName { private get; set; } + + public WixBundleRow BundleInfo { private get; set; } + + public WixChainRow Chain { private get; set; } + + public string OutputPath { private get; set; } + + public IEnumerable RollbackBoundaries { private get; set; } + + public IEnumerable OrderedPackages { private get; set; } + + public IEnumerable OrderedSearches { private get; set; } + + public Dictionary Payloads { private get; set; } + + public Dictionary Containers { private get; set; } + + public IEnumerable UXContainerPayloads { private get; set; } + + public IEnumerable Catalogs { private get; set; } + + public void Execute() + { + using (XmlTextWriter writer = new XmlTextWriter(this.OutputPath, Encoding.UTF8)) + { + writer.WriteStartDocument(); + + writer.WriteStartElement("BurnManifest", BurnCommon.BurnNamespace); + + // Write the condition, if there is one + if (null != this.BundleInfo.Condition) + { + writer.WriteElementString("Condition", this.BundleInfo.Condition); + } + + // Write the log element if default logging wasn't disabled. + if (!String.IsNullOrEmpty(this.BundleInfo.LogPrefix)) + { + writer.WriteStartElement("Log"); + if (!String.IsNullOrEmpty(this.BundleInfo.LogPathVariable)) + { + writer.WriteAttributeString("PathVariable", this.BundleInfo.LogPathVariable); + } + writer.WriteAttributeString("Prefix", this.BundleInfo.LogPrefix); + writer.WriteAttributeString("Extension", this.BundleInfo.LogExtension); + writer.WriteEndElement(); + } + + + // Get update if specified. + WixBundleUpdateRow updateRow = this.Output.Tables["WixBundleUpdate"].RowsAs().FirstOrDefault(); + + if (null != updateRow) + { + writer.WriteStartElement("Update"); + writer.WriteAttributeString("Location", updateRow.Location); + writer.WriteEndElement(); // + } + + // Write the RelatedBundle elements + + // For the related bundles with duplicated identifiers the second instance is ignored (i.e. the Duplicates + // enumeration in the index row list is not used). + RowIndexedList relatedBundles = new RowIndexedList(this.Output.Tables["WixRelatedBundle"]); + + foreach (WixRelatedBundleRow relatedBundle in relatedBundles) + { + writer.WriteStartElement("RelatedBundle"); + writer.WriteAttributeString("Id", relatedBundle.Id); + writer.WriteAttributeString("Action", Convert.ToString(relatedBundle.Action, CultureInfo.InvariantCulture)); + writer.WriteEndElement(); + } + + // Write the variables + IEnumerable variables = this.Output.Tables["WixBundleVariable"].RowsAs(); + + foreach (WixBundleVariableRow variable in variables) + { + writer.WriteStartElement("Variable"); + writer.WriteAttributeString("Id", variable.Id); + if (null != variable.Type) + { + writer.WriteAttributeString("Value", variable.Value); + writer.WriteAttributeString("Type", variable.Type); + } + writer.WriteAttributeString("Hidden", variable.Hidden ? "yes" : "no"); + writer.WriteAttributeString("Persisted", variable.Persisted ? "yes" : "no"); + writer.WriteEndElement(); + } + + // Write the searches + foreach (WixSearchInfo searchinfo in this.OrderedSearches) + { + searchinfo.WriteXml(writer); + } + + // write the UX element + writer.WriteStartElement("UX"); + if (!String.IsNullOrEmpty(this.BundleInfo.SplashScreenBitmapPath)) + { + writer.WriteAttributeString("SplashScreen", "yes"); + } + + // write the UX allPayloads... + foreach (WixBundlePayloadRow payload in this.UXContainerPayloads) + { + writer.WriteStartElement("Payload"); + this.WriteBurnManifestPayloadAttributes(writer, payload, true, this.Payloads); + writer.WriteEndElement(); + } + + writer.WriteEndElement(); // + + // write the catalog elements + if (this.Catalogs.Any()) + { + foreach (WixBundleCatalogRow catalog in this.Catalogs) + { + writer.WriteStartElement("Catalog"); + writer.WriteAttributeString("Id", catalog.Id); + writer.WriteAttributeString("Payload", catalog.Payload); + writer.WriteEndElement(); + } + } + + foreach (WixBundleContainerRow container in this.Containers.Values) + { + if (!String.IsNullOrEmpty(container.WorkingPath) && Compiler.BurnUXContainerId != container.Id) + { + writer.WriteStartElement("Container"); + this.WriteBurnManifestContainerAttributes(writer, this.ExecutableName, container); + writer.WriteEndElement(); + } + } + + foreach (WixBundlePayloadRow payload in this.Payloads.Values) + { + if (PackagingType.Embedded == payload.Packaging && Compiler.BurnUXContainerId != payload.Container) + { + writer.WriteStartElement("Payload"); + this.WriteBurnManifestPayloadAttributes(writer, payload, true, this.Payloads); + writer.WriteEndElement(); + } + else if (PackagingType.External == payload.Packaging) + { + writer.WriteStartElement("Payload"); + this.WriteBurnManifestPayloadAttributes(writer, payload, false, this.Payloads); + writer.WriteEndElement(); + } + } + + foreach (WixBundleRollbackBoundaryRow rollbackBoundary in this.RollbackBoundaries) + { + writer.WriteStartElement("RollbackBoundary"); + writer.WriteAttributeString("Id", rollbackBoundary.ChainPackageId); + writer.WriteAttributeString("Vital", YesNoType.Yes == rollbackBoundary.Vital ? "yes" : "no"); + writer.WriteAttributeString("Transaction", YesNoType.Yes == rollbackBoundary.Transaction ? "yes" : "no"); + writer.WriteEndElement(); + } + + // Write the registration information... + writer.WriteStartElement("Registration"); + + writer.WriteAttributeString("Id", this.BundleInfo.BundleId.ToString("B")); + writer.WriteAttributeString("ExecutableName", this.ExecutableName); + writer.WriteAttributeString("PerMachine", this.BundleInfo.PerMachine ? "yes" : "no"); + writer.WriteAttributeString("Tag", this.BundleInfo.Tag); + writer.WriteAttributeString("Version", this.BundleInfo.Version); + writer.WriteAttributeString("ProviderKey", this.BundleInfo.ProviderKey); + + writer.WriteStartElement("Arp"); + writer.WriteAttributeString("Register", (0 < this.BundleInfo.DisableModify && this.BundleInfo.DisableRemove) ? "no" : "yes"); // do not register if disabled modify and remove. + writer.WriteAttributeString("DisplayName", this.BundleInfo.Name); + writer.WriteAttributeString("DisplayVersion", this.BundleInfo.Version); + + if (!String.IsNullOrEmpty(this.BundleInfo.Publisher)) + { + writer.WriteAttributeString("Publisher", this.BundleInfo.Publisher); + } + + if (!String.IsNullOrEmpty(this.BundleInfo.HelpLink)) + { + writer.WriteAttributeString("HelpLink", this.BundleInfo.HelpLink); + } + + if (!String.IsNullOrEmpty(this.BundleInfo.HelpTelephone)) + { + writer.WriteAttributeString("HelpTelephone", this.BundleInfo.HelpTelephone); + } + + if (!String.IsNullOrEmpty(this.BundleInfo.AboutUrl)) + { + writer.WriteAttributeString("AboutUrl", this.BundleInfo.AboutUrl); + } + + if (!String.IsNullOrEmpty(this.BundleInfo.UpdateUrl)) + { + writer.WriteAttributeString("UpdateUrl", this.BundleInfo.UpdateUrl); + } + + if (!String.IsNullOrEmpty(this.BundleInfo.ParentName)) + { + writer.WriteAttributeString("ParentDisplayName", this.BundleInfo.ParentName); + } + + if (1 == this.BundleInfo.DisableModify) + { + writer.WriteAttributeString("DisableModify", "yes"); + } + else if (2 == this.BundleInfo.DisableModify) + { + writer.WriteAttributeString("DisableModify", "button"); + } + + if (this.BundleInfo.DisableRemove) + { + writer.WriteAttributeString("DisableRemove", "yes"); + } + writer.WriteEndElement(); // + + // Get update registration if specified. + WixUpdateRegistrationRow updateRegistrationInfo = this.Output.Tables["WixUpdateRegistration"].RowsAs().FirstOrDefault(); + + if (null != updateRegistrationInfo) + { + writer.WriteStartElement("Update"); // + writer.WriteAttributeString("Manufacturer", updateRegistrationInfo.Manufacturer); + + if (!String.IsNullOrEmpty(updateRegistrationInfo.Department)) + { + writer.WriteAttributeString("Department", updateRegistrationInfo.Department); + } + + if (!String.IsNullOrEmpty(updateRegistrationInfo.ProductFamily)) + { + writer.WriteAttributeString("ProductFamily", updateRegistrationInfo.ProductFamily); + } + + writer.WriteAttributeString("Name", updateRegistrationInfo.Name); + writer.WriteAttributeString("Classification", updateRegistrationInfo.Classification); + writer.WriteEndElement(); // + } + + IEnumerable bundleTags = this.Output.Tables["WixBundleTag"].RowsAs(); + + foreach (Row row in bundleTags) + { + writer.WriteStartElement("SoftwareTag"); + writer.WriteAttributeString("Filename", (string)row[0]); + writer.WriteAttributeString("Regid", (string)row[1]); + writer.WriteCData((string)row[4]); + writer.WriteEndElement(); + } + + writer.WriteEndElement(); // + + // write the Chain... + writer.WriteStartElement("Chain"); + if (this.Chain.DisableRollback) + { + writer.WriteAttributeString("DisableRollback", "yes"); + } + + if (this.Chain.DisableSystemRestore) + { + writer.WriteAttributeString("DisableSystemRestore", "yes"); + } + + if (this.Chain.ParallelCache) + { + writer.WriteAttributeString("ParallelCache", "yes"); + } + + // Index a few tables by package. + ILookup targetCodesByPatch = this.Output.Tables["WixBundlePatchTargetCode"].RowsAs().ToLookup(r => r.MspPackageId); + ILookup msiFeaturesByPackage = this.Output.Tables["WixBundleMsiFeature"].RowsAs().ToLookup(r => r.ChainPackageId); + ILookup msiPropertiesByPackage = this.Output.Tables["WixBundleMsiProperty"].RowsAs().ToLookup(r => r.ChainPackageId); + ILookup payloadsByPackage = this.Payloads.Values.ToLookup(p => p.Package); + ILookup relatedPackagesByPackage = this.Output.Tables["WixBundleRelatedPackage"].RowsAs().ToLookup(r => r.ChainPackageId); + ILookup slipstreamMspsByPackage = this.Output.Tables["WixBundleSlipstreamMsp"].RowsAs().ToLookup(r => r.ChainPackageId); + ILookup exitCodesByPackage = this.Output.Tables["WixBundlePackageExitCode"].RowsAs().ToLookup(r => r.ChainPackageId); + ILookup commandLinesByPackage = this.Output.Tables["WixBundlePackageCommandLine"].RowsAs().ToLookup(r => r.ChainPackageId); + + // Build up the list of target codes from all the MSPs in the chain. + List targetCodes = new List(); + + foreach (PackageFacade package in this.OrderedPackages) + { + writer.WriteStartElement(String.Format(CultureInfo.InvariantCulture, "{0}Package", package.Package.Type)); + + writer.WriteAttributeString("Id", package.Package.WixChainItemId); + + switch (package.Package.Cache) + { + case YesNoAlwaysType.No: + writer.WriteAttributeString("Cache", "no"); + break; + case YesNoAlwaysType.Yes: + writer.WriteAttributeString("Cache", "yes"); + break; + case YesNoAlwaysType.Always: + writer.WriteAttributeString("Cache", "always"); + break; + } + + writer.WriteAttributeString("CacheId", package.Package.CacheId); + writer.WriteAttributeString("InstallSize", Convert.ToString(package.Package.InstallSize)); + writer.WriteAttributeString("Size", Convert.ToString(package.Package.Size)); + writer.WriteAttributeString("PerMachine", YesNoDefaultType.Yes == package.Package.PerMachine ? "yes" : "no"); + writer.WriteAttributeString("Permanent", package.Package.Permanent ? "yes" : "no"); + writer.WriteAttributeString("Vital", (YesNoType.Yes == package.Package.Vital) ? "yes" : "no"); + + if (null != package.Package.RollbackBoundary) + { + writer.WriteAttributeString("RollbackBoundaryForward", package.Package.RollbackBoundary); + } + + if (!String.IsNullOrEmpty(package.Package.RollbackBoundaryBackward)) + { + writer.WriteAttributeString("RollbackBoundaryBackward", package.Package.RollbackBoundaryBackward); + } + + if (!String.IsNullOrEmpty(package.Package.LogPathVariable)) + { + writer.WriteAttributeString("LogPathVariable", package.Package.LogPathVariable); + } + + if (!String.IsNullOrEmpty(package.Package.RollbackLogPathVariable)) + { + writer.WriteAttributeString("RollbackLogPathVariable", package.Package.RollbackLogPathVariable); + } + + if (!String.IsNullOrEmpty(package.Package.InstallCondition)) + { + writer.WriteAttributeString("InstallCondition", package.Package.InstallCondition); + } + + if (WixBundlePackageType.Exe == package.Package.Type) + { + writer.WriteAttributeString("DetectCondition", package.ExePackage.DetectCondition); + writer.WriteAttributeString("InstallArguments", package.ExePackage.InstallCommand); + writer.WriteAttributeString("UninstallArguments", package.ExePackage.UninstallCommand); + writer.WriteAttributeString("RepairArguments", package.ExePackage.RepairCommand); + writer.WriteAttributeString("Repairable", package.ExePackage.Repairable ? "yes" : "no"); + if (!String.IsNullOrEmpty(package.ExePackage.ExeProtocol)) + { + writer.WriteAttributeString("Protocol", package.ExePackage.ExeProtocol); + } + } + else if (WixBundlePackageType.Msi == package.Package.Type) + { + writer.WriteAttributeString("ProductCode", package.MsiPackage.ProductCode); + writer.WriteAttributeString("Language", package.MsiPackage.ProductLanguage.ToString(CultureInfo.InvariantCulture)); + writer.WriteAttributeString("Version", package.MsiPackage.ProductVersion); + writer.WriteAttributeString("DisplayInternalUI", package.MsiPackage.DisplayInternalUI ? "yes" : "no"); + if (!String.IsNullOrEmpty(package.MsiPackage.UpgradeCode)) + { + writer.WriteAttributeString("UpgradeCode", package.MsiPackage.UpgradeCode); + } + } + else if (WixBundlePackageType.Msp == package.Package.Type) + { + writer.WriteAttributeString("PatchCode", package.MspPackage.PatchCode); + writer.WriteAttributeString("PatchXml", package.MspPackage.PatchXml); + writer.WriteAttributeString("DisplayInternalUI", package.MspPackage.DisplayInternalUI ? "yes" : "no"); + + // If there is still a chance that all of our patches will target a narrow set of + // product codes, add the patch list to the overall list. + if (null != targetCodes) + { + if (!package.MspPackage.TargetUnspecified) + { + IEnumerable patchTargetCodes = targetCodesByPatch[package.MspPackage.ChainPackageId]; + + targetCodes.AddRange(patchTargetCodes); + } + else // we have a patch that targets the world, so throw the whole list away. + { + targetCodes = null; + } + } + } + else if (WixBundlePackageType.Msu == package.Package.Type) + { + writer.WriteAttributeString("DetectCondition", package.MsuPackage.DetectCondition); + writer.WriteAttributeString("KB", package.MsuPackage.MsuKB); + } + + IEnumerable packageMsiFeatures = msiFeaturesByPackage[package.Package.WixChainItemId]; + + foreach (WixBundleMsiFeatureRow feature in packageMsiFeatures) + { + writer.WriteStartElement("MsiFeature"); + writer.WriteAttributeString("Id", feature.Name); + writer.WriteEndElement(); + } + + IEnumerable packageMsiProperties = msiPropertiesByPackage[package.Package.WixChainItemId]; + + foreach (WixBundleMsiPropertyRow msiProperty in packageMsiProperties) + { + writer.WriteStartElement("MsiProperty"); + writer.WriteAttributeString("Id", msiProperty.Name); + writer.WriteAttributeString("Value", msiProperty.Value); + if (!String.IsNullOrEmpty(msiProperty.Condition)) + { + writer.WriteAttributeString("Condition", msiProperty.Condition); + } + writer.WriteEndElement(); + } + + IEnumerable packageSlipstreamMsps = slipstreamMspsByPackage[package.Package.WixChainItemId]; + + foreach (WixBundleSlipstreamMspRow slipstreamMsp in packageSlipstreamMsps) + { + writer.WriteStartElement("SlipstreamMsp"); + writer.WriteAttributeString("Id", slipstreamMsp.MspPackageId); + writer.WriteEndElement(); + } + + IEnumerable packageExitCodes = exitCodesByPackage[package.Package.WixChainItemId]; + + foreach (WixBundlePackageExitCodeRow exitCode in packageExitCodes) + { + writer.WriteStartElement("ExitCode"); + + if (exitCode.Code.HasValue) + { + writer.WriteAttributeString("Code", unchecked((uint)exitCode.Code).ToString(CultureInfo.InvariantCulture)); + } + else + { + writer.WriteAttributeString("Code", "*"); + } + + writer.WriteAttributeString("Type", ((int)exitCode.Behavior).ToString(CultureInfo.InvariantCulture)); + writer.WriteEndElement(); + } + + IEnumerable packageCommandLines = commandLinesByPackage[package.Package.WixChainItemId]; + + foreach (WixBundlePackageCommandLineRow commandLine in packageCommandLines) + { + writer.WriteStartElement("CommandLine"); + writer.WriteAttributeString("InstallArgument", commandLine.InstallArgument); + writer.WriteAttributeString("UninstallArgument", commandLine.UninstallArgument); + writer.WriteAttributeString("RepairArgument", commandLine.RepairArgument); + writer.WriteAttributeString("Condition", commandLine.Condition); + writer.WriteEndElement(); + } + + // Output the dependency information. + foreach (ProvidesDependency dependency in package.Provides) + { + // TODO: Add to wixpdb as an imported table, or link package wixpdbs to bundle wixpdbs. + dependency.WriteXml(writer); + } + + IEnumerable packageRelatedPackages = relatedPackagesByPackage[package.Package.WixChainItemId]; + + foreach (WixBundleRelatedPackageRow related in packageRelatedPackages) + { + writer.WriteStartElement("RelatedPackage"); + writer.WriteAttributeString("Id", related.Id); + if (!String.IsNullOrEmpty(related.MinVersion)) + { + writer.WriteAttributeString("MinVersion", related.MinVersion); + writer.WriteAttributeString("MinInclusive", related.MinInclusive ? "yes" : "no"); + } + if (!String.IsNullOrEmpty(related.MaxVersion)) + { + writer.WriteAttributeString("MaxVersion", related.MaxVersion); + writer.WriteAttributeString("MaxInclusive", related.MaxInclusive ? "yes" : "no"); + } + writer.WriteAttributeString("OnlyDetect", related.OnlyDetect ? "yes" : "no"); + + string[] relatedLanguages = related.Languages.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + + if (0 < relatedLanguages.Length) + { + writer.WriteAttributeString("LangInclusive", related.LangInclusive ? "yes" : "no"); + foreach (string language in relatedLanguages) + { + writer.WriteStartElement("Language"); + writer.WriteAttributeString("Id", language); + writer.WriteEndElement(); + } + } + writer.WriteEndElement(); + } + + // Write any contained Payloads with the PackagePayload being first + writer.WriteStartElement("PayloadRef"); + writer.WriteAttributeString("Id", package.Package.PackagePayload); + writer.WriteEndElement(); + + IEnumerable packagePayloads = payloadsByPackage[package.Package.WixChainItemId]; + + foreach (WixBundlePayloadRow payload in packagePayloads) + { + if (payload.Id != package.Package.PackagePayload) + { + writer.WriteStartElement("PayloadRef"); + writer.WriteAttributeString("Id", payload.Id); + writer.WriteEndElement(); + } + } + + writer.WriteEndElement(); // + } + writer.WriteEndElement(); // + + if (null != targetCodes) + { + foreach (WixBundlePatchTargetCodeRow targetCode in targetCodes) + { + writer.WriteStartElement("PatchTargetCode"); + writer.WriteAttributeString("TargetCode", targetCode.TargetCode); + writer.WriteAttributeString("Product", targetCode.TargetsProductCode ? "yes" : "no"); + writer.WriteEndElement(); + } + } + + // Write the ApprovedExeForElevation elements. + IEnumerable approvedExesForElevation = this.Output.Tables["WixApprovedExeForElevation"].RowsAs(); + + foreach (WixApprovedExeForElevationRow approvedExeForElevation in approvedExesForElevation) + { + writer.WriteStartElement("ApprovedExeForElevation"); + writer.WriteAttributeString("Id", approvedExeForElevation.Id); + writer.WriteAttributeString("Key", approvedExeForElevation.Key); + + if (!String.IsNullOrEmpty(approvedExeForElevation.ValueName)) + { + writer.WriteAttributeString("ValueName", approvedExeForElevation.ValueName); + } + + if (approvedExeForElevation.Win64) + { + writer.WriteAttributeString("Win64", "yes"); + } + + writer.WriteEndElement(); + } + + writer.WriteEndDocument(); // + } + } + + private void WriteBurnManifestContainerAttributes(XmlTextWriter writer, string executableName, WixBundleContainerRow container) + { + writer.WriteAttributeString("Id", container.Id); + writer.WriteAttributeString("FileSize", container.Size.ToString(CultureInfo.InvariantCulture)); + writer.WriteAttributeString("Hash", container.Hash); + + if (ContainerType.Detached == container.Type) + { + string resolvedUrl = this.ResolveUrl(container.DownloadUrl, null, null, container.Id, container.Name); + if (!String.IsNullOrEmpty(resolvedUrl)) + { + writer.WriteAttributeString("DownloadUrl", resolvedUrl); + } + else if (!String.IsNullOrEmpty(container.DownloadUrl)) + { + writer.WriteAttributeString("DownloadUrl", container.DownloadUrl); + } + + writer.WriteAttributeString("FilePath", container.Name); + } + else if (ContainerType.Attached == container.Type) + { + if (!String.IsNullOrEmpty(container.DownloadUrl)) + { + Messaging.Instance.OnMessage(WixWarnings.DownloadUrlNotSupportedForAttachedContainers(container.SourceLineNumbers, container.Id)); + } + + writer.WriteAttributeString("FilePath", executableName); // attached containers use the name of the bundle since they are attached to the executable. + writer.WriteAttributeString("AttachedIndex", container.AttachedContainerIndex.ToString(CultureInfo.InvariantCulture)); + writer.WriteAttributeString("Attached", "yes"); + writer.WriteAttributeString("Primary", "yes"); + } + } + + private void WriteBurnManifestPayloadAttributes(XmlTextWriter writer, WixBundlePayloadRow payload, bool embeddedOnly, Dictionary allPayloads) + { + Debug.Assert(!embeddedOnly || PackagingType.Embedded == payload.Packaging); + + writer.WriteAttributeString("Id", payload.Id); + writer.WriteAttributeString("FilePath", payload.Name); + writer.WriteAttributeString("FileSize", payload.FileSize.ToString(CultureInfo.InvariantCulture)); + writer.WriteAttributeString("Hash", payload.Hash); + + if (payload.LayoutOnly) + { + writer.WriteAttributeString("LayoutOnly", "yes"); + } + + if (!String.IsNullOrEmpty(payload.PublicKey)) + { + writer.WriteAttributeString("CertificateRootPublicKeyIdentifier", payload.PublicKey); + } + + if (!String.IsNullOrEmpty(payload.Thumbprint)) + { + writer.WriteAttributeString("CertificateRootThumbprint", payload.Thumbprint); + } + + switch (payload.Packaging) + { + case PackagingType.Embedded: // this means it's in a container. + if (!String.IsNullOrEmpty(payload.DownloadUrl)) + { + Messaging.Instance.OnMessage(WixWarnings.DownloadUrlNotSupportedForEmbeddedPayloads(payload.SourceLineNumbers, payload.Id)); + } + + writer.WriteAttributeString("Packaging", "embedded"); + writer.WriteAttributeString("SourcePath", payload.EmbeddedId); + + if (Compiler.BurnUXContainerId != payload.Container) + { + writer.WriteAttributeString("Container", payload.Container); + } + break; + + case PackagingType.External: + string packageId = payload.ParentPackagePayload; + string parentUrl = payload.ParentPackagePayload == null ? null : allPayloads[payload.ParentPackagePayload].DownloadUrl; + string resolvedUrl = this.ResolveUrl(payload.DownloadUrl, parentUrl, packageId, payload.Id, payload.Name); + if (!String.IsNullOrEmpty(resolvedUrl)) + { + writer.WriteAttributeString("DownloadUrl", resolvedUrl); + } + else if (!String.IsNullOrEmpty(payload.DownloadUrl)) + { + writer.WriteAttributeString("DownloadUrl", payload.DownloadUrl); + } + + writer.WriteAttributeString("Packaging", "external"); + writer.WriteAttributeString("SourcePath", payload.Name); + break; + } + + if (!String.IsNullOrEmpty(payload.Catalog)) + { + writer.WriteAttributeString("Catalog", payload.Catalog); + } + } + + private string ResolveUrl(string url, string fallbackUrl, string packageId, string payloadId, string fileName) + { + string resolved = null; + foreach (IBinderFileManager fileManager in this.FileManagers) + { + resolved = fileManager.ResolveUrl(url, fallbackUrl, packageId, payloadId, fileName); + if (!String.IsNullOrEmpty(resolved)) + { + break; + } + } + + return resolved; + } + } +} diff --git a/src/WixToolset.Core/Bind/Bundles/CreateContainerCommand.cs b/src/WixToolset.Core/Bind/Bundles/CreateContainerCommand.cs new file mode 100644 index 00000000..1bf987e3 --- /dev/null +++ b/src/WixToolset.Core/Bind/Bundles/CreateContainerCommand.cs @@ -0,0 +1,68 @@ +// 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. + +namespace WixToolset.Bind.Bundles +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + using System.Linq; + using WixToolset.Cab; + using WixToolset.Data; + using WixToolset.Data.Rows; + + /// + /// Creates cabinet files. + /// + internal class CreateContainerCommand : ICommand + { + public CompressionLevel DefaultCompressionLevel { private get; set; } + + public IEnumerable Payloads { private get; set; } + + public string ManifestFile { private get; set; } + + public string OutputPath { private get; set; } + + public string Hash { get; private set; } + + public long Size { get; private set; } + + public void Execute() + { + int payloadCount = this.Payloads.Count(); // The number of embedded payloads + + if (!String.IsNullOrEmpty(this.ManifestFile)) + { + ++payloadCount; + } + + using (WixCreateCab cab = new WixCreateCab(Path.GetFileName(this.OutputPath), Path.GetDirectoryName(this.OutputPath), payloadCount, 0, 0, this.DefaultCompressionLevel)) + { + // If a manifest was provided always add it as "payload 0" to the container. + if (!String.IsNullOrEmpty(this.ManifestFile)) + { + cab.AddFile(this.ManifestFile, "0"); + } + + foreach (WixBundlePayloadRow payload in this.Payloads) + { + Debug.Assert(PackagingType.Embedded == payload.Packaging); + + Messaging.Instance.OnMessage(WixVerboses.LoadingPayload(payload.FullFileName)); + + cab.AddFile(payload.FullFileName, payload.EmbeddedId); + } + + cab.Complete(); + } + + // Now that the container is created, set the outputs of the command. + FileInfo fileInfo = new FileInfo(this.OutputPath); + + this.Hash = Common.GetFileHash(fileInfo.FullName); + + this.Size = fileInfo.Length; + } + } +} diff --git a/src/WixToolset.Core/Bind/Bundles/GetPackageFacadesCommand.cs b/src/WixToolset.Core/Bind/Bundles/GetPackageFacadesCommand.cs new file mode 100644 index 00000000..dc19e380 --- /dev/null +++ b/src/WixToolset.Core/Bind/Bundles/GetPackageFacadesCommand.cs @@ -0,0 +1,62 @@ +// 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. + +namespace WixToolset.Bind.Bundles +{ + using System.Collections.Generic; + using WixToolset.Data; + using WixToolset.Data.Rows; + + internal class GetPackageFacadesCommand : ICommand + { + public Table PackageTable { private get; set; } + + public Table ExePackageTable { private get; set; } + + public Table MsiPackageTable { private get; set; } + + public Table MspPackageTable { private get; set; } + + public Table MsuPackageTable { private get; set; } + + public IDictionary PackageFacades { get; private set; } + + public void Execute() + { + RowDictionary exePackages = new RowDictionary(this.ExePackageTable); + RowDictionary msiPackages = new RowDictionary(this.MsiPackageTable); + RowDictionary mspPackages = new RowDictionary(this.MspPackageTable); + RowDictionary msuPackages = new RowDictionary(this.MsuPackageTable); + + Dictionary facades = new Dictionary(this.PackageTable.Rows.Count); + + foreach (WixBundlePackageRow package in this.PackageTable.Rows) + { + string id = package.WixChainItemId; + PackageFacade facade = null; + + switch (package.Type) + { + case WixBundlePackageType.Exe: + facade = new PackageFacade(package, exePackages.Get(id)); + break; + + case WixBundlePackageType.Msi: + facade = new PackageFacade(package, msiPackages.Get(id)); + break; + + case WixBundlePackageType.Msp: + facade = new PackageFacade(package, mspPackages.Get(id)); + break; + + case WixBundlePackageType.Msu: + facade = new PackageFacade(package, msuPackages.Get(id)); + break; + } + + facades.Add(id, facade); + } + + this.PackageFacades = facades; + } + } +} diff --git a/src/WixToolset.Core/Bind/Bundles/OrderPackagesAndRollbackBoundariesCommand.cs b/src/WixToolset.Core/Bind/Bundles/OrderPackagesAndRollbackBoundariesCommand.cs new file mode 100644 index 00000000..ac3a301d --- /dev/null +++ b/src/WixToolset.Core/Bind/Bundles/OrderPackagesAndRollbackBoundariesCommand.cs @@ -0,0 +1,145 @@ +// 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. + +namespace WixToolset.Bind.Bundles +{ + using System; + using System.Collections.Generic; + using WixToolset.Data; + using WixToolset.Data.Rows; + + internal class OrderPackagesAndRollbackBoundariesCommand : ICommand + { + public Table WixGroupTable { private get; set; } + + public RowDictionary Boundaries { private get; set; } + + public IDictionary PackageFacades { private get; set; } + + public IEnumerable OrderedPackageFacades { get; private set; } + + public IEnumerable UsedRollbackBoundaries { get; private set; } + + public void Execute() + { + List orderedFacades = new List(); + List usedBoundaries = new List(); + + // Process the chain of packages to add them in the correct order + // and assign the forward rollback boundaries as appropriate. Remember + // rollback boundaries are authored as elements in the chain which + // we re-interpret here to add them as attributes on the next available + // package in the chain. Essentially we mark some packages as being + // the start of a rollback boundary when installing and repairing. + // We handle uninstall (aka: backwards) rollback boundaries after + // we get these install/repair (aka: forward) rollback boundaries + // defined. + WixBundleRollbackBoundaryRow previousRollbackBoundary = null; + WixBundleRollbackBoundaryRow lastRollbackBoundary = null; + bool boundaryHadX86Package = false; + + foreach (WixGroupRow row in this.WixGroupTable.Rows) + { + if (ComplexReferenceChildType.Package == row.ChildType && ComplexReferenceParentType.PackageGroup == row.ParentType && "WixChain" == row.ParentId) + { + PackageFacade facade = null; + if (PackageFacades.TryGetValue(row.ChildId, out facade)) + { + if (null != previousRollbackBoundary) + { + usedBoundaries.Add(previousRollbackBoundary); + facade.Package.RollbackBoundary = previousRollbackBoundary.ChainPackageId; + previousRollbackBoundary = null; + + boundaryHadX86Package = (facade.Package.x64 == YesNoType.Yes); + } + + // Error if MSI transaction has x86 package preceding x64 packages + if ((lastRollbackBoundary != null) && (lastRollbackBoundary.Transaction == YesNoType.Yes) + && boundaryHadX86Package + && (facade.Package.x64 == YesNoType.Yes)) + { + Messaging.Instance.OnMessage(WixErrors.MsiTransactionX86BeforeX64(lastRollbackBoundary.SourceLineNumbers)); + } + boundaryHadX86Package = boundaryHadX86Package || (facade.Package.x64 == YesNoType.No); + + orderedFacades.Add(facade); + } + else // must be a rollback boundary. + { + // Discard the next rollback boundary if we have a previously defined boundary. + WixBundleRollbackBoundaryRow nextRollbackBoundary = Boundaries.Get(row.ChildId); + if (null != previousRollbackBoundary) + { + Messaging.Instance.OnMessage(WixWarnings.DiscardedRollbackBoundary(nextRollbackBoundary.SourceLineNumbers, nextRollbackBoundary.ChainPackageId)); + } + else + { + previousRollbackBoundary = nextRollbackBoundary; + lastRollbackBoundary = nextRollbackBoundary; + } + } + } + } + + if (null != previousRollbackBoundary) + { + Messaging.Instance.OnMessage(WixWarnings.DiscardedRollbackBoundary(previousRollbackBoundary.SourceLineNumbers, previousRollbackBoundary.ChainPackageId)); + } + + // With the forward rollback boundaries assigned, we can now go + // through the packages with rollback boundaries and assign backward + // rollback boundaries. Backward rollback boundaries are used when + // the chain is going "backwards" which (AFAIK) only happens during + // uninstall. + // + // Consider the scenario with three packages: A, B and C. Packages A + // and C are marked as rollback boundary packages and package B is + // not. The naive implementation would execute the chain like this + // (numbers indicate where rollback boundaries would end up): + // install: 1 A B 2 C + // uninstall: 2 C B 1 A + // + // The uninstall chain is wrong, A and B should be grouped together + // not C and B. The fix is to label packages with a "backwards" + // rollback boundary used during uninstall. The backwards rollback + // boundaries are assigned to the package *before* the next rollback + // boundary. Using our example from above again, I'll mark the + // backwards rollback boundaries prime (aka: with '). + // install: 1 A B 1' 2 C 2' + // uninstall: 2' C 2 1' B A 1 + // + // If the marked boundaries are ignored during install you get the + // same thing as above (good) and if the non-marked boundaries are + // ignored during uninstall then A and B are correctly grouped. + // Here's what it looks like without all the markers: + // install: 1 A B 2 C + // uninstall: 2 C 1 B A + // Woot! + string previousRollbackBoundaryId = null; + PackageFacade previousFacade = null; + + foreach (PackageFacade package in orderedFacades) + { + if (null != package.Package.RollbackBoundary) + { + if (null != previousFacade) + { + previousFacade.Package.RollbackBoundaryBackward = previousRollbackBoundaryId; + } + + previousRollbackBoundaryId = package.Package.RollbackBoundary; + } + + previousFacade = package; + } + + if (!String.IsNullOrEmpty(previousRollbackBoundaryId) && null != previousFacade) + { + previousFacade.Package.RollbackBoundaryBackward = previousRollbackBoundaryId; + } + + this.OrderedPackageFacades = orderedFacades; + this.UsedRollbackBoundaries = usedBoundaries; + } + } +} diff --git a/src/WixToolset.Core/Bind/Bundles/PackageFacade.cs b/src/WixToolset.Core/Bind/Bundles/PackageFacade.cs new file mode 100644 index 00000000..f7e6410f --- /dev/null +++ b/src/WixToolset.Core/Bind/Bundles/PackageFacade.cs @@ -0,0 +1,58 @@ +// 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. + +namespace WixToolset.Bind.Bundles +{ + using WixToolset.Data.Rows; + + internal class PackageFacade + { + private PackageFacade(WixBundlePackageRow package) + { + this.Package = package; + this.Provides = new ProvidesDependencyCollection(); + } + + public PackageFacade(WixBundlePackageRow package, WixBundleExePackageRow exePackage) + : this(package) + { + this.ExePackage = exePackage; + } + + public PackageFacade(WixBundlePackageRow package, WixBundleMsiPackageRow msiPackage) + : this(package) + { + this.MsiPackage = msiPackage; + } + + public PackageFacade(WixBundlePackageRow package, WixBundleMspPackageRow mspPackage) + : this(package) + { + this.MspPackage = mspPackage; + } + + public PackageFacade(WixBundlePackageRow package, WixBundleMsuPackageRow msuPackage) + : this(package) + { + this.MsuPackage = msuPackage; + } + + public WixBundlePackageRow Package { get; private set; } + + public WixBundleExePackageRow ExePackage { get; private set; } + + public WixBundleMsiPackageRow MsiPackage { get; private set; } + + public WixBundleMspPackageRow MspPackage { get; private set; } + + public WixBundleMsuPackageRow MsuPackage { get; private set; } + + /// + /// The provides dependencies authored and imported for this package. + /// + /// + /// TODO: Eventually this collection should turn into Rows so they are tracked in the PDB but + /// the relationship with the extension makes it much trickier to pull off. + /// + public ProvidesDependencyCollection Provides { get; private set; } + } +} diff --git a/src/WixToolset.Core/Bind/Bundles/ProcessExePackageCommand.cs b/src/WixToolset.Core/Bind/Bundles/ProcessExePackageCommand.cs new file mode 100644 index 00000000..a1e7c271 --- /dev/null +++ b/src/WixToolset.Core/Bind/Bundles/ProcessExePackageCommand.cs @@ -0,0 +1,33 @@ +// 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. + +namespace WixToolset.Bind.Bundles +{ + using System; + using WixToolset.Data; + using WixToolset.Data.Rows; + + /// + /// Initializes package state from the Exe contents. + /// + internal class ProcessExePackageCommand : ICommand + { + public RowDictionary AuthoredPayloads { private get; set; } + + public PackageFacade Facade { private get; set; } + + /// + /// Processes the Exe packages to add properties and payloads from the Exe packages. + /// + public void Execute() + { + WixBundlePayloadRow packagePayload = this.AuthoredPayloads.Get(this.Facade.Package.PackagePayload); + + if (String.IsNullOrEmpty(this.Facade.Package.CacheId)) + { + this.Facade.Package.CacheId = packagePayload.Hash; + } + + this.Facade.Package.Version = packagePayload.Version; + } + } +} diff --git a/src/WixToolset.Core/Bind/Bundles/ProcessMsiPackageCommand.cs b/src/WixToolset.Core/Bind/Bundles/ProcessMsiPackageCommand.cs new file mode 100644 index 00000000..f73776c0 --- /dev/null +++ b/src/WixToolset.Core/Bind/Bundles/ProcessMsiPackageCommand.cs @@ -0,0 +1,560 @@ +// 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. + +namespace WixToolset.Bind.Bundles +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Diagnostics; + using System.Globalization; + using System.IO; + using System.Linq; + using WixToolset.Data; + using WixToolset.Data.Rows; + using WixToolset.Extensibility; + using WixToolset.Msi; + using WixToolset.Core.Native; + using Dtf = WixToolset.Dtf.WindowsInstaller; + + /// + /// Initializes package state from the MSI contents. + /// + internal class ProcessMsiPackageCommand : ICommand + { + private const string PropertySqlFormat = "SELECT `Value` FROM `Property` WHERE `Property` = '{0}'"; + + public RowDictionary AuthoredPayloads { private get; set; } + + public PackageFacade Facade { private get; set; } + + public IBinderFileManager FileManager { private get; set; } + + public Table MsiFeatureTable { private get; set; } + + public Table MsiPropertyTable { private get; set; } + + public Table PayloadTable { private get; set; } + + public Table RelatedPackageTable { private get; set; } + + /// + /// Processes the MSI packages to add properties and payloads from the MSI packages. + /// + public void Execute() + { + WixBundlePayloadRow packagePayload = this.AuthoredPayloads.Get(this.Facade.Package.PackagePayload); + + string sourcePath = packagePayload.FullFileName; + bool longNamesInImage = false; + bool compressed = false; + bool x64 = false; + try + { + // Read data out of the msi database... + using (Dtf.SummaryInfo sumInfo = new Dtf.SummaryInfo(sourcePath, false)) + { + // 1 is the Word Count summary information stream bit that means + // the MSI uses short file names when set. We care about long file + // names so check when the bit is not set. + longNamesInImage = 0 == (sumInfo.WordCount & 1); + + // 2 is the Word Count summary information stream bit that means + // files are compressed in the MSI by default when the bit is set. + compressed = 2 == (sumInfo.WordCount & 2); + + x64 = (sumInfo.Template.Contains("x64") || sumInfo.Template.Contains("Intel64")); + + // 8 is the Word Count summary information stream bit that means + // "Elevated privileges are not required to install this package." + // in MSI 4.5 and below, if this bit is 0, elevation is required. + this.Facade.Package.PerMachine = (0 == (sumInfo.WordCount & 8)) ? YesNoDefaultType.Yes : YesNoDefaultType.No; + this.Facade.Package.x64 = x64 ? YesNoType.Yes : YesNoType.No; + } + + using (Dtf.Database db = new Dtf.Database(sourcePath)) + { + this.Facade.MsiPackage.ProductCode = ProcessMsiPackageCommand.GetProperty(db, "ProductCode"); + this.Facade.MsiPackage.UpgradeCode = ProcessMsiPackageCommand.GetProperty(db, "UpgradeCode"); + this.Facade.MsiPackage.Manufacturer = ProcessMsiPackageCommand.GetProperty(db, "Manufacturer"); + this.Facade.MsiPackage.ProductLanguage = Convert.ToInt32(ProcessMsiPackageCommand.GetProperty(db, "ProductLanguage"), CultureInfo.InvariantCulture); + this.Facade.MsiPackage.ProductVersion = ProcessMsiPackageCommand.GetProperty(db, "ProductVersion"); + + if (!Common.IsValidModuleOrBundleVersion(this.Facade.MsiPackage.ProductVersion)) + { + // not a proper .NET version (e.g., five fields); can we get a valid four-part version number? + string version = null; + string[] versionParts = this.Facade.MsiPackage.ProductVersion.Split('.'); + int count = versionParts.Length; + if (0 < count) + { + version = versionParts[0]; + for (int i = 1; i < 4 && i < count; ++i) + { + version = String.Concat(version, ".", versionParts[i]); + } + } + + if (!String.IsNullOrEmpty(version) && Common.IsValidModuleOrBundleVersion(version)) + { + Messaging.Instance.OnMessage(WixWarnings.VersionTruncated(this.Facade.Package.SourceLineNumbers, this.Facade.MsiPackage.ProductVersion, sourcePath, version)); + this.Facade.MsiPackage.ProductVersion = version; + } + else + { + Messaging.Instance.OnMessage(WixErrors.InvalidProductVersion(this.Facade.Package.SourceLineNumbers, this.Facade.MsiPackage.ProductVersion, sourcePath)); + } + } + + if (String.IsNullOrEmpty(this.Facade.Package.CacheId)) + { + this.Facade.Package.CacheId = String.Format("{0}v{1}", this.Facade.MsiPackage.ProductCode, this.Facade.MsiPackage.ProductVersion); + } + + if (String.IsNullOrEmpty(this.Facade.Package.DisplayName)) + { + this.Facade.Package.DisplayName = ProcessMsiPackageCommand.GetProperty(db, "ProductName"); + } + + if (String.IsNullOrEmpty(this.Facade.Package.Description)) + { + this.Facade.Package.Description = ProcessMsiPackageCommand.GetProperty(db, "ARPCOMMENTS"); + } + + ISet payloadNames = this.GetPayloadTargetNames(); + + ISet msiPropertyNames = this.GetMsiPropertyNames(); + + this.SetPerMachineAppropriately(db, sourcePath); + + // Ensure the MSI package is appropriately marked visible or not. + this.SetPackageVisibility(db, msiPropertyNames); + + // Unless the MSI or setup code overrides the default, set MSIFASTINSTALL for best performance. + if (!msiPropertyNames.Contains("MSIFASTINSTALL") && !ProcessMsiPackageCommand.HasProperty(db, "MSIFASTINSTALL")) + { + this.AddMsiProperty("MSIFASTINSTALL", "7"); + } + + this.CreateRelatedPackages(db); + + // If feature selection is enabled, represent the Feature table in the manifest. + if (this.Facade.MsiPackage.EnableFeatureSelection) + { + this.CreateMsiFeatures(db); + } + + // Add all external cabinets as package payloads. + this.ImportExternalCabinetAsPayloads(db, packagePayload, payloadNames); + + // Add all external files as package payloads and calculate the total install size as the rollup of + // File table's sizes. + this.Facade.Package.InstallSize = this.ImportExternalFileAsPayloadsAndReturnInstallSize(db, packagePayload, longNamesInImage, compressed, payloadNames); + + // Add all dependency providers from the MSI. + this.ImportDependencyProviders(db); + } + } + catch (Dtf.InstallerException e) + { + Messaging.Instance.OnMessage(WixErrors.UnableToReadPackageInformation(this.Facade.Package.SourceLineNumbers, sourcePath, e.Message)); + } + } + + private ISet GetPayloadTargetNames() + { + IEnumerable payloadNames = this.PayloadTable.RowsAs() + .Where(r => r.Package == this.Facade.Package.WixChainItemId) + .Select(r => r.Name); + + return new HashSet(payloadNames, StringComparer.OrdinalIgnoreCase); + } + + private ISet GetMsiPropertyNames() + { + IEnumerable properties = this.MsiPropertyTable.RowsAs() + .Where(r => r.ChainPackageId == this.Facade.Package.WixChainItemId) + .Select(r => r.Name); + + return new HashSet(properties, StringComparer.Ordinal); + } + + private void SetPerMachineAppropriately(Dtf.Database db, string sourcePath) + { + if (this.Facade.MsiPackage.ForcePerMachine) + { + if (YesNoDefaultType.No == this.Facade.Package.PerMachine) + { + Messaging.Instance.OnMessage(WixWarnings.PerUserButForcingPerMachine(this.Facade.Package.SourceLineNumbers, sourcePath)); + this.Facade.Package.PerMachine = YesNoDefaultType.Yes; // ensure that we think the package is per-machine. + } + + // Force ALLUSERS=1 via the MSI command-line. + this.AddMsiProperty("ALLUSERS", "1"); + } + else + { + string allusers = ProcessMsiPackageCommand.GetProperty(db, "ALLUSERS"); + + if (String.IsNullOrEmpty(allusers)) + { + // Not forced per-machine and no ALLUSERS property, flip back to per-user. + if (YesNoDefaultType.Yes == this.Facade.Package.PerMachine) + { + Messaging.Instance.OnMessage(WixWarnings.ImplicitlyPerUser(this.Facade.Package.SourceLineNumbers, sourcePath)); + this.Facade.Package.PerMachine = YesNoDefaultType.No; + } + } + else if (allusers.Equals("1", StringComparison.Ordinal)) + { + if (YesNoDefaultType.No == this.Facade.Package.PerMachine) + { + Messaging.Instance.OnMessage(WixErrors.PerUserButAllUsersEquals1(this.Facade.Package.SourceLineNumbers, sourcePath)); + } + } + else if (allusers.Equals("2", StringComparison.Ordinal)) + { + Messaging.Instance.OnMessage(WixWarnings.DiscouragedAllUsersValue(this.Facade.Package.SourceLineNumbers, sourcePath, (YesNoDefaultType.Yes == this.Facade.Package.PerMachine) ? "machine" : "user")); + } + else + { + Messaging.Instance.OnMessage(WixErrors.UnsupportedAllUsersValue(this.Facade.Package.SourceLineNumbers, sourcePath, allusers)); + } + } + } + + private void SetPackageVisibility(Dtf.Database db, ISet msiPropertyNames) + { + bool alreadyVisible = !ProcessMsiPackageCommand.HasProperty(db, "ARPSYSTEMCOMPONENT"); + + if (alreadyVisible != this.Facade.Package.Visible) // if not already set to the correct visibility. + { + // If the authoring specifically added "ARPSYSTEMCOMPONENT", don't do it again. + if (!msiPropertyNames.Contains("ARPSYSTEMCOMPONENT")) + { + this.AddMsiProperty("ARPSYSTEMCOMPONENT", this.Facade.Package.Visible ? String.Empty : "1"); + } + } + } + + private void CreateRelatedPackages(Dtf.Database db) + { + // Represent the Upgrade table as related packages. + if (db.Tables.Contains("Upgrade")) + { + using (Dtf.View view = db.OpenView("SELECT `UpgradeCode`, `VersionMin`, `VersionMax`, `Language`, `Attributes` FROM `Upgrade`")) + { + view.Execute(); + while (true) + { + using (Dtf.Record record = view.Fetch()) + { + if (null == record) + { + break; + } + + WixBundleRelatedPackageRow related = (WixBundleRelatedPackageRow)this.RelatedPackageTable.CreateRow(this.Facade.Package.SourceLineNumbers); + related.ChainPackageId = this.Facade.Package.WixChainItemId; + related.Id = record.GetString(1); + related.MinVersion = record.GetString(2); + related.MaxVersion = record.GetString(3); + related.Languages = record.GetString(4); + + int attributes = record.GetInteger(5); + related.OnlyDetect = (attributes & MsiInterop.MsidbUpgradeAttributesOnlyDetect) == MsiInterop.MsidbUpgradeAttributesOnlyDetect; + related.MinInclusive = (attributes & MsiInterop.MsidbUpgradeAttributesVersionMinInclusive) == MsiInterop.MsidbUpgradeAttributesVersionMinInclusive; + related.MaxInclusive = (attributes & MsiInterop.MsidbUpgradeAttributesVersionMaxInclusive) == MsiInterop.MsidbUpgradeAttributesVersionMaxInclusive; + related.LangInclusive = (attributes & MsiInterop.MsidbUpgradeAttributesLanguagesExclusive) == 0; + } + } + } + } + } + + private void CreateMsiFeatures(Dtf.Database db) + { + if (db.Tables.Contains("Feature")) + { + using (Dtf.View featureView = db.OpenView("SELECT `Component_` FROM `FeatureComponents` WHERE `Feature_` = ?")) + using (Dtf.View componentView = db.OpenView("SELECT `FileSize` FROM `File` WHERE `Component_` = ?")) + { + using (Dtf.Record featureRecord = new Dtf.Record(1)) + using (Dtf.Record componentRecord = new Dtf.Record(1)) + { + using (Dtf.View allFeaturesView = db.OpenView("SELECT * FROM `Feature`")) + { + allFeaturesView.Execute(); + + while (true) + { + using (Dtf.Record allFeaturesResultRecord = allFeaturesView.Fetch()) + { + if (null == allFeaturesResultRecord) + { + break; + } + + string featureName = allFeaturesResultRecord.GetString(1); + + // Calculate the Feature size. + featureRecord.SetString(1, featureName); + featureView.Execute(featureRecord); + + // Loop over all the components for the feature to calculate the size of the feature. + long size = 0; + while (true) + { + using (Dtf.Record componentResultRecord = featureView.Fetch()) + { + if (null == componentResultRecord) + { + break; + } + string component = componentResultRecord.GetString(1); + componentRecord.SetString(1, component); + componentView.Execute(componentRecord); + + while (true) + { + using (Dtf.Record fileResultRecord = componentView.Fetch()) + { + if (null == fileResultRecord) + { + break; + } + + string fileSize = fileResultRecord.GetString(1); + size += Convert.ToInt32(fileSize, CultureInfo.InvariantCulture.NumberFormat); + } + } + } + } + + WixBundleMsiFeatureRow feature = (WixBundleMsiFeatureRow)this.MsiFeatureTable.CreateRow(this.Facade.Package.SourceLineNumbers); + feature.ChainPackageId = this.Facade.Package.WixChainItemId; + feature.Name = featureName; + feature.Parent = allFeaturesResultRecord.GetString(2); + feature.Title = allFeaturesResultRecord.GetString(3); + feature.Description = allFeaturesResultRecord.GetString(4); + feature.Display = allFeaturesResultRecord.GetInteger(5); + feature.Level = allFeaturesResultRecord.GetInteger(6); + feature.Directory = allFeaturesResultRecord.GetString(7); + feature.Attributes = allFeaturesResultRecord.GetInteger(8); + feature.Size = size; + } + } + } + } + } + } + } + + private void ImportExternalCabinetAsPayloads(Dtf.Database db, WixBundlePayloadRow packagePayload, ISet payloadNames) + { + if (db.Tables.Contains("Media")) + { + foreach (string cabinet in db.ExecuteStringQuery("SELECT `Cabinet` FROM `Media`")) + { + if (!String.IsNullOrEmpty(cabinet) && !cabinet.StartsWith("#", StringComparison.Ordinal)) + { + // If we didn't find the Payload as an existing child of the package, we need to + // add it. We expect the file to exist on-disk in the same relative location as + // the MSI expects to find it... + string cabinetName = Path.Combine(Path.GetDirectoryName(packagePayload.Name), cabinet); + + if (!payloadNames.Contains(cabinetName)) + { + string generatedId = Common.GenerateIdentifier("cab", packagePayload.Id, cabinet); + string payloadSourceFile = FileManager.ResolveRelatedFile(packagePayload.UnresolvedSourceFile, cabinet, "Cabinet", this.Facade.Package.SourceLineNumbers, BindStage.Normal); + + WixBundlePayloadRow payload = (WixBundlePayloadRow)this.PayloadTable.CreateRow(this.Facade.Package.SourceLineNumbers); + payload.Id = generatedId; + payload.Name = cabinetName; + payload.SourceFile = payloadSourceFile; + payload.Compressed = packagePayload.Compressed; + payload.UnresolvedSourceFile = cabinetName; + payload.Package = packagePayload.Package; + payload.Container = packagePayload.Container; + payload.ContentFile = true; + payload.EnableSignatureValidation = packagePayload.EnableSignatureValidation; + payload.Packaging = packagePayload.Packaging; + payload.ParentPackagePayload = packagePayload.Id; + } + } + } + } + } + + private long ImportExternalFileAsPayloadsAndReturnInstallSize(Dtf.Database db, WixBundlePayloadRow packagePayload, bool longNamesInImage, bool compressed, ISet payloadNames) + { + long size = 0; + + if (db.Tables.Contains("Component") && db.Tables.Contains("Directory") && db.Tables.Contains("File")) + { + Hashtable directories = new Hashtable(); + + // Load up the directory hash table so we will be able to resolve source paths + // for files in the MSI database. + using (Dtf.View view = db.OpenView("SELECT `Directory`, `Directory_Parent`, `DefaultDir` FROM `Directory`")) + { + view.Execute(); + while (true) + { + using (Dtf.Record record = view.Fetch()) + { + if (null == record) + { + break; + } + + string sourceName = Installer.GetName(record.GetString(3), true, longNamesInImage); + directories.Add(record.GetString(1), new ResolvedDirectory(record.GetString(2), sourceName)); + } + } + } + + // Resolve the source paths to external files and add each file size to the total + // install size of the package. + using (Dtf.View view = db.OpenView("SELECT `Directory_`, `File`, `FileName`, `File`.`Attributes`, `FileSize` FROM `Component`, `File` WHERE `Component`.`Component`=`File`.`Component_`")) + { + view.Execute(); + while (true) + { + using (Dtf.Record record = view.Fetch()) + { + if (null == record) + { + break; + } + + // Skip adding the loose files as payloads if it was suppressed. + if (!this.Facade.MsiPackage.SuppressLooseFilePayloadGeneration) + { + // If the file is explicitly uncompressed or the MSI is uncompressed and the file is not + // explicitly marked compressed then this is an external file. + if (MsiInterop.MsidbFileAttributesNoncompressed == (record.GetInteger(4) & MsiInterop.MsidbFileAttributesNoncompressed) || + (!compressed && 0 == (record.GetInteger(4) & MsiInterop.MsidbFileAttributesCompressed))) + { + string fileSourcePath = Binder.GetFileSourcePath(directories, record.GetString(1), record.GetString(3), compressed, longNamesInImage); + string name = Path.Combine(Path.GetDirectoryName(packagePayload.Name), fileSourcePath); + + if (!payloadNames.Contains(name)) + { + string generatedId = Common.GenerateIdentifier("f", packagePayload.Id, record.GetString(2)); + string payloadSourceFile = FileManager.ResolveRelatedFile(packagePayload.UnresolvedSourceFile, fileSourcePath, "File", this.Facade.Package.SourceLineNumbers, BindStage.Normal); + + WixBundlePayloadRow payload = (WixBundlePayloadRow)this.PayloadTable.CreateRow(this.Facade.Package.SourceLineNumbers); + payload.Id = generatedId; + payload.Name = name; + payload.SourceFile = payloadSourceFile; + payload.Compressed = packagePayload.Compressed; + payload.UnresolvedSourceFile = name; + payload.Package = packagePayload.Package; + payload.Container = packagePayload.Container; + payload.ContentFile = true; + payload.EnableSignatureValidation = packagePayload.EnableSignatureValidation; + payload.Packaging = packagePayload.Packaging; + payload.ParentPackagePayload = packagePayload.Id; + } + } + } + + size += record.GetInteger(5); + } + } + } + } + + return size; + } + + private void AddMsiProperty(string name, string value) + { + WixBundleMsiPropertyRow row = (WixBundleMsiPropertyRow)this.MsiPropertyTable.CreateRow(this.Facade.MsiPackage.SourceLineNumbers); + row.ChainPackageId = this.Facade.Package.WixChainItemId; + row.Name = name; + row.Value = value; + } + + private void ImportDependencyProviders(Dtf.Database db) + { + if (db.Tables.Contains("WixDependencyProvider")) + { + string query = "SELECT `ProviderKey`, `Version`, `DisplayName`, `Attributes` FROM `WixDependencyProvider`"; + + using (Dtf.View view = db.OpenView(query)) + { + view.Execute(); + while (true) + { + using (Dtf.Record record = view.Fetch()) + { + if (null == record) + { + break; + } + + // Import the provider key and attributes. + string providerKey = record.GetString(1); + string version = record.GetString(2) ?? this.Facade.MsiPackage.ProductVersion; + string displayName = record.GetString(3) ?? this.Facade.Package.DisplayName; + int attributes = record.GetInteger(4); + + ProvidesDependency dependency = new ProvidesDependency(providerKey, version, displayName, attributes); + dependency.Imported = true; + + this.Facade.Provides.Add(dependency); + } + } + } + } + } + + /// + /// Queries a Windows Installer database for a Property value. + /// + /// Database to query. + /// Property to examine. + /// String value for result or null if query doesn't match a single result. + private static string GetProperty(Dtf.Database db, string property) + { + try + { + return db.ExecuteScalar(PropertyQuery(property)).ToString(); + } + catch (Dtf.InstallerException) + { + } + + return null; + } + + /// + /// Queries a Windows Installer database to determine if one or more rows exist in the Property table. + /// + /// Database to query. + /// Property to examine. + /// True if query matches at least one result. + private static bool HasProperty(Dtf.Database db, string property) + { + try + { + return 0 < db.ExecuteQuery(PropertyQuery(property)).Count; + } + catch (Dtf.InstallerException) + { + } + + return false; + } + + private static string PropertyQuery(string property) + { + // quick sanity check that we'll be creating a valid query... + // TODO: Are there any other special characters we should be looking for? + Debug.Assert(!property.Contains("'")); + + return String.Format(CultureInfo.InvariantCulture, ProcessMsiPackageCommand.PropertySqlFormat, property); + } + } +} diff --git a/src/WixToolset.Core/Bind/Bundles/ProcessMspPackageCommand.cs b/src/WixToolset.Core/Bind/Bundles/ProcessMspPackageCommand.cs new file mode 100644 index 00000000..24063221 --- /dev/null +++ b/src/WixToolset.Core/Bind/Bundles/ProcessMspPackageCommand.cs @@ -0,0 +1,189 @@ +// 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. + +namespace WixToolset.Bind.Bundles +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Globalization; + using System.IO; + using System.Text; + using System.Xml; + using WixToolset.Data; + using WixToolset.Data.Rows; + using Dtf = WixToolset.Dtf.WindowsInstaller; + + /// + /// Initializes package state from the Msp contents. + /// + internal class ProcessMspPackageCommand : ICommand + { + private const string PatchMetadataFormat = "SELECT `Value` FROM `MsiPatchMetadata` WHERE `Property` = '{0}'"; + private static readonly Encoding XmlOutputEncoding = new UTF8Encoding(false); + + public RowDictionary AuthoredPayloads { private get; set; } + + public PackageFacade Facade { private get; set; } + + public Table WixBundlePatchTargetCodeTable { private get; set; } + + /// + /// Processes the Msp packages to add properties and payloads from the Msp packages. + /// + public void Execute() + { + WixBundlePayloadRow packagePayload = this.AuthoredPayloads.Get(this.Facade.Package.PackagePayload); + + string sourcePath = packagePayload.FullFileName; + + try + { + // Read data out of the msp database... + using (Dtf.SummaryInfo sumInfo = new Dtf.SummaryInfo(sourcePath, false)) + { + this.Facade.MspPackage.PatchCode = sumInfo.RevisionNumber.Substring(0, 38); + } + + using (Dtf.Database db = new Dtf.Database(sourcePath)) + { + if (String.IsNullOrEmpty(this.Facade.Package.DisplayName)) + { + this.Facade.Package.DisplayName = ProcessMspPackageCommand.GetPatchMetadataProperty(db, "DisplayName"); + } + + if (String.IsNullOrEmpty(this.Facade.Package.Description)) + { + this.Facade.Package.Description = ProcessMspPackageCommand.GetPatchMetadataProperty(db, "Description"); + } + + this.Facade.MspPackage.Manufacturer = ProcessMspPackageCommand.GetPatchMetadataProperty(db, "ManufacturerName"); + } + + this.ProcessPatchXml(packagePayload, sourcePath); + } + catch (Dtf.InstallerException e) + { + Messaging.Instance.OnMessage(WixErrors.UnableToReadPackageInformation(packagePayload.SourceLineNumbers, sourcePath, e.Message)); + return; + } + + if (String.IsNullOrEmpty(this.Facade.Package.CacheId)) + { + this.Facade.Package.CacheId = this.Facade.MspPackage.PatchCode; + } + } + + private void ProcessPatchXml(WixBundlePayloadRow packagePayload, string sourcePath) + { + HashSet uniqueTargetCodes = new HashSet(); + + string patchXml = Dtf.Installer.ExtractPatchXmlData(sourcePath); + + XmlDocument doc = new XmlDocument(); + doc.LoadXml(patchXml); + + XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable); + nsmgr.AddNamespace("p", "http://www.microsoft.com/msi/patch_applicability.xsd"); + + // Determine target ProductCodes and/or UpgradeCodes. + foreach (XmlNode node in doc.SelectNodes("/p:MsiPatch/p:TargetProduct", nsmgr)) + { + // If this patch targets a product code, this is the best case. + XmlNode targetCodeElement = node.SelectSingleNode("p:TargetProductCode", nsmgr); + WixBundlePatchTargetCodeAttributes attributes = WixBundlePatchTargetCodeAttributes.None; + + if (ProcessMspPackageCommand.TargetsCode(targetCodeElement)) + { + attributes = WixBundlePatchTargetCodeAttributes.TargetsProductCode; + } + else // maybe targets an upgrade code? + { + targetCodeElement = node.SelectSingleNode("p:UpgradeCode", nsmgr); + if (ProcessMspPackageCommand.TargetsCode(targetCodeElement)) + { + attributes = WixBundlePatchTargetCodeAttributes.TargetsUpgradeCode; + } + else // this patch targets an unknown number of products + { + this.Facade.MspPackage.Attributes |= WixBundleMspPackageAttributes.TargetUnspecified; + } + } + + string targetCode = targetCodeElement.InnerText; + + if (uniqueTargetCodes.Add(targetCode)) + { + WixBundlePatchTargetCodeRow row = (WixBundlePatchTargetCodeRow)this.WixBundlePatchTargetCodeTable.CreateRow(packagePayload.SourceLineNumbers); + row.MspPackageId = packagePayload.Id; + row.TargetCode = targetCode; + row.Attributes = attributes; + } + } + + // Suppress patch sequence data for improved performance. + XmlNode root = doc.DocumentElement; + foreach (XmlNode node in root.SelectNodes("p:SequenceData", nsmgr)) + { + root.RemoveChild(node); + } + + // Save the XML as compact as possible. + using (StringWriter writer = new StringWriter()) + { + XmlWriterSettings settings = new XmlWriterSettings() + { + Encoding = ProcessMspPackageCommand.XmlOutputEncoding, + Indent = false, + NewLineChars = string.Empty, + NewLineHandling = NewLineHandling.Replace, + }; + + using (XmlWriter xmlWriter = XmlWriter.Create(writer, settings)) + { + doc.WriteTo(xmlWriter); + } + + this.Facade.MspPackage.PatchXml = writer.ToString(); + } + } + + /// + /// Queries a Windows Installer patch database for a Property value from the MsiPatchMetadata table. + /// + /// Database to query. + /// Property to examine. + /// String value for result or null if query doesn't match a single result. + private static string GetPatchMetadataProperty(Dtf.Database db, string property) + { + try + { + return db.ExecuteScalar(PatchMetadataPropertyQuery(property)).ToString(); + } + catch (Dtf.InstallerException) + { + } + + return null; + } + + private static string PatchMetadataPropertyQuery(string property) + { + // quick sanity check that we'll be creating a valid query... + // TODO: Are there any other special characters we should be looking for? + Debug.Assert(!property.Contains("'")); + + return String.Format(CultureInfo.InvariantCulture, ProcessMspPackageCommand.PatchMetadataFormat, property); + } + + private static bool TargetsCode(XmlNode node) + { + if (null != node) + { + XmlAttribute attr = node.Attributes["Validate"]; + return null != attr && "true".Equals(attr.Value); + } + + return false; + } + } +} diff --git a/src/WixToolset.Core/Bind/Bundles/ProcessMsuPackageCommand.cs b/src/WixToolset.Core/Bind/Bundles/ProcessMsuPackageCommand.cs new file mode 100644 index 00000000..ba59f5f5 --- /dev/null +++ b/src/WixToolset.Core/Bind/Bundles/ProcessMsuPackageCommand.cs @@ -0,0 +1,30 @@ +// 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. + +namespace WixToolset.Bind.Bundles +{ + using System; + using WixToolset.Data; + using WixToolset.Data.Rows; + + /// + /// Processes the Msu packages to add properties and payloads from the Msu packages. + /// + internal class ProcessMsuPackageCommand : ICommand + { + public RowDictionary AuthoredPayloads { private get; set; } + + public PackageFacade Facade { private get; set; } + + public void Execute() + { + WixBundlePayloadRow packagePayload = this.AuthoredPayloads.Get(this.Facade.Package.PackagePayload); + + if (String.IsNullOrEmpty(this.Facade.Package.CacheId)) + { + this.Facade.Package.CacheId = packagePayload.Hash; + } + + this.Facade.Package.PerMachine = YesNoDefaultType.Yes; // MSUs are always per-machine. + } + } +} diff --git a/src/WixToolset.Core/Bind/Bundles/ProcessPayloadsCommand.cs b/src/WixToolset.Core/Bind/Bundles/ProcessPayloadsCommand.cs new file mode 100644 index 00000000..a83a7a4a --- /dev/null +++ b/src/WixToolset.Core/Bind/Bundles/ProcessPayloadsCommand.cs @@ -0,0 +1,159 @@ +// 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. + +namespace WixToolset.Bind.Bundles +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + using System.Security.Cryptography; + using System.Security.Cryptography.X509Certificates; + using System.Text; + using WixToolset.Data; + using WixToolset.Data.Rows; + + internal class ProcessPayloadsCommand : ICommand + { + private static readonly Version EmptyVersion = new Version(0, 0, 0, 0); + + public IEnumerable Payloads { private get; set; } + + public PackagingType DefaultPackaging { private get; set; } + + public string LayoutDirectory { private get; set; } + + public IEnumerable FileTransfers { get; private set; } + + public void Execute() + { + List fileTransfers = new List(); + + foreach (WixBundlePayloadRow payload in this.Payloads) + { + string normalizedPath = payload.Name.Replace('\\', '/'); + if (normalizedPath.StartsWith("../", StringComparison.Ordinal) || normalizedPath.Contains("/../")) + { + Messaging.Instance.OnMessage(WixErrors.PayloadMustBeRelativeToCache(payload.SourceLineNumbers, "Payload", "Name", payload.Name)); + } + + // Embedded files (aka: files from binary .wixlibs) are not content files (because they are hidden + // in the .wixlib). + ObjectField field = (ObjectField)payload.Fields[2]; + payload.ContentFile = !field.EmbeddedFileIndex.HasValue; + + this.UpdatePayloadPackagingType(payload); + + if (String.IsNullOrEmpty(payload.SourceFile)) + { + // Remote payloads obviously cannot be embedded. + Debug.Assert(PackagingType.Embedded != payload.Packaging); + } + else // not a remote payload so we have a lot more to update. + { + this.UpdatePayloadFileInformation(payload); + + this.UpdatePayloadVersionInformation(payload); + + // External payloads need to be transfered. + if (PackagingType.External == payload.Packaging) + { + FileTransfer transfer; + if (FileTransfer.TryCreate(payload.FullFileName, Path.Combine(this.LayoutDirectory, payload.Name), false, "Payload", payload.SourceLineNumbers, out transfer)) + { + fileTransfers.Add(transfer); + } + } + } + } + + this.FileTransfers = fileTransfers; + } + + private void UpdatePayloadPackagingType(WixBundlePayloadRow payload) + { + if (PackagingType.Unknown == payload.Packaging) + { + if (YesNoDefaultType.Yes == payload.Compressed) + { + payload.Packaging = PackagingType.Embedded; + } + else if (YesNoDefaultType.No == payload.Compressed) + { + payload.Packaging = PackagingType.External; + } + else + { + payload.Packaging = this.DefaultPackaging; + } + } + + // Embedded payloads that are not assigned a container already are placed in the default attached + // container. + if (PackagingType.Embedded == payload.Packaging && String.IsNullOrEmpty(payload.Container)) + { + payload.Container = Compiler.BurnDefaultAttachedContainerId; + } + } + + private void UpdatePayloadFileInformation(WixBundlePayloadRow payload) + { + FileInfo fileInfo = new FileInfo(payload.SourceFile); + + if (null != fileInfo) + { + payload.FileSize = (int)fileInfo.Length; + + payload.Hash = Common.GetFileHash(fileInfo.FullName); + + // Try to get the certificate if the payload is a signed file and we're not suppressing signature validation. + if (payload.EnableSignatureValidation) + { + X509Certificate2 certificate = null; + try + { + certificate = new X509Certificate2(fileInfo.FullName); + } + catch (CryptographicException) // we don't care about non-signed files. + { + } + + // If there is a certificate, remember its hashed public key identifier and thumbprint. + if (null != certificate) + { + byte[] publicKeyIdentifierHash = new byte[128]; + uint publicKeyIdentifierHashSize = (uint)publicKeyIdentifierHash.Length; + + WixToolset.Core.Native.NativeMethods.HashPublicKeyInfo(certificate.Handle, publicKeyIdentifierHash, ref publicKeyIdentifierHashSize); + StringBuilder sb = new StringBuilder(((int)publicKeyIdentifierHashSize + 1) * 2); + for (int i = 0; i < publicKeyIdentifierHashSize; ++i) + { + sb.AppendFormat("{0:X2}", publicKeyIdentifierHash[i]); + } + + payload.PublicKey = sb.ToString(); + payload.Thumbprint = certificate.Thumbprint; + } + } + } + } + + private void UpdatePayloadVersionInformation(WixBundlePayloadRow payload) + { + FileVersionInfo versionInfo = FileVersionInfo.GetVersionInfo(payload.SourceFile); + + if (null != versionInfo) + { + // Use the fixed version info block for the file since the resource text may not be a dotted quad. + Version version = new Version(versionInfo.ProductMajorPart, versionInfo.ProductMinorPart, versionInfo.ProductBuildPart, versionInfo.ProductPrivatePart); + + if (ProcessPayloadsCommand.EmptyVersion != version) + { + payload.Version = version.ToString(); + } + + payload.Description = versionInfo.FileDescription; + payload.DisplayName = versionInfo.ProductName; + } + } + } +} diff --git a/src/WixToolset.Core/Bind/Bundles/VerifyPayloadsWithCatalogCommand.cs b/src/WixToolset.Core/Bind/Bundles/VerifyPayloadsWithCatalogCommand.cs new file mode 100644 index 00000000..9c614c26 --- /dev/null +++ b/src/WixToolset.Core/Bind/Bundles/VerifyPayloadsWithCatalogCommand.cs @@ -0,0 +1,148 @@ +// 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. + +namespace WixToolset.Bind.Bundles +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Runtime.InteropServices; + using System.Text; + using WixToolset.Data; + using WixToolset.Data.Rows; + + internal class VerifyPayloadsWithCatalogCommand : ICommand + { + public IEnumerable Catalogs { private get; set; } + + public IEnumerable Payloads { private get; set; } + + public void Execute() + { + List catalogIdsWithPaths = this.Catalogs + .Join(this.Payloads, + catalog => catalog.Payload, + payload => payload.Id, + (catalog, payload) => new CatalogIdWithPath() { Id = catalog.Id, FullPath = Path.GetFullPath(payload.SourceFile) }) + .ToList(); + + foreach (WixBundlePayloadRow payloadInfo in this.Payloads) + { + // Payloads that are not embedded should be verfied. + if (String.IsNullOrEmpty(payloadInfo.EmbeddedId)) + { + bool validated = false; + + foreach (CatalogIdWithPath catalog in catalogIdsWithPaths) + { + if (!validated) + { + // Get the file hash + uint cryptHashSize = 20; + byte[] cryptHashBytes = new byte[cryptHashSize]; + int error; + IntPtr fileHandle = IntPtr.Zero; + using (FileStream payloadStream = File.OpenRead(payloadInfo.FullFileName)) + { + // Get the file handle + fileHandle = payloadStream.SafeFileHandle.DangerousGetHandle(); + + // 20 bytes is usually the hash size. Future hashes may be bigger + if (!VerifyInterop.CryptCATAdminCalcHashFromFileHandle(fileHandle, ref cryptHashSize, cryptHashBytes, 0)) + { + error = Marshal.GetLastWin32Error(); + + if (VerifyInterop.ErrorInsufficientBuffer == error) + { + error = 0; + cryptHashBytes = new byte[cryptHashSize]; + if (!VerifyInterop.CryptCATAdminCalcHashFromFileHandle(fileHandle, ref cryptHashSize, cryptHashBytes, 0)) + { + error = Marshal.GetLastWin32Error(); + } + } + + if (0 != error) + { + Messaging.Instance.OnMessage(WixErrors.CatalogFileHashFailed(payloadInfo.FullFileName, error)); + } + } + } + + VerifyInterop.WinTrustCatalogInfo catalogData = new VerifyInterop.WinTrustCatalogInfo(); + VerifyInterop.WinTrustData trustData = new VerifyInterop.WinTrustData(); + try + { + // Create WINTRUST_CATALOG_INFO structure + catalogData.cbStruct = (uint)Marshal.SizeOf(catalogData); + catalogData.cbCalculatedFileHash = cryptHashSize; + catalogData.pbCalculatedFileHash = Marshal.AllocCoTaskMem((int)cryptHashSize); + Marshal.Copy(cryptHashBytes, 0, catalogData.pbCalculatedFileHash, (int)cryptHashSize); + + StringBuilder hashString = new StringBuilder(); + foreach (byte hashByte in cryptHashBytes) + { + hashString.Append(hashByte.ToString("X2")); + } + catalogData.pcwszMemberTag = hashString.ToString(); + + // The file names need to be lower case for older OSes + catalogData.pcwszMemberFilePath = payloadInfo.FullFileName.ToLowerInvariant(); + catalogData.pcwszCatalogFilePath = catalog.FullPath.ToLowerInvariant(); + + // Create WINTRUST_DATA structure + trustData.cbStruct = (uint)Marshal.SizeOf(trustData); + trustData.dwUIChoice = VerifyInterop.WTD_UI_NONE; + trustData.fdwRevocationChecks = VerifyInterop.WTD_REVOKE_NONE; + trustData.dwUnionChoice = VerifyInterop.WTD_CHOICE_CATALOG; + trustData.dwStateAction = VerifyInterop.WTD_STATEACTION_VERIFY; + trustData.dwProvFlags = VerifyInterop.WTD_REVOCATION_CHECK_NONE; + + // Create the structure pointers for unmanaged + trustData.pCatalog = Marshal.AllocCoTaskMem(Marshal.SizeOf(catalogData)); + Marshal.StructureToPtr(catalogData, trustData.pCatalog, false); + + // Call WinTrustVerify to validate the file with the catalog + IntPtr noWindow = new IntPtr(-1); + Guid verifyGuid = new Guid(VerifyInterop.GenericVerify2); + long verifyResult = VerifyInterop.WinVerifyTrust(noWindow, ref verifyGuid, ref trustData); + if (0 == verifyResult) + { + payloadInfo.Catalog = catalog.Id; + validated = true; + break; + } + } + finally + { + // Free the structure memory + if (IntPtr.Zero != trustData.pCatalog) + { + Marshal.FreeCoTaskMem(trustData.pCatalog); + } + + if (IntPtr.Zero != catalogData.pbCalculatedFileHash) + { + Marshal.FreeCoTaskMem(catalogData.pbCalculatedFileHash); + } + } + } + } + + // Error message if the file was not validated by one of the catalogs + if (!validated) + { + Messaging.Instance.OnMessage(WixErrors.CatalogVerificationFailed(payloadInfo.FullFileName)); + } + } + } + } + + private class CatalogIdWithPath + { + public string Id { get; set; } + + public string FullPath { get; set; } + } + } +} diff --git a/src/WixToolset.Core/Bind/Databases/AssignMediaCommand.cs b/src/WixToolset.Core/Bind/Databases/AssignMediaCommand.cs new file mode 100644 index 00000000..5e2650e9 --- /dev/null +++ b/src/WixToolset.Core/Bind/Databases/AssignMediaCommand.cs @@ -0,0 +1,314 @@ +// 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. + +namespace WixToolset.Bind.Databases +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using WixToolset.Data; + using WixToolset.Data.Rows; + + /// + /// AssignMediaCommand assigns files to cabs based on Media or MediaTemplate rows. + /// + public class AssignMediaCommand : ICommand + { + public AssignMediaCommand() + { + this.CabinetNameTemplate = "Cab{0}.cab"; + } + + public Output Output { private get; set; } + + public bool FilesCompressed { private get; set; } + + public string CabinetNameTemplate { private get; set; } + + public IEnumerable FileFacades { private get; set; } + + public TableDefinitionCollection TableDefinitions { private get; set; } + + /// + /// Gets cabinets with their file rows. + /// + public Dictionary> FileFacadesByCabinetMedia { get; private set; } + + /// + /// Get media rows. + /// + public RowDictionary MediaRows { get; private set; } + + /// + /// Get uncompressed file rows. This will contain file rows of File elements that are marked with compression=no. + /// This contains all the files when Package element is marked with compression=no + /// + public IEnumerable UncompressedFileFacades { get; private set; } + + public void Execute() + { + Dictionary> filesByCabinetMedia = new Dictionary>(); + + RowDictionary mediaRows = new RowDictionary(); + + List uncompressedFiles = new List(); + + MediaRow mergeModuleMediaRow = null; + Table mediaTable = this.Output.Tables["Media"]; + Table mediaTemplateTable = this.Output.Tables["WixMediaTemplate"]; + + // If both tables are authored, it is an error. + if ((mediaTemplateTable != null && mediaTemplateTable.Rows.Count > 0) && (mediaTable != null && mediaTable.Rows.Count > 1)) + { + throw new WixException(WixErrors.MediaTableCollision(null)); + } + + // When building merge module, all the files go to "#MergeModule.CABinet". + if (OutputType.Module == this.Output.Type) + { + Table mergeModuleMediaTable = new Table(null, this.TableDefinitions["Media"]); + mergeModuleMediaRow = (MediaRow)mergeModuleMediaTable.CreateRow(null); + mergeModuleMediaRow.Cabinet = "#MergeModule.CABinet"; + + filesByCabinetMedia.Add(mergeModuleMediaRow, new List()); + } + + if (OutputType.Module == this.Output.Type || null == mediaTemplateTable) + { + this.ManuallyAssignFiles(mediaTable, mergeModuleMediaRow, this.FileFacades, filesByCabinetMedia, mediaRows, uncompressedFiles); + } + else + { + this.AutoAssignFiles(mediaTable, this.FileFacades, filesByCabinetMedia, mediaRows, uncompressedFiles); + } + + this.FileFacadesByCabinetMedia = new Dictionary>(); + + foreach (var mediaRowWithFiles in filesByCabinetMedia) + { + this.FileFacadesByCabinetMedia.Add(mediaRowWithFiles.Key, mediaRowWithFiles.Value); + } + + this.MediaRows = mediaRows; + + this.UncompressedFileFacades = uncompressedFiles; + } + + /// + /// Assign files to cabinets based on MediaTemplate authoring. + /// + /// FileRowCollection + private void AutoAssignFiles(Table mediaTable, IEnumerable fileFacades, Dictionary> filesByCabinetMedia, RowDictionary mediaRows, List uncompressedFiles) + { + const int MaxCabIndex = 999; + + ulong currentPreCabSize = 0; + ulong maxPreCabSizeInBytes; + int maxPreCabSizeInMB = 0; + int currentCabIndex = 0; + + MediaRow currentMediaRow = null; + + Table mediaTemplateTable = this.Output.Tables["WixMediaTemplate"]; + + // Auto assign files to cabinets based on maximum uncompressed media size + mediaTable.Rows.Clear(); + WixMediaTemplateRow mediaTemplateRow = (WixMediaTemplateRow)mediaTemplateTable.Rows[0]; + + if (!String.IsNullOrEmpty(mediaTemplateRow.CabinetTemplate)) + { + this.CabinetNameTemplate = mediaTemplateRow.CabinetTemplate; + } + + string mumsString = Environment.GetEnvironmentVariable("WIX_MUMS"); + + try + { + // Override authored mums value if environment variable is authored. + if (!String.IsNullOrEmpty(mumsString)) + { + maxPreCabSizeInMB = Int32.Parse(mumsString); + } + else + { + maxPreCabSizeInMB = mediaTemplateRow.MaximumUncompressedMediaSize; + } + + maxPreCabSizeInBytes = (ulong)maxPreCabSizeInMB * 1024 * 1024; + } + catch (FormatException) + { + throw new WixException(WixErrors.IllegalEnvironmentVariable("WIX_MUMS", mumsString)); + } + catch (OverflowException) + { + throw new WixException(WixErrors.MaximumUncompressedMediaSizeTooLarge(null, maxPreCabSizeInMB)); + } + + foreach (FileFacade facade in this.FileFacades) + { + // When building a product, if the current file is not to be compressed or if + // the package set not to be compressed, don't cab it. + if (OutputType.Product == this.Output.Type && + (YesNoType.No == facade.File.Compressed || + (YesNoType.NotSet == facade.File.Compressed && !this.FilesCompressed))) + { + uncompressedFiles.Add(facade); + continue; + } + + if (currentCabIndex == MaxCabIndex) + { + // Associate current file with last cab (irrespective of the size) and cab index is not incremented anymore. + List cabinetFiles = filesByCabinetMedia[currentMediaRow]; + facade.WixFile.DiskId = currentCabIndex; + cabinetFiles.Add(facade); + continue; + } + + // Update current cab size. + currentPreCabSize += (ulong)facade.File.FileSize; + + if (currentPreCabSize > maxPreCabSizeInBytes) + { + // Overflow due to current file + currentMediaRow = this.AddMediaRow(mediaTemplateRow, mediaTable, ++currentCabIndex); + mediaRows.Add(currentMediaRow); + filesByCabinetMedia.Add(currentMediaRow, new List()); + + List cabinetFileRows = filesByCabinetMedia[currentMediaRow]; + facade.WixFile.DiskId = currentCabIndex; + cabinetFileRows.Add(facade); + // Now files larger than MaxUncompressedMediaSize will be the only file in its cabinet so as to respect MaxUncompressedMediaSize + currentPreCabSize = (ulong)facade.File.FileSize; + } + else + { + // File fits in the current cab. + if (currentMediaRow == null) + { + // Create new cab and MediaRow + currentMediaRow = this.AddMediaRow(mediaTemplateRow, mediaTable, ++currentCabIndex); + mediaRows.Add(currentMediaRow); + filesByCabinetMedia.Add(currentMediaRow, new List()); + } + + // Associate current file with current cab. + List cabinetFiles = filesByCabinetMedia[currentMediaRow]; + facade.WixFile.DiskId = currentCabIndex; + cabinetFiles.Add(facade); + } + } + + // If there are uncompressed files and no MediaRow, create a default one. + if (uncompressedFiles.Count > 0 && mediaTable.Rows.Count == 0) + { + MediaRow defaultMediaRow = (MediaRow)mediaTable.CreateRow(null); + defaultMediaRow.DiskId = 1; + mediaRows.Add(defaultMediaRow); + } + } + + /// + /// Assign files to cabinets based on Media authoring. + /// + /// + /// + /// + private void ManuallyAssignFiles(Table mediaTable, MediaRow mergeModuleMediaRow, IEnumerable fileFacades, Dictionary> filesByCabinetMedia, RowDictionary mediaRows, List uncompressedFiles) + { + if (OutputType.Module != this.Output.Type) + { + if (null != mediaTable) + { + Dictionary cabinetMediaRows = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + foreach (MediaRow mediaRow in mediaTable.Rows) + { + // If the Media row has a cabinet, make sure it is unique across all Media rows. + if (!String.IsNullOrEmpty(mediaRow.Cabinet)) + { + MediaRow existingRow; + if (cabinetMediaRows.TryGetValue(mediaRow.Cabinet, out existingRow)) + { + Messaging.Instance.OnMessage(WixErrors.DuplicateCabinetName(mediaRow.SourceLineNumbers, mediaRow.Cabinet)); + Messaging.Instance.OnMessage(WixErrors.DuplicateCabinetName2(existingRow.SourceLineNumbers, existingRow.Cabinet)); + } + else + { + cabinetMediaRows.Add(mediaRow.Cabinet, mediaRow); + } + } + + mediaRows.Add(mediaRow); + } + } + + foreach (MediaRow mediaRow in mediaRows.Values) + { + if (null != mediaRow.Cabinet) + { + filesByCabinetMedia.Add(mediaRow, new List()); + } + } + } + + foreach (FileFacade facade in fileFacades) + { + if (OutputType.Module == this.Output.Type) + { + filesByCabinetMedia[mergeModuleMediaRow].Add(facade); + } + else + { + MediaRow mediaRow; + if (!mediaRows.TryGetValue(facade.WixFile.DiskId.ToString(CultureInfo.InvariantCulture), out mediaRow)) + { + Messaging.Instance.OnMessage(WixErrors.MissingMedia(facade.File.SourceLineNumbers, facade.WixFile.DiskId)); + continue; + } + + // When building a product, if the current file is not to be compressed or if + // the package set not to be compressed, don't cab it. + if (OutputType.Product == this.Output.Type && + (YesNoType.No == facade.File.Compressed || + (YesNoType.NotSet == facade.File.Compressed && !this.FilesCompressed))) + { + uncompressedFiles.Add(facade); + } + else // file is marked compressed. + { + List cabinetFiles; + if (filesByCabinetMedia.TryGetValue(mediaRow, out cabinetFiles)) + { + cabinetFiles.Add(facade); + } + else + { + Messaging.Instance.OnMessage(WixErrors.ExpectedMediaCabinet(facade.File.SourceLineNumbers, facade.File.File, facade.WixFile.DiskId)); + } + } + } + } + } + + /// + /// Adds a row to the media table with cab name template filled in. + /// + /// + /// + /// + private MediaRow AddMediaRow(WixMediaTemplateRow mediaTemplateRow, Table mediaTable, int cabIndex) + { + MediaRow currentMediaRow = (MediaRow)mediaTable.CreateRow(mediaTemplateRow.SourceLineNumbers); + currentMediaRow.DiskId = cabIndex; + currentMediaRow.Cabinet = String.Format(CultureInfo.InvariantCulture, this.CabinetNameTemplate, cabIndex); + + Table wixMediaTable = this.Output.EnsureTable(this.TableDefinitions["WixMedia"]); + WixMediaRow row = (WixMediaRow)wixMediaTable.CreateRow(mediaTemplateRow.SourceLineNumbers); + row.DiskId = cabIndex; + row.CompressionLevel = mediaTemplateRow.CompressionLevel; + + return currentMediaRow; + } + } +} diff --git a/src/WixToolset.Core/Bind/Databases/BindSummaryInfoCommand.cs b/src/WixToolset.Core/Bind/Databases/BindSummaryInfoCommand.cs new file mode 100644 index 00000000..95bd4cf0 --- /dev/null +++ b/src/WixToolset.Core/Bind/Databases/BindSummaryInfoCommand.cs @@ -0,0 +1,135 @@ +// 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. + +namespace WixToolset.Bind.Databases +{ + using System; + using System.Globalization; + using WixToolset.Data; + + /// + /// Binds the summary information table of a database. + /// + internal class BindSummaryInfoCommand : ICommand + { + /// + /// The output to bind. + /// + public Output Output { private get; set; } + + /// + /// Returns a flag indicating if files are compressed by default. + /// + public bool Compressed { get; private set; } + + /// + /// Returns a flag indicating if uncompressed files use long filenames. + /// + public bool LongNames { get; private set; } + + public int InstallerVersion { get; private set; } + + /// + /// Modularization guid, or null if the output is not a module. + /// + public string ModularizationGuid { get; private set; } + + public void Execute() + { + this.Compressed = false; + this.LongNames = false; + this.InstallerVersion = 0; + this.ModularizationGuid = null; + + Table summaryInformationTable = this.Output.Tables["_SummaryInformation"]; + + if (null != summaryInformationTable) + { + bool foundCreateDataTime = false; + bool foundLastSaveDataTime = false; + bool foundCreatingApplication = false; + string now = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss", CultureInfo.InvariantCulture); + + foreach (Row summaryInformationRow in summaryInformationTable.Rows) + { + switch (summaryInformationRow.FieldAsInteger(0)) + { + case 1: // PID_CODEPAGE + // make sure the code page is an int and not a web name or null + string codepage = summaryInformationRow.FieldAsString(1); + + if (null == codepage) + { + codepage = "0"; + } + else + { + summaryInformationRow[1] = Common.GetValidCodePage(codepage, false, false, summaryInformationRow.SourceLineNumbers).ToString(CultureInfo.InvariantCulture); + } + break; + case 9: // PID_REVNUMBER + string packageCode = (string)summaryInformationRow[1]; + + if (OutputType.Module == this.Output.Type) + { + this.ModularizationGuid = packageCode.Substring(1, 36).Replace('-', '_'); + } + else if ("*" == packageCode) + { + // set the revision number (package/patch code) if it should be automatically generated + summaryInformationRow[1] = Common.GenerateGuid(); + } + break; + case 12: // PID_CREATE_DTM + foundCreateDataTime = true; + break; + case 13: // PID_LASTSAVE_DTM + foundLastSaveDataTime = true; + break; + case 14: + this.InstallerVersion = summaryInformationRow.FieldAsInteger(1); + break; + case 15: // PID_WORDCOUNT + if (OutputType.Patch == this.Output.Type) + { + this.LongNames = true; + this.Compressed = true; + } + else + { + this.LongNames = (0 == (summaryInformationRow.FieldAsInteger(1) & 1)); + this.Compressed = (2 == (summaryInformationRow.FieldAsInteger(1) & 2)); + } + break; + case 18: // PID_APPNAME + foundCreatingApplication = true; + break; + } + } + + // add a summary information row for the create time/date property if its not already set + if (!foundCreateDataTime) + { + Row createTimeDateRow = summaryInformationTable.CreateRow(null); + createTimeDateRow[0] = 12; + createTimeDateRow[1] = now; + } + + // add a summary information row for the last save time/date property if its not already set + if (!foundLastSaveDataTime) + { + Row lastSaveTimeDateRow = summaryInformationTable.CreateRow(null); + lastSaveTimeDateRow[0] = 13; + lastSaveTimeDateRow[1] = now; + } + + // add a summary information row for the creating application property if its not already set + if (!foundCreatingApplication) + { + Row creatingApplicationRow = summaryInformationTable.CreateRow(null); + creatingApplicationRow[0] = 18; + creatingApplicationRow[1] = String.Format(CultureInfo.InvariantCulture, AppCommon.GetCreatingApplicationString()); + } + } + } + } +} diff --git a/src/WixToolset.Core/Bind/Databases/CabinetBuilder.cs b/src/WixToolset.Core/Bind/Databases/CabinetBuilder.cs new file mode 100644 index 00000000..2de6ec25 --- /dev/null +++ b/src/WixToolset.Core/Bind/Databases/CabinetBuilder.cs @@ -0,0 +1,176 @@ +// 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. + +namespace WixToolset.Bind.Databases +{ + using System; + using System.Collections; + using System.IO; + using System.Linq; + using System.Threading; + using WixToolset.Cab; + using WixToolset.Data; + using WixToolset.Data.Rows; + + /// + /// Builds cabinets using multiple threads. This implements a thread pool that generates cabinets with multiple + /// threads. Unlike System.Threading.ThreadPool, it waits until all threads are finished. + /// + internal sealed class CabinetBuilder + { + private Queue cabinetWorkItems; + private object lockObject; + private int threadCount; + + // Address of Binder's callback function for Cabinet Splitting + private IntPtr newCabNamesCallBackAddress; + + public int MaximumCabinetSizeForLargeFileSplitting { get; set; } + public int MaximumUncompressedMediaSize { get; set; } + + /// + /// Instantiate a new CabinetBuilder. + /// + /// number of threads to use + /// Address of Binder's callback function for Cabinet Splitting + public CabinetBuilder(int threadCount, IntPtr newCabNamesCallBackAddress) + { + if (0 >= threadCount) + { + throw new ArgumentOutOfRangeException("threadCount"); + } + + this.cabinetWorkItems = new Queue(); + this.lockObject = new object(); + + this.threadCount = threadCount; + + // Set Address of Binder's callback function for Cabinet Splitting + this.newCabNamesCallBackAddress = newCabNamesCallBackAddress; + } + + /// + /// Enqueues a CabinetWorkItem to the queue. + /// + /// cabinet work item + public void Enqueue(CabinetWorkItem cabinetWorkItem) + { + this.cabinetWorkItems.Enqueue(cabinetWorkItem); + } + + /// + /// Create the queued cabinets. + /// + /// error message number (zero if no error) + public void CreateQueuedCabinets() + { + // don't create more threads than the number of cabinets to build + if (this.cabinetWorkItems.Count < this.threadCount) + { + this.threadCount = this.cabinetWorkItems.Count; + } + + if (0 < this.threadCount) + { + Thread[] threads = new Thread[this.threadCount]; + + for (int i = 0; i < threads.Length; i++) + { + threads[i] = new Thread(new ThreadStart(this.ProcessWorkItems)); + threads[i].Start(); + } + + // wait for all threads to finish + foreach (Thread thread in threads) + { + thread.Join(); + } + } + } + + /// + /// This function gets called by multiple threads to do actual work. + /// It takes one work item at a time and calls this.CreateCabinet(). + /// It does not return until cabinetWorkItems queue is empty + /// + private void ProcessWorkItems() + { + try + { + while (true) + { + CabinetWorkItem cabinetWorkItem; + + lock (this.cabinetWorkItems) + { + // check if there are any more cabinets to create + if (0 == this.cabinetWorkItems.Count) + { + break; + } + + cabinetWorkItem = (CabinetWorkItem)this.cabinetWorkItems.Dequeue(); + } + + // create a cabinet + this.CreateCabinet(cabinetWorkItem); + } + } + catch (WixException we) + { + Messaging.Instance.OnMessage(we.Error); + } + catch (Exception e) + { + Messaging.Instance.OnMessage(WixErrors.UnexpectedException(e.Message, e.GetType().ToString(), e.StackTrace)); + } + } + + /// + /// Creates a cabinet using the wixcab.dll interop layer. + /// + /// CabinetWorkItem containing information about the cabinet to create. + private void CreateCabinet(CabinetWorkItem cabinetWorkItem) + { + Messaging.Instance.OnMessage(WixVerboses.CreateCabinet(cabinetWorkItem.CabinetFile)); + + int maxCabinetSize = 0; // The value of 0 corresponds to default of 2GB which means no cabinet splitting + ulong maxPreCompressedSizeInBytes = 0; + + if (MaximumCabinetSizeForLargeFileSplitting != 0) + { + // User Specified Max Cab Size for File Splitting, So Check if this cabinet has a single file larger than MaximumUncompressedFileSize + // If a file is larger than MaximumUncompressedFileSize, then the cabinet containing it will have only this file + if (1 == cabinetWorkItem.FileFacades.Count()) + { + // Cabinet has Single File, Check if this is Large File than needs Splitting into Multiple cabs + // Get the Value for Max Uncompressed Media Size + maxPreCompressedSizeInBytes = (ulong)MaximumUncompressedMediaSize * 1024 * 1024; + + foreach (FileFacade facade in cabinetWorkItem.FileFacades) // No other easy way than looping to get the only row + { + if ((ulong)facade.File.FileSize >= maxPreCompressedSizeInBytes) + { + // If file is larger than MaximumUncompressedFileSize set Maximum Cabinet Size for Cabinet Splitting + maxCabinetSize = MaximumCabinetSizeForLargeFileSplitting; + } + } + } + } + + // create the cabinet file + string cabinetFileName = Path.GetFileName(cabinetWorkItem.CabinetFile); + string cabinetDirectory = Path.GetDirectoryName(cabinetWorkItem.CabinetFile); + + using (WixCreateCab cab = new WixCreateCab(cabinetFileName, cabinetDirectory, cabinetWorkItem.FileFacades.Count(), maxCabinetSize, cabinetWorkItem.MaxThreshold, cabinetWorkItem.CompressionLevel)) + { + foreach (FileFacade facade in cabinetWorkItem.FileFacades) + { + cab.AddFile(facade); + } + + cab.Complete(newCabNamesCallBackAddress); + } + } + } +} + diff --git a/src/WixToolset.Core/Bind/Databases/CabinetWorkItem.cs b/src/WixToolset.Core/Bind/Databases/CabinetWorkItem.cs new file mode 100644 index 00000000..20241bc9 --- /dev/null +++ b/src/WixToolset.Core/Bind/Databases/CabinetWorkItem.cs @@ -0,0 +1,78 @@ +// 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. + +namespace WixToolset.Bind.Databases +{ + using System.Collections.Generic; + using WixToolset.Data; + using WixToolset.Data.Rows; + + /// + /// A cabinet builder work item. + /// + internal sealed class CabinetWorkItem + { + private string cabinetFile; + private CompressionLevel compressionLevel; + //private BinderFileManager binderFileManager; + private int maxThreshold; + + /// + /// Instantiate a new CabinetWorkItem. + /// + /// The collection of files in this cabinet. + /// The cabinet file. + /// Maximum threshold for each cabinet. + /// The compression level of the cabinet. + /// The binder file manager. + public CabinetWorkItem(IEnumerable fileFacades, string cabinetFile, int maxThreshold, CompressionLevel compressionLevel /*, BinderFileManager binderFileManager*/) + { + this.cabinetFile = cabinetFile; + this.compressionLevel = compressionLevel; + this.FileFacades = fileFacades; + //this.binderFileManager = binderFileManager; + this.maxThreshold = maxThreshold; + } + + /// + /// Gets the cabinet file. + /// + /// The cabinet file. + public string CabinetFile + { + get { return this.cabinetFile; } + } + + /// + /// Gets the compression level of the cabinet. + /// + /// The compression level of the cabinet. + public CompressionLevel CompressionLevel + { + get { return this.compressionLevel; } + } + + /// + /// Gets the collection of files in this cabinet. + /// + /// The collection of files in this cabinet. + public IEnumerable FileFacades { get; private set; } + + /// + /// Gets the binder file manager. + /// + /// The binder file manager. + //public BinderFileManager BinderFileManager + //{ + // get { return this.binderFileManager; } + //} + + /// + /// Gets the max threshold. + /// + /// The maximum threshold for a folder in a cabinet. + public int MaxThreshold + { + get { return this.maxThreshold; } + } + } +} diff --git a/src/WixToolset.Core/Bind/Databases/ConfigurationCallback.cs b/src/WixToolset.Core/Bind/Databases/ConfigurationCallback.cs new file mode 100644 index 00000000..7cb18e0f --- /dev/null +++ b/src/WixToolset.Core/Bind/Databases/ConfigurationCallback.cs @@ -0,0 +1,91 @@ +// 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. + +namespace WixToolset.Bind.Databases +{ + using System; + using System.Collections; + using System.Globalization; + using WixToolset.MergeMod; + + /// + /// Callback object for configurable merge modules. + /// + internal sealed class ConfigurationCallback : IMsmConfigureModule + { + private const int SOk = 0x0; + private const int SFalse = 0x1; + private Hashtable configurationData; + + /// + /// Creates a ConfigurationCallback object. + /// + /// String to break up into name/value pairs. + public ConfigurationCallback(string configData) + { + if (String.IsNullOrEmpty(configData)) + { + throw new ArgumentNullException("configData"); + } + + string[] pairs = configData.Split(','); + this.configurationData = new Hashtable(pairs.Length); + for (int i = 0; i < pairs.Length; ++i) + { + string[] nameVal = pairs[i].Split('='); + string name = nameVal[0]; + string value = nameVal[1]; + + name = name.Replace("%2C", ","); + name = name.Replace("%3D", "="); + name = name.Replace("%25", "%"); + + value = value.Replace("%2C", ","); + value = value.Replace("%3D", "="); + value = value.Replace("%25", "%"); + + this.configurationData[name] = value; + } + } + + /// + /// Returns text data based on name. + /// + /// Name of value to return. + /// Out param to put configuration data into. + /// S_OK if value provided, S_FALSE if not. + public int ProvideTextData(string name, out string configData) + { + if (this.configurationData.Contains(name)) + { + configData = (string)this.configurationData[name]; + return SOk; + } + else + { + configData = null; + return SFalse; + } + } + + /// + /// Returns integer data based on name. + /// + /// Name of value to return. + /// Out param to put configuration data into. + /// S_OK if value provided, S_FALSE if not. + public int ProvideIntegerData(string name, out int configData) + { + if (this.configurationData.Contains(name)) + { + string val = (string)this.configurationData[name]; + configData = Convert.ToInt32(val, CultureInfo.InvariantCulture); + return SOk; + } + else + { + configData = 0; + return SFalse; + } + } + } +} diff --git a/src/WixToolset.Core/Bind/Databases/CopyTransformDataCommand.cs b/src/WixToolset.Core/Bind/Databases/CopyTransformDataCommand.cs new file mode 100644 index 00000000..af1ab3b0 --- /dev/null +++ b/src/WixToolset.Core/Bind/Databases/CopyTransformDataCommand.cs @@ -0,0 +1,606 @@ +// 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. + +namespace WixToolset.Bind.Databases +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using WixToolset.Data; + using WixToolset.Data.Rows; + using WixToolset.Extensibility; + using WixToolset.Core.Native; + + internal class CopyTransformDataCommand : ICommand + { + public bool CopyOutFileRows { private get; set; } + + public BinderFileManagerCore FileManagerCore { private get; set; } + + public IEnumerable FileManagers { private get; set; } + + public Output Output { private get; set; } + + public TableDefinitionCollection TableDefinitions { private get; set; } + + public IEnumerable FileFacades { get; private set; } + + public void Execute() + { + Debug.Assert(OutputType.Patch != this.Output.Type); + + List allFileRows = this.CopyOutFileRows ? new List() : null; + +#if false // TODO: Fix this patching related code to work correctly with FileFacades. + bool copyToPatch = (allFileRows != null); + bool copyFromPatch = !copyToPatch; + + RowDictionary patchMediaRows = new RowDictionary(); + + Dictionary> patchMediaFileRows = new Dictionary>(); + + Table patchActualFileTable = this.Output.EnsureTable(this.TableDefinitions["File"]); + Table patchFileTable = this.Output.EnsureTable(this.TableDefinitions["WixFile"]); + + if (copyFromPatch) + { + // index patch files by diskId+fileId + foreach (WixFileRow patchFileRow in patchFileTable.Rows) + { + int diskId = patchFileRow.DiskId; + RowDictionary mediaFileRows; + if (!patchMediaFileRows.TryGetValue(diskId, out mediaFileRows)) + { + mediaFileRows = new RowDictionary(); + patchMediaFileRows.Add(diskId, mediaFileRows); + } + + mediaFileRows.Add(patchFileRow); + } + + Table patchMediaTable = this.Output.EnsureTable(this.TableDefinitions["Media"]); + patchMediaRows = new RowDictionary(patchMediaTable); + } + + // index paired transforms + Dictionary pairedTransforms = new Dictionary(); + foreach (SubStorage substorage in this.Output.SubStorages) + { + if (substorage.Name.StartsWith("#")) + { + pairedTransforms.Add(substorage.Name.Substring(1), substorage.Data); + } + } + + try + { + // copy File bind data into substorages + foreach (SubStorage substorage in this.Output.SubStorages) + { + if (substorage.Name.StartsWith("#")) + { + // no changes necessary for paired transforms + continue; + } + + Output mainTransform = substorage.Data; + Table mainWixFileTable = mainTransform.Tables["WixFile"]; + Table mainMsiFileHashTable = mainTransform.Tables["MsiFileHash"]; + + this.FileManagerCore.ActiveSubStorage = substorage; + + RowDictionary mainWixFiles = new RowDictionary(mainWixFileTable); + RowDictionary mainMsiFileHashIndex = new RowDictionary(); + + Table mainFileTable = mainTransform.Tables["File"]; + Output pairedTransform = (Output)pairedTransforms[substorage.Name]; + + // copy Media.LastSequence and index the MsiFileHash table if it exists. + if (copyFromPatch) + { + Table pairedMediaTable = pairedTransform.Tables["Media"]; + foreach (MediaRow pairedMediaRow in pairedMediaTable.Rows) + { + MediaRow patchMediaRow = patchMediaRows.Get(pairedMediaRow.DiskId); + pairedMediaRow.Fields[1] = patchMediaRow.Fields[1]; + } + + if (null != mainMsiFileHashTable) + { + mainMsiFileHashIndex = new RowDictionary(mainMsiFileHashTable); + } + + // Validate file row changes for keypath-related issues + this.ValidateFileRowChanges(mainTransform); + } + + // Index File table of pairedTransform + Table pairedFileTable = pairedTransform.Tables["File"]; + RowDictionary pairedFileRows = new RowDictionary(pairedFileTable); + + if (null != mainFileTable) + { + if (copyFromPatch) + { + // Remove the MsiFileHash table because it will be updated later with the final file hash for each file + mainTransform.Tables.Remove("MsiFileHash"); + } + + foreach (FileRow mainFileRow in mainFileTable.Rows) + { + if (RowOperation.Delete == mainFileRow.Operation) + { + continue; + } + else if (RowOperation.None == mainFileRow.Operation && !copyToPatch) + { + continue; + } + + WixFileRow mainWixFileRow = mainWixFiles.Get(mainFileRow.File); + + if (copyToPatch) // when copying to the patch, we need compare the underlying files and include all file changes. + { + ObjectField objectField = (ObjectField)mainWixFileRow.Fields[6]; + FileRow pairedFileRow = pairedFileRows.Get(mainFileRow.File); + + // If the file is new, we always need to add it to the patch. + if (mainFileRow.Operation != RowOperation.Add) + { + // If PreviousData doesn't exist, target and upgrade layout point to the same location. No need to compare. + if (null == objectField.PreviousData) + { + if (mainFileRow.Operation == RowOperation.None) + { + continue; + } + } + else + { + // TODO: should this entire condition be placed in the binder file manager? + if ((0 == (PatchAttributeType.Ignore & mainWixFileRow.PatchAttributes)) && + !this.CompareFiles(objectField.PreviousData.ToString(), objectField.Data.ToString())) + { + // If the file is different, we need to mark the mainFileRow and pairedFileRow as modified. + mainFileRow.Operation = RowOperation.Modify; + if (null != pairedFileRow) + { + // Always patch-added, but never non-compressed. + pairedFileRow.Attributes |= MsiInterop.MsidbFileAttributesPatchAdded; + pairedFileRow.Attributes &= ~MsiInterop.MsidbFileAttributesNoncompressed; + pairedFileRow.Fields[6].Modified = true; + pairedFileRow.Operation = RowOperation.Modify; + } + } + else + { + // The File is same. We need mark all the attributes as unchanged. + mainFileRow.Operation = RowOperation.None; + foreach (Field field in mainFileRow.Fields) + { + field.Modified = false; + } + + if (null != pairedFileRow) + { + pairedFileRow.Attributes &= ~MsiInterop.MsidbFileAttributesPatchAdded; + pairedFileRow.Fields[6].Modified = false; + pairedFileRow.Operation = RowOperation.None; + } + continue; + } + } + } + else if (null != pairedFileRow) // RowOperation.Add + { + // Always patch-added, but never non-compressed. + pairedFileRow.Attributes |= MsiInterop.MsidbFileAttributesPatchAdded; + pairedFileRow.Attributes &= ~MsiInterop.MsidbFileAttributesNoncompressed; + pairedFileRow.Fields[6].Modified = true; + pairedFileRow.Operation = RowOperation.Add; + } + } + + // index patch files by diskId+fileId + int diskId = mainWixFileRow.DiskId; + + RowDictionary mediaFileRows; + if (!patchMediaFileRows.TryGetValue(diskId, out mediaFileRows)) + { + mediaFileRows = new RowDictionary(); + patchMediaFileRows.Add(diskId, mediaFileRows); + } + + string fileId = mainFileRow.File; + WixFileRow patchFileRow = mediaFileRows.Get(fileId); + if (copyToPatch) + { + if (null == patchFileRow) + { + FileRow patchActualFileRow = (FileRow)patchFileTable.CreateRow(mainFileRow.SourceLineNumbers); + patchActualFileRow.CopyFrom(mainFileRow); + + patchFileRow = (WixFileRow)patchFileTable.CreateRow(mainFileRow.SourceLineNumbers); + patchFileRow.CopyFrom(mainWixFileRow); + + mediaFileRows.Add(patchFileRow); + + allFileRows.Add(new FileFacade(patchActualFileRow, patchFileRow, null)); // TODO: should we be passing along delta information? Probably, right? + } + else + { + // TODO: confirm the rest of data is identical? + + // make sure Source is same. Otherwise we are silently ignoring a file. + if (0 != String.Compare(patchFileRow.Source, mainWixFileRow.Source, StringComparison.OrdinalIgnoreCase)) + { + Messaging.Instance.OnMessage(WixErrors.SameFileIdDifferentSource(mainFileRow.SourceLineNumbers, fileId, patchFileRow.Source, mainWixFileRow.Source)); + } + + // capture the previous file versions (and associated data) from this targeted instance of the baseline into the current filerow. + patchFileRow.AppendPreviousDataFrom(mainWixFileRow); + } + } + else + { + // copy data from the patch back to the transform + if (null != patchFileRow) + { + FileRow pairedFileRow = (FileRow)pairedFileRows.Get(fileId); + for (int i = 0; i < patchFileRow.Fields.Length; i++) + { + string patchValue = patchFileRow[i] == null ? "" : patchFileRow[i].ToString(); + string mainValue = mainFileRow[i] == null ? "" : mainFileRow[i].ToString(); + + if (1 == i) + { + // File.Component_ changes should not come from the shared file rows + // that contain the file information as each individual transform might + // have different changes (or no changes at all). + } + // File.Attributes should not changed for binary deltas + else if (6 == i) + { + if (null != patchFileRow.Patch) + { + // File.Attribute should not change for binary deltas + pairedFileRow.Attributes = mainFileRow.Attributes; + mainFileRow.Fields[i].Modified = false; + } + } + // File.Sequence is updated in pairedTransform, not mainTransform + else if (7 == i) + { + // file sequence is updated in Patch table instead of File table for delta patches + if (null != patchFileRow.Patch) + { + pairedFileRow.Fields[i].Modified = false; + } + else + { + pairedFileRow[i] = patchFileRow[i]; + pairedFileRow.Fields[i].Modified = true; + } + mainFileRow.Fields[i].Modified = false; + } + else if (patchValue != mainValue) + { + mainFileRow[i] = patchFileRow[i]; + mainFileRow.Fields[i].Modified = true; + if (mainFileRow.Operation == RowOperation.None) + { + mainFileRow.Operation = RowOperation.Modify; + } + } + } + + // copy MsiFileHash row for this File + Row patchHashRow; + if (!mainMsiFileHashIndex.TryGetValue(patchFileRow.File, out patchHashRow)) + { + patchHashRow = patchFileRow.Hash; + } + + if (null != patchHashRow) + { + Table mainHashTable = mainTransform.EnsureTable(this.TableDefinitions["MsiFileHash"]); + Row mainHashRow = mainHashTable.CreateRow(mainFileRow.SourceLineNumbers); + for (int i = 0; i < patchHashRow.Fields.Length; i++) + { + mainHashRow[i] = patchHashRow[i]; + if (i > 1) + { + // assume all hash fields have been modified + mainHashRow.Fields[i].Modified = true; + } + } + + // assume the MsiFileHash operation follows the File one + mainHashRow.Operation = mainFileRow.Operation; + } + + // copy MsiAssemblyName rows for this File + List patchAssemblyNameRows = patchFileRow.AssemblyNames; + if (null != patchAssemblyNameRows) + { + Table mainAssemblyNameTable = mainTransform.EnsureTable(this.TableDefinitions["MsiAssemblyName"]); + foreach (Row patchAssemblyNameRow in patchAssemblyNameRows) + { + // Copy if there isn't an identical modified/added row already in the transform. + bool foundMatchingModifiedRow = false; + foreach (Row mainAssemblyNameRow in mainAssemblyNameTable.Rows) + { + if (RowOperation.None != mainAssemblyNameRow.Operation && mainAssemblyNameRow.GetPrimaryKey('/').Equals(patchAssemblyNameRow.GetPrimaryKey('/'))) + { + foundMatchingModifiedRow = true; + break; + } + } + + if (!foundMatchingModifiedRow) + { + Row mainAssemblyNameRow = mainAssemblyNameTable.CreateRow(mainFileRow.SourceLineNumbers); + for (int i = 0; i < patchAssemblyNameRow.Fields.Length; i++) + { + mainAssemblyNameRow[i] = patchAssemblyNameRow[i]; + } + + // assume value field has been modified + mainAssemblyNameRow.Fields[2].Modified = true; + mainAssemblyNameRow.Operation = mainFileRow.Operation; + } + } + } + + // Add patch header for this file + if (null != patchFileRow.Patch) + { + // Add the PatchFiles action automatically to the AdminExecuteSequence and InstallExecuteSequence tables. + AddPatchFilesActionToSequenceTable(SequenceTable.AdminExecuteSequence, mainTransform, pairedTransform, mainFileRow); + AddPatchFilesActionToSequenceTable(SequenceTable.InstallExecuteSequence, mainTransform, pairedTransform, mainFileRow); + + // Add to Patch table + Table patchTable = pairedTransform.EnsureTable(this.TableDefinitions["Patch"]); + if (0 == patchTable.Rows.Count) + { + patchTable.Operation = TableOperation.Add; + } + + Row patchRow = patchTable.CreateRow(mainFileRow.SourceLineNumbers); + patchRow[0] = patchFileRow.File; + patchRow[1] = patchFileRow.Sequence; + + FileInfo patchFile = new FileInfo(patchFileRow.Source); + patchRow[2] = (int)patchFile.Length; + patchRow[3] = 0 == (PatchAttributeType.AllowIgnoreOnError & patchFileRow.PatchAttributes) ? 0 : 1; + + string streamName = patchTable.Name + "." + patchRow[0] + "." + patchRow[1]; + if (MsiInterop.MsiMaxStreamNameLength < streamName.Length) + { + streamName = "_" + Guid.NewGuid().ToString("D").ToUpperInvariant().Replace('-', '_'); + Table patchHeadersTable = pairedTransform.EnsureTable(this.TableDefinitions["MsiPatchHeaders"]); + if (0 == patchHeadersTable.Rows.Count) + { + patchHeadersTable.Operation = TableOperation.Add; + } + Row patchHeadersRow = patchHeadersTable.CreateRow(mainFileRow.SourceLineNumbers); + patchHeadersRow[0] = streamName; + patchHeadersRow[1] = patchFileRow.Patch; + patchRow[5] = streamName; + patchHeadersRow.Operation = RowOperation.Add; + } + else + { + patchRow[4] = patchFileRow.Patch; + } + patchRow.Operation = RowOperation.Add; + } + } + else + { + // TODO: throw because all transform rows should have made it into the patch + } + } + } + } + + if (copyFromPatch) + { + this.Output.Tables.Remove("Media"); + this.Output.Tables.Remove("File"); + this.Output.Tables.Remove("MsiFileHash"); + this.Output.Tables.Remove("MsiAssemblyName"); + } + } + } + finally + { + this.FileManagerCore.ActiveSubStorage = null; + } +#endif + this.FileFacades = allFileRows; + } + + /// + /// Adds the PatchFiles action to the sequence table if it does not already exist. + /// + /// The sequence table to check or modify. + /// The primary authoring transform. + /// The secondary patch transform. + /// The file row that contains information about the patched file. + private void AddPatchFilesActionToSequenceTable(SequenceTable table, Output mainTransform, Output pairedTransform, Row mainFileRow) + { + // Find/add PatchFiles action (also determine sequence for it). + // Search mainTransform first, then pairedTransform (pairedTransform overrides). + bool hasPatchFilesAction = false; + int seqInstallFiles = 0; + int seqDuplicateFiles = 0; + string tableName = table.ToString(); + + TestSequenceTableForPatchFilesAction( + mainTransform.Tables[tableName], + ref hasPatchFilesAction, + ref seqInstallFiles, + ref seqDuplicateFiles); + TestSequenceTableForPatchFilesAction( + pairedTransform.Tables[tableName], + ref hasPatchFilesAction, + ref seqInstallFiles, + ref seqDuplicateFiles); + if (!hasPatchFilesAction) + { + Table iesTable = pairedTransform.EnsureTable(this.TableDefinitions[tableName]); + if (0 == iesTable.Rows.Count) + { + iesTable.Operation = TableOperation.Add; + } + + Row patchAction = iesTable.CreateRow(null); + WixActionRow wixPatchAction = WindowsInstallerStandard.GetStandardActions()[table, "PatchFiles"]; + int sequence = wixPatchAction.Sequence; + // Test for default sequence value's appropriateness + if (seqInstallFiles >= sequence || (0 != seqDuplicateFiles && seqDuplicateFiles <= sequence)) + { + if (0 != seqDuplicateFiles) + { + if (seqDuplicateFiles < seqInstallFiles) + { + throw new WixException(WixErrors.InsertInvalidSequenceActionOrder(mainFileRow.SourceLineNumbers, iesTable.Name, "InstallFiles", "DuplicateFiles", wixPatchAction.Action)); + } + else + { + sequence = (seqDuplicateFiles + seqInstallFiles) / 2; + if (seqInstallFiles == sequence || seqDuplicateFiles == sequence) + { + throw new WixException(WixErrors.InsertSequenceNoSpace(mainFileRow.SourceLineNumbers, iesTable.Name, "InstallFiles", "DuplicateFiles", wixPatchAction.Action)); + } + } + } + else + { + sequence = seqInstallFiles + 1; + } + } + patchAction[0] = wixPatchAction.Action; + patchAction[1] = wixPatchAction.Condition; + patchAction[2] = sequence; + patchAction.Operation = RowOperation.Add; + } + } + + /// + /// Tests sequence table for PatchFiles and associated actions + /// + /// The table to test. + /// Set to true if PatchFiles action is found. Left unchanged otherwise. + /// Set to sequence value of InstallFiles action if found. Left unchanged otherwise. + /// Set to sequence value of DuplicateFiles action if found. Left unchanged otherwise. + private static void TestSequenceTableForPatchFilesAction(Table iesTable, ref bool hasPatchFilesAction, ref int seqInstallFiles, ref int seqDuplicateFiles) + { + if (null != iesTable) + { + foreach (Row iesRow in iesTable.Rows) + { + if (String.Equals("PatchFiles", (string)iesRow[0], StringComparison.Ordinal)) + { + hasPatchFilesAction = true; + } + if (String.Equals("InstallFiles", (string)iesRow[0], StringComparison.Ordinal)) + { + seqInstallFiles = (int)iesRow.Fields[2].Data; + } + if (String.Equals("DuplicateFiles", (string)iesRow[0], StringComparison.Ordinal)) + { + seqDuplicateFiles = (int)iesRow.Fields[2].Data; + } + } + } + } + + /// + /// Signal a warning if a non-keypath file was changed in a patch without also changing the keypath file of the component. + /// + /// The output to validate. + private void ValidateFileRowChanges(Output transform) + { + Table componentTable = transform.Tables["Component"]; + Table fileTable = transform.Tables["File"]; + + // There's no sense validating keypaths if the transform has no component or file table + if (componentTable == null || fileTable == null) + { + return; + } + + Dictionary componentKeyPath = new Dictionary(componentTable.Rows.Count); + + // Index the Component table for non-directory & non-registry key paths. + foreach (Row row in componentTable.Rows) + { + if (null != row.Fields[5].Data && + 0 != ((int)row.Fields[3].Data & MsiInterop.MsidbComponentAttributesRegistryKeyPath)) + { + componentKeyPath.Add(row.Fields[0].Data.ToString(), row.Fields[5].Data.ToString()); + } + } + + Dictionary componentWithChangedKeyPath = new Dictionary(); + Dictionary componentWithNonKeyPathChanged = new Dictionary(); + // Verify changes in the file table, now that file diffing has occurred + foreach (FileRow row in fileTable.Rows) + { + string fileId = row.Fields[0].Data.ToString(); + string componentId = row.Fields[1].Data.ToString(); + + if (RowOperation.Modify != row.Operation) + { + continue; + } + + // If this file is the keypath of a component + if (componentKeyPath.ContainsValue(fileId)) + { + if (!componentWithChangedKeyPath.ContainsKey(componentId)) + { + componentWithChangedKeyPath.Add(componentId, fileId); + } + } + else + { + if (!componentWithNonKeyPathChanged.ContainsKey(componentId)) + { + componentWithNonKeyPathChanged.Add(componentId, fileId); + } + } + } + + foreach (KeyValuePair componentFile in componentWithNonKeyPathChanged) + { + // Make sure all changes to non keypath files also had a change in the keypath. + if (!componentWithChangedKeyPath.ContainsKey(componentFile.Key) && componentKeyPath.ContainsKey(componentFile.Key)) + { + Messaging.Instance.OnMessage(WixWarnings.UpdateOfNonKeyPathFile((string)componentFile.Value, (string)componentFile.Key, (string)componentKeyPath[componentFile.Key])); + } + } + } + + private bool CompareFiles(string targetFile, string updatedFile) + { + bool? compared = null; + foreach (IBinderFileManager fileManager in this.FileManagers) + { + compared = fileManager.CompareFiles(targetFile, updatedFile); + if (compared.HasValue) + { + break; + } + } + + if (!compared.HasValue) + { + throw new InvalidOperationException(); // TODO: something needs to be said here that none of the binder file managers returned a result. + } + + return compared.Value; + } + } +} diff --git a/src/WixToolset.Core/Bind/Databases/CreateCabinetsCommand.cs b/src/WixToolset.Core/Bind/Databases/CreateCabinetsCommand.cs new file mode 100644 index 00000000..35c8abb4 --- /dev/null +++ b/src/WixToolset.Core/Bind/Databases/CreateCabinetsCommand.cs @@ -0,0 +1,489 @@ +// 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. + +namespace WixToolset.Bind.Databases +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Runtime.InteropServices; + using System.Threading; + using WixToolset.Data; + using WixToolset.Data.Rows; + using WixToolset.Extensibility; + + /// + /// Creates cabinet files. + /// + internal class CreateCabinetsCommand : ICommand + { + private List fileTransfers; + + private FileSplitCabNamesCallback newCabNamesCallBack; + + private Dictionary lastCabinetAddedToMediaTable; // Key is First Cabinet Name, Value is Last Cabinet Added in the Split Sequence + + public CreateCabinetsCommand() + { + this.fileTransfers = new List(); + + this.newCabNamesCallBack = NewCabNamesCallBack; + } + + /// + /// Sets the number of threads to use for cabinet creation. + /// + public int CabbingThreadCount { private get; set; } + + public string TempFilesLocation { private get; set; } + + /// + /// Sets the default compression level to use for cabinets + /// that don't have their compression level explicitly set. + /// + public CompressionLevel DefaultCompressionLevel { private get; set; } + + public Output Output { private get; set; } + + public IEnumerable FileManagers { private get; set; } + + public string LayoutDirectory { private get; set; } + + public bool Compressed { private get; set; } + + public Dictionary> FileRowsByCabinet { private get; set; } + + public Func ResolveMedia { private get; set; } + + public TableDefinitionCollection TableDefinitions { private get; set; } + + public Table WixMediaTable { private get; set; } + + public IEnumerable FileTransfers { get { return this.fileTransfers; } } + + /// Output to generate image for. + /// Array of files to be transfered. + /// The directory in which the image should be layed out. + /// Flag if source image should be compressed. + /// The uncompressed file rows. + public void Execute() + { + RowDictionary wixMediaRows = new RowDictionary(this.WixMediaTable); + + this.lastCabinetAddedToMediaTable = new Dictionary(); + + this.SetCabbingThreadCount(); + + // Send Binder object to Facilitate NewCabNamesCallBack Callback + CabinetBuilder cabinetBuilder = new CabinetBuilder(this.CabbingThreadCount, Marshal.GetFunctionPointerForDelegate(this.newCabNamesCallBack)); + + // Supply Compile MediaTemplate Attributes to Cabinet Builder + int MaximumCabinetSizeForLargeFileSplitting; + int MaximumUncompressedMediaSize; + this.GetMediaTemplateAttributes(out MaximumCabinetSizeForLargeFileSplitting, out MaximumUncompressedMediaSize); + cabinetBuilder.MaximumCabinetSizeForLargeFileSplitting = MaximumCabinetSizeForLargeFileSplitting; + cabinetBuilder.MaximumUncompressedMediaSize = MaximumUncompressedMediaSize; + + foreach (var entry in this.FileRowsByCabinet) + { + MediaRow mediaRow = entry.Key; + IEnumerable files = entry.Value; + CompressionLevel compressionLevel = this.DefaultCompressionLevel; + + WixMediaRow wixMediaRow = null; + string mediaLayoutFolder = null; + + if (wixMediaRows.TryGetValue(mediaRow.GetKey(), out wixMediaRow)) + { + mediaLayoutFolder = wixMediaRow.Layout; + + if (wixMediaRow.CompressionLevel.HasValue) + { + compressionLevel = wixMediaRow.CompressionLevel.Value; + } + } + + string cabinetDir = this.ResolveMedia(mediaRow, mediaLayoutFolder, this.LayoutDirectory); + + CabinetWorkItem cabinetWorkItem = this.CreateCabinetWorkItem(this.Output, cabinetDir, mediaRow, compressionLevel, files, this.fileTransfers); + if (null != cabinetWorkItem) + { + cabinetBuilder.Enqueue(cabinetWorkItem); + } + } + + // stop processing if an error previously occurred + if (Messaging.Instance.EncounteredError) + { + return; + } + + // create queued cabinets with multiple threads + cabinetBuilder.CreateQueuedCabinets(); + if (Messaging.Instance.EncounteredError) + { + return; + } + } + + /// + /// Sets the thead count to the number of processors if the current thread count is set to 0. + /// + /// The thread count value must be greater than 0 otherwise and exception will be thrown. + private void SetCabbingThreadCount() + { + // default the number of cabbing threads to the number of processors if it wasn't specified + if (0 == this.CabbingThreadCount) + { + string numberOfProcessors = System.Environment.GetEnvironmentVariable("NUMBER_OF_PROCESSORS"); + + try + { + if (null != numberOfProcessors) + { + this.CabbingThreadCount = Convert.ToInt32(numberOfProcessors, CultureInfo.InvariantCulture.NumberFormat); + + if (0 >= this.CabbingThreadCount) + { + throw new WixException(WixErrors.IllegalEnvironmentVariable("NUMBER_OF_PROCESSORS", numberOfProcessors)); + } + } + else // default to 1 if the environment variable is not set + { + this.CabbingThreadCount = 1; + } + + Messaging.Instance.OnMessage(WixVerboses.SetCabbingThreadCount(this.CabbingThreadCount.ToString())); + } + catch (ArgumentException) + { + throw new WixException(WixErrors.IllegalEnvironmentVariable("NUMBER_OF_PROCESSORS", numberOfProcessors)); + } + catch (FormatException) + { + throw new WixException(WixErrors.IllegalEnvironmentVariable("NUMBER_OF_PROCESSORS", numberOfProcessors)); + } + } + } + + + /// + /// Creates a work item to create a cabinet. + /// + /// Output for the current database. + /// Directory to create cabinet in. + /// MediaRow containing information about the cabinet. + /// Collection of files in this cabinet. + /// Array of files to be transfered. + /// created CabinetWorkItem object + private CabinetWorkItem CreateCabinetWorkItem(Output output, string cabinetDir, MediaRow mediaRow, CompressionLevel compressionLevel, IEnumerable fileFacades, List fileTransfers) + { + CabinetWorkItem cabinetWorkItem = null; + string tempCabinetFileX = Path.Combine(this.TempFilesLocation, mediaRow.Cabinet); + + // check for an empty cabinet + if (!fileFacades.Any()) + { + string cabinetName = mediaRow.Cabinet; + + // remove the leading '#' from the embedded cabinet name to make the warning easier to understand + if (cabinetName.StartsWith("#", StringComparison.Ordinal)) + { + cabinetName = cabinetName.Substring(1); + } + + // If building a patch, remind them to run -p for torch. + if (OutputType.Patch == output.Type) + { + Messaging.Instance.OnMessage(WixWarnings.EmptyCabinet(mediaRow.SourceLineNumbers, cabinetName, true)); + } + else + { + Messaging.Instance.OnMessage(WixWarnings.EmptyCabinet(mediaRow.SourceLineNumbers, cabinetName)); + } + } + + ResolvedCabinet resolvedCabinet = this.ResolveCabinet(tempCabinetFileX, fileFacades); + + // create a cabinet work item if it's not being skipped + if (CabinetBuildOption.BuildAndCopy == resolvedCabinet.BuildOption || CabinetBuildOption.BuildAndMove == resolvedCabinet.BuildOption) + { + int maxThreshold = 0; // default to the threshold for best smartcabbing (makes smallest cabinet). + + cabinetWorkItem = new CabinetWorkItem(fileFacades, resolvedCabinet.Path, maxThreshold, compressionLevel/*, this.FileManager*/); + } + else // reuse the cabinet from the cabinet cache. + { + Messaging.Instance.OnMessage(WixVerboses.ReusingCabCache(mediaRow.SourceLineNumbers, mediaRow.Cabinet, resolvedCabinet.Path)); + + try + { + // Ensure the cached cabinet timestamp is current to prevent perpetual incremental builds. The + // problematic scenario goes like this. Imagine two cabinets in the cache. Update a file that + // goes into one of the cabinets. One cabinet will get rebuilt, the other will be copied from + // the cache. Now the file (an input) has a newer timestamp than the reused cabient (an output) + // causing the project to look like it perpetually needs a rebuild until all of the reused + // cabinets get newer timestamps. + File.SetLastWriteTime(resolvedCabinet.Path, DateTime.Now); + } + catch (Exception e) + { + Messaging.Instance.OnMessage(WixWarnings.CannotUpdateCabCache(mediaRow.SourceLineNumbers, resolvedCabinet.Path, e.Message)); + } + } + + if (mediaRow.Cabinet.StartsWith("#", StringComparison.Ordinal)) + { + Table streamsTable = output.EnsureTable(this.TableDefinitions["_Streams"]); + + Row streamRow = streamsTable.CreateRow(mediaRow.SourceLineNumbers); + streamRow[0] = mediaRow.Cabinet.Substring(1); + streamRow[1] = resolvedCabinet.Path; + } + else + { + string destinationPath = Path.Combine(cabinetDir, mediaRow.Cabinet); + FileTransfer transfer; + if (FileTransfer.TryCreate(resolvedCabinet.Path, destinationPath, CabinetBuildOption.BuildAndMove == resolvedCabinet.BuildOption, "Cabinet", mediaRow.SourceLineNumbers, out transfer)) + { + transfer.Built = true; + fileTransfers.Add(transfer); + } + } + + return cabinetWorkItem; + } + + private ResolvedCabinet ResolveCabinet(string cabinetPath, IEnumerable fileFacades) + { + ResolvedCabinet resolved = null; + + List filesWithPath = fileFacades.Select(f => new BindFileWithPath() { Id = f.File.File, Path = f.WixFile.Source }).ToList(); + + foreach (IBinderFileManager fileManager in this.FileManagers) + { + resolved = fileManager.ResolveCabinet(cabinetPath, filesWithPath); + if (null != resolved) + { + break; + } + } + + return resolved; + } + + /// + /// Delegate for Cabinet Split Callback + /// + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + internal delegate void FileSplitCabNamesCallback([MarshalAs(UnmanagedType.LPWStr)]string firstCabName, [MarshalAs(UnmanagedType.LPWStr)]string newCabName, [MarshalAs(UnmanagedType.LPWStr)]string fileToken); + + /// + /// Call back to Add File Transfer for new Cab and add new Cab to Media table + /// This callback can come from Multiple Cabinet Builder Threads and so should be thread safe + /// This callback will not be called in case there is no File splitting. i.e. MaximumCabinetSizeForLargeFileSplitting was not authored + /// + /// The name of splitting cabinet without extention e.g. "cab1". + /// The name of the new cabinet that would be formed by splitting e.g. "cab1b.cab" + /// The file token of the first file present in the splitting cabinet + internal void NewCabNamesCallBack([MarshalAs(UnmanagedType.LPWStr)]string firstCabName, [MarshalAs(UnmanagedType.LPWStr)]string newCabName, [MarshalAs(UnmanagedType.LPWStr)]string fileToken) + { + // Locking Mutex here as this callback can come from Multiple Cabinet Builder Threads + Mutex mutex = new Mutex(false, "WixCabinetSplitBinderCallback"); + try + { + if (!mutex.WaitOne(0, false)) // Check if you can get the lock + { + // Cound not get the Lock + Messaging.Instance.OnMessage(WixVerboses.CabinetsSplitInParallel()); + mutex.WaitOne(); // Wait on other thread + } + + string firstCabinetName = firstCabName + ".cab"; + string newCabinetName = newCabName; + bool transferAdded = false; // Used for Error Handling + + // Create File Transfer for new Cabinet using transfer of Base Cabinet + foreach (FileTransfer transfer in this.FileTransfers) + { + if (firstCabinetName.Equals(Path.GetFileName(transfer.Source), StringComparison.InvariantCultureIgnoreCase)) + { + string newCabSourcePath = Path.Combine(Path.GetDirectoryName(transfer.Source), newCabinetName); + string newCabTargetPath = Path.Combine(Path.GetDirectoryName(transfer.Destination), newCabinetName); + + FileTransfer newTransfer; + if (FileTransfer.TryCreate(newCabSourcePath, newCabTargetPath, transfer.Move, "Cabinet", transfer.SourceLineNumbers, out newTransfer)) + { + newTransfer.Built = true; + this.fileTransfers.Add(newTransfer); + transferAdded = true; + break; + } + } + } + + // Check if File Transfer was added + if (!transferAdded) + { + throw new WixException(WixErrors.SplitCabinetCopyRegistrationFailed(newCabinetName, firstCabinetName)); + } + + // Add the new Cabinets to media table using LastSequence of Base Cabinet + Table mediaTable = this.Output.Tables["Media"]; + Table wixFileTable = this.Output.Tables["WixFile"]; + int diskIDForLastSplitCabAdded = 0; // The DiskID value for the first cab in this cabinet split chain + int lastSequenceForLastSplitCabAdded = 0; // The LastSequence value for the first cab in this cabinet split chain + bool lastSplitCabinetFound = false; // Used for Error Handling + + string lastCabinetOfThisSequence = String.Empty; + // Get the Value of Last Cabinet Added in this split Sequence from Dictionary + if (!this.lastCabinetAddedToMediaTable.TryGetValue(firstCabinetName, out lastCabinetOfThisSequence)) + { + // If there is no value for this sequence, then use first Cabinet is the last one of this split sequence + lastCabinetOfThisSequence = firstCabinetName; + } + + foreach (MediaRow mediaRow in mediaTable.Rows) + { + // Get details for the Last Cabinet Added in this Split Sequence + if ((lastSequenceForLastSplitCabAdded == 0) && lastCabinetOfThisSequence.Equals(mediaRow.Cabinet, StringComparison.InvariantCultureIgnoreCase)) + { + lastSequenceForLastSplitCabAdded = mediaRow.LastSequence; + diskIDForLastSplitCabAdded = mediaRow.DiskId; + lastSplitCabinetFound = true; + } + + // Check for Name Collision for the new Cabinet added + if (newCabinetName.Equals(mediaRow.Cabinet, StringComparison.InvariantCultureIgnoreCase)) + { + // Name Collision of generated Split Cabinet Name and user Specified Cab name for current row + throw new WixException(WixErrors.SplitCabinetNameCollision(newCabinetName, firstCabinetName)); + } + } + + // Check if the last Split Cabinet was found in the Media Table + if (!lastSplitCabinetFound) + { + throw new WixException(WixErrors.SplitCabinetInsertionFailed(newCabinetName, firstCabinetName, lastCabinetOfThisSequence)); + } + + // The new Row has to be inserted just after the last cab in this cabinet split chain according to DiskID Sort + // This is because the FDI Extract requires DiskID of Split Cabinets to be continuous. It Fails otherwise with + // Error 2350 (FDI Server Error) as next DiskID did not have the right split cabinet during extraction + MediaRow newMediaRow = (MediaRow)mediaTable.CreateRow(null); + newMediaRow.Cabinet = newCabinetName; + newMediaRow.DiskId = diskIDForLastSplitCabAdded + 1; // When Sorted with DiskID, this new Cabinet Row is an Insertion + newMediaRow.LastSequence = lastSequenceForLastSplitCabAdded; + + // Now increment the DiskID for all rows that come after the newly inserted row to Ensure that DiskId is unique + foreach (MediaRow mediaRow in mediaTable.Rows) + { + // Check if this row comes after inserted row and it is not the new cabinet inserted row + if (mediaRow.DiskId >= newMediaRow.DiskId && !newCabinetName.Equals(mediaRow.Cabinet, StringComparison.InvariantCultureIgnoreCase)) + { + mediaRow.DiskId++; // Increment DiskID + } + } + + // Now Increment DiskID for All files Rows so that they refer to the right Media Row + foreach (WixFileRow wixFileRow in wixFileTable.Rows) + { + // Check if this row comes after inserted row and if this row is not the file that has to go into the current cabinet + // This check will work as we have only one large file in every splitting cabinet + // If we want to support splitting cabinet with more large files we need to update this code + if (wixFileRow.DiskId >= newMediaRow.DiskId && !wixFileRow.File.Equals(fileToken, StringComparison.InvariantCultureIgnoreCase)) + { + wixFileRow.DiskId++; // Increment DiskID + } + } + + // Update the Last Cabinet Added in the Split Sequence in Dictionary for future callback + this.lastCabinetAddedToMediaTable[firstCabinetName] = newCabinetName; + + mediaTable.ValidateRows(); // Valdiates DiskDIs, throws Exception as Wix Error if validation fails + } + finally + { + // Releasing the Mutex here + mutex.ReleaseMutex(); + } + } + + + /// + /// Gets Compiler Values of MediaTemplate Attributes governing Maximum Cabinet Size after applying Environment Variable Overrides + /// + /// Output to generate image for. + /// The indexed file rows. + private void GetMediaTemplateAttributes(out int maxCabSizeForLargeFileSplitting, out int maxUncompressedMediaSize) + { + // Get Environment Variable Overrides for MediaTemplate Attributes governing Maximum Cabinet Size + string mcslfsString = Environment.GetEnvironmentVariable("WIX_MCSLFS"); + string mumsString = Environment.GetEnvironmentVariable("WIX_MUMS"); + int maxCabSizeForLargeFileInMB = 0; + int maxPreCompressedSizeInMB = 0; + ulong testOverFlow = 0; + + // Supply Compile MediaTemplate Attributes to Cabinet Builder + Table mediaTemplateTable = this.Output.Tables["WixMediaTemplate"]; + if (mediaTemplateTable != null) + { + WixMediaTemplateRow mediaTemplateRow = (WixMediaTemplateRow)mediaTemplateTable.Rows[0]; + + // Get the Value for Max Cab Size for File Splitting + try + { + // Override authored mcslfs value if environment variable is authored. + if (!String.IsNullOrEmpty(mcslfsString)) + { + maxCabSizeForLargeFileInMB = Int32.Parse(mcslfsString); + } + else + { + maxCabSizeForLargeFileInMB = mediaTemplateRow.MaximumCabinetSizeForLargeFileSplitting; + } + testOverFlow = (ulong)maxCabSizeForLargeFileInMB * 1024 * 1024; + } + catch (FormatException) + { + throw new WixException(WixErrors.IllegalEnvironmentVariable("WIX_MCSLFS", mcslfsString)); + } + catch (OverflowException) + { + throw new WixException(WixErrors.MaximumCabinetSizeForLargeFileSplittingTooLarge(null, maxCabSizeForLargeFileInMB, CompilerCore.MaxValueOfMaxCabSizeForLargeFileSplitting)); + } + + try + { + // Override authored mums value if environment variable is authored. + if (!String.IsNullOrEmpty(mumsString)) + { + maxPreCompressedSizeInMB = Int32.Parse(mumsString); + } + else + { + maxPreCompressedSizeInMB = mediaTemplateRow.MaximumUncompressedMediaSize; + } + testOverFlow = (ulong)maxPreCompressedSizeInMB * 1024 * 1024; + } + catch (FormatException) + { + throw new WixException(WixErrors.IllegalEnvironmentVariable("WIX_MUMS", mumsString)); + } + catch (OverflowException) + { + throw new WixException(WixErrors.MaximumUncompressedMediaSizeTooLarge(null, maxPreCompressedSizeInMB)); + } + + maxCabSizeForLargeFileSplitting = maxCabSizeForLargeFileInMB; + maxUncompressedMediaSize = maxPreCompressedSizeInMB; + } + else + { + maxCabSizeForLargeFileSplitting = 0; + maxUncompressedMediaSize = CompilerCore.DefaultMaximumUncompressedMediaSize; + } + } + } +} diff --git a/src/WixToolset.Core/Bind/Databases/CreateDeltaPatchesCommand.cs b/src/WixToolset.Core/Bind/Databases/CreateDeltaPatchesCommand.cs new file mode 100644 index 00000000..933a1ea8 --- /dev/null +++ b/src/WixToolset.Core/Bind/Databases/CreateDeltaPatchesCommand.cs @@ -0,0 +1,86 @@ +// 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. + +namespace WixToolset.Bind.Databases +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using WixToolset.Data; + using WixToolset.Data.Rows; + + /// + /// Creates delta patches and updates the appropriate rows to point to the newly generated patches. + /// + internal class CreateDeltaPatchesCommand : ICommand + { + public IEnumerable FileFacades { private get; set; } + + public Table WixPatchIdTable { private get; set; } + + public string TempFilesLocation { private get; set; } + + public void Execute() + { + bool optimizePatchSizeForLargeFiles = false; + PatchAPI.PatchInterop.PatchSymbolFlagsType apiPatchingSymbolFlags = 0; + + if (null != this.WixPatchIdTable) + { + Row row = this.WixPatchIdTable.Rows[0]; + if (null != row) + { + if (null != row[2]) + { + optimizePatchSizeForLargeFiles = (1 == Convert.ToUInt32(row[2], CultureInfo.InvariantCulture)); + } + + if (null != row[3]) + { + apiPatchingSymbolFlags = (PatchAPI.PatchInterop.PatchSymbolFlagsType)Convert.ToUInt32(row[3], CultureInfo.InvariantCulture); + } + } + } + + foreach (FileFacade facade in this.FileFacades) + { + if (RowOperation.Modify == facade.File.Operation && + 0 != (facade.WixFile.PatchAttributes & PatchAttributeType.IncludeWholeFile)) + { + string deltaBase = String.Concat("delta_", facade.File.File); + string deltaFile = Path.Combine(this.TempFilesLocation, String.Concat(deltaBase, ".dpf")); + string headerFile = Path.Combine(this.TempFilesLocation, String.Concat(deltaBase, ".phd")); + + bool retainRangeWarning = false; + + if (PatchAPI.PatchInterop.CreateDelta( + deltaFile, + facade.WixFile.Source, + facade.DeltaPatchFile.Symbols, + facade.DeltaPatchFile.RetainOffsets, + new[] { facade.WixFile.PreviousSource }, + facade.DeltaPatchFile.PreviousSymbols.Split(new[] { ';' }), + facade.DeltaPatchFile.PreviousIgnoreLengths.Split(new[] { ';' }), + facade.DeltaPatchFile.PreviousIgnoreOffsets.Split(new[] { ';' }), + facade.DeltaPatchFile.PreviousRetainLengths.Split(new[] { ';' }), + facade.DeltaPatchFile.PreviousRetainOffsets.Split(new[] { ';' }), + apiPatchingSymbolFlags, + optimizePatchSizeForLargeFiles, + out retainRangeWarning)) + { + PatchAPI.PatchInterop.ExtractDeltaHeader(deltaFile, headerFile); + + facade.WixFile.Source = deltaFile; + facade.WixFile.DeltaPatchHeaderSource = headerFile; + } + + if (retainRangeWarning) + { + // TODO: get patch family to add to warning message for PatchWiz parity. + Messaging.Instance.OnMessage(WixWarnings.RetainRangeMismatch(facade.File.SourceLineNumbers, facade.File.File)); + } + } + } + } + } +} diff --git a/src/WixToolset.Core/Bind/Databases/CreateSpecialPropertiesCommand.cs b/src/WixToolset.Core/Bind/Databases/CreateSpecialPropertiesCommand.cs new file mode 100644 index 00000000..5db2768b --- /dev/null +++ b/src/WixToolset.Core/Bind/Databases/CreateSpecialPropertiesCommand.cs @@ -0,0 +1,68 @@ +// 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. + +namespace WixToolset.Bind.Databases +{ + using System; + using System.Collections.Generic; + using WixToolset.Data; + using WixToolset.Data.Rows; + + internal class CreateSpecialPropertiesCommand : ICommand + { + public Table PropertyTable { private get; set; } + + public Table WixPropertyTable { private get; set; } + + public void Execute() + { + // Create the special properties. + if (null != this.WixPropertyTable) + { + // Create lists of the properties that contribute to the special lists of properties. + SortedSet adminProperties = new SortedSet(); + SortedSet secureProperties = new SortedSet(); + SortedSet hiddenProperties = new SortedSet(); + + foreach (WixPropertyRow wixPropertyRow in this.WixPropertyTable.Rows) + { + if (wixPropertyRow.Admin) + { + adminProperties.Add(wixPropertyRow.Id); + } + + if (wixPropertyRow.Hidden) + { + hiddenProperties.Add(wixPropertyRow.Id); + } + + if (wixPropertyRow.Secure) + { + secureProperties.Add(wixPropertyRow.Id); + } + } + + Table propertyTable = this.PropertyTable; + if (0 < adminProperties.Count) + { + PropertyRow row = (PropertyRow)propertyTable.CreateRow(null); + row.Property = "AdminProperties"; + row.Value = String.Join(";", adminProperties); + } + + if (0 < secureProperties.Count) + { + PropertyRow row = (PropertyRow)propertyTable.CreateRow(null); + row.Property = "SecureCustomProperties"; + row.Value = String.Join(";", secureProperties); + } + + if (0 < hiddenProperties.Count) + { + PropertyRow row = (PropertyRow)propertyTable.CreateRow(null); + row.Property = "MsiHiddenProperties"; + row.Value = String.Join(";", hiddenProperties); + } + } + } + } +} diff --git a/src/WixToolset.Core/Bind/Databases/ExtractMergeModuleFilesCommand.cs b/src/WixToolset.Core/Bind/Databases/ExtractMergeModuleFilesCommand.cs new file mode 100644 index 00000000..bee1488b --- /dev/null +++ b/src/WixToolset.Core/Bind/Databases/ExtractMergeModuleFilesCommand.cs @@ -0,0 +1,225 @@ +// 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. + +namespace WixToolset.Bind.Databases +{ + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Runtime.InteropServices; + using WixToolset.Cab; + using WixToolset.Data; + using WixToolset.Data.Rows; + using WixToolset.MergeMod; + using WixToolset.Msi; + using WixToolset.Core.Native; + + /// + /// Retrieve files information and extract them from merge modules. + /// + internal class ExtractMergeModuleFilesCommand : ICommand + { + public IEnumerable FileFacades { private get; set; } + + public Table FileTable { private get; set; } + + public Table WixFileTable { private get; set; } + + public Table WixMergeTable { private get; set; } + + public int OutputInstallerVersion { private get; set; } + + public bool SuppressLayout { private get; set; } + + public string TempFilesLocation { private get; set; } + + public IEnumerable MergeModulesFileFacades { get; private set; } + + public void Execute() + { + List mergeModulesFileFacades = new List(); + + IMsmMerge2 merge = MsmInterop.GetMsmMerge(); + + // Index all of the file rows to be able to detect collisions with files in the Merge Modules. + // It may seem a bit expensive to build up this index solely for the purpose of checking collisions + // and you may be thinking, "Surely, we must need the file rows indexed elsewhere." It turns out + // there are other cases where we need all the file rows indexed, however they are not common cases. + // Now since Merge Modules are already slow and generally less desirable than .wixlibs we'll let + // this case be slightly more expensive because the cost of maintaining an indexed file row collection + // is a lot more costly for the common cases. + Dictionary indexedFileFacades = this.FileFacades.ToDictionary(f => f.File.File, StringComparer.Ordinal); + + foreach (WixMergeRow wixMergeRow in this.WixMergeTable.Rows) + { + bool containsFiles = this.CreateFacadesForMergeModuleFiles(wixMergeRow, mergeModulesFileFacades, indexedFileFacades); + + // If the module has files and creating layout + if (containsFiles && !this.SuppressLayout) + { + this.ExtractFilesFromMergeModule(merge, wixMergeRow); + } + } + + this.MergeModulesFileFacades = mergeModulesFileFacades; + } + + private bool CreateFacadesForMergeModuleFiles(WixMergeRow wixMergeRow, List mergeModulesFileFacades, Dictionary indexedFileFacades) + { + bool containsFiles = false; + + try + { + // read the module's File table to get its FileMediaInformation entries and gather any other information needed from the module. + using (Database db = new Database(wixMergeRow.SourceFile, OpenDatabase.ReadOnly)) + { + if (db.TableExists("File") && db.TableExists("Component")) + { + Dictionary uniqueModuleFileIdentifiers = new Dictionary(StringComparer.OrdinalIgnoreCase); + + using (View view = db.OpenExecuteView("SELECT `File`, `Directory_` FROM `File`, `Component` WHERE `Component_`=`Component`")) + { + // add each file row from the merge module into the file row collection (check for errors along the way) + while (true) + { + using (Record record = view.Fetch()) + { + if (null == record) + { + break; + } + + // NOTE: this is very tricky - the merge module file rows are not added to the + // file table because they should not be created via idt import. Instead, these + // rows are created by merging in the actual modules. + FileRow fileRow = (FileRow)this.FileTable.CreateRow(wixMergeRow.SourceLineNumbers, false); + fileRow.File = record[1]; + fileRow.Compressed = wixMergeRow.FileCompression; + + WixFileRow wixFileRow = (WixFileRow)this.WixFileTable.CreateRow(wixMergeRow.SourceLineNumbers, false); + wixFileRow.Directory = record[2]; + wixFileRow.DiskId = wixMergeRow.DiskId; + wixFileRow.PatchGroup = -1; + wixFileRow.Source = String.Concat(this.TempFilesLocation, Path.DirectorySeparatorChar, "MergeId.", wixMergeRow.Number.ToString(CultureInfo.InvariantCulture), Path.DirectorySeparatorChar, record[1]); + + FileFacade mergeModuleFileFacade = new FileFacade(true, fileRow, wixFileRow); + + FileFacade collidingFacade; + + // If case-sensitive collision with another merge module or a user-authored file identifier. + if (indexedFileFacades.TryGetValue(mergeModuleFileFacade.File.File, out collidingFacade)) + { + Messaging.Instance.OnMessage(WixErrors.DuplicateModuleFileIdentifier(wixMergeRow.SourceLineNumbers, wixMergeRow.Id, collidingFacade.File.File)); + } + else if (uniqueModuleFileIdentifiers.TryGetValue(mergeModuleFileFacade.File.File, out collidingFacade)) // case-insensitive collision with another file identifier in the same merge module + { + Messaging.Instance.OnMessage(WixErrors.DuplicateModuleCaseInsensitiveFileIdentifier(wixMergeRow.SourceLineNumbers, wixMergeRow.Id, mergeModuleFileFacade.File.File, collidingFacade.File.File)); + } + else // no collision + { + mergeModulesFileFacades.Add(mergeModuleFileFacade); + + // Keep updating the indexes as new rows are added. + indexedFileFacades.Add(mergeModuleFileFacade.File.File, mergeModuleFileFacade); + uniqueModuleFileIdentifiers.Add(mergeModuleFileFacade.File.File, mergeModuleFileFacade); + } + + containsFiles = true; + } + } + } + } + + // Get the summary information to detect the Schema + using (SummaryInformation summaryInformation = new SummaryInformation(db)) + { + string moduleInstallerVersionString = summaryInformation.GetProperty(14); + + try + { + int moduleInstallerVersion = Convert.ToInt32(moduleInstallerVersionString, CultureInfo.InvariantCulture); + if (moduleInstallerVersion > this.OutputInstallerVersion) + { + Messaging.Instance.OnMessage(WixWarnings.InvalidHigherInstallerVersionInModule(wixMergeRow.SourceLineNumbers, wixMergeRow.Id, moduleInstallerVersion, this.OutputInstallerVersion)); + } + } + catch (FormatException) + { + throw new WixException(WixErrors.MissingOrInvalidModuleInstallerVersion(wixMergeRow.SourceLineNumbers, wixMergeRow.Id, wixMergeRow.SourceFile, moduleInstallerVersionString)); + } + } + } + } + catch (FileNotFoundException) + { + throw new WixException(WixErrors.FileNotFound(wixMergeRow.SourceLineNumbers, wixMergeRow.SourceFile)); + } + catch (Win32Exception) + { + throw new WixException(WixErrors.CannotOpenMergeModule(wixMergeRow.SourceLineNumbers, wixMergeRow.Id, wixMergeRow.SourceFile)); + } + + return containsFiles; + } + + private void ExtractFilesFromMergeModule(IMsmMerge2 merge, WixMergeRow wixMergeRow) + { + bool moduleOpen = false; + short mergeLanguage; + + try + { + mergeLanguage = Convert.ToInt16(wixMergeRow.Language, CultureInfo.InvariantCulture); + } + catch (System.FormatException) + { + Messaging.Instance.OnMessage(WixErrors.InvalidMergeLanguage(wixMergeRow.SourceLineNumbers, wixMergeRow.Id, wixMergeRow.Language)); + return; + } + + try + { + merge.OpenModule(wixMergeRow.SourceFile, mergeLanguage); + moduleOpen = true; + + string safeMergeId = wixMergeRow.Number.ToString(CultureInfo.InvariantCulture.NumberFormat); + + // extract the module cabinet, then explode all of the files to a temp directory + string moduleCabPath = String.Concat(this.TempFilesLocation, Path.DirectorySeparatorChar, safeMergeId, ".module.cab"); + merge.ExtractCAB(moduleCabPath); + + string mergeIdPath = String.Concat(this.TempFilesLocation, Path.DirectorySeparatorChar, "MergeId.", safeMergeId); + Directory.CreateDirectory(mergeIdPath); + + using (WixExtractCab extractCab = new WixExtractCab()) + { + try + { + extractCab.Extract(moduleCabPath, mergeIdPath); + } + catch (FileNotFoundException) + { + throw new WixException(WixErrors.CabFileDoesNotExist(moduleCabPath, wixMergeRow.SourceFile, mergeIdPath)); + } + catch + { + throw new WixException(WixErrors.CabExtractionFailed(moduleCabPath, wixMergeRow.SourceFile, mergeIdPath)); + } + } + } + catch (COMException ce) + { + throw new WixException(WixErrors.UnableToOpenModule(wixMergeRow.SourceLineNumbers, wixMergeRow.SourceFile, ce.Message)); + } + finally + { + if (moduleOpen) + { + merge.CloseModule(); + } + } + } + } +} diff --git a/src/WixToolset.Core/Bind/Databases/FileFacade.cs b/src/WixToolset.Core/Bind/Databases/FileFacade.cs new file mode 100644 index 00000000..37115c97 --- /dev/null +++ b/src/WixToolset.Core/Bind/Databases/FileFacade.cs @@ -0,0 +1,44 @@ +// 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. + +namespace WixToolset.Bind.Databases +{ + using System.Collections.Generic; + using WixToolset.Data; + using WixToolset.Data.Rows; + + public class FileFacade + { + public FileFacade(FileRow file, WixFileRow wixFile, WixDeltaPatchFileRow deltaPatchFile) + { + this.File = file; + this.WixFile = wixFile; + this.DeltaPatchFile = deltaPatchFile; + } + + public FileFacade(bool fromModule, FileRow file, WixFileRow wixFile) + { + this.FromModule = fromModule; + this.File = file; + this.WixFile = wixFile; + } + + public bool FromModule { get; private set; } + + public FileRow File { get; private set; } + + public WixFileRow WixFile { get; private set; } + + public WixDeltaPatchFileRow DeltaPatchFile { get; private set; } + + /// + /// Gets the set of MsiAssemblyName rows created for this file. + /// + /// RowCollection of MsiAssemblyName table. + public List AssemblyNames { get; set; } + + /// + /// Gets or sets the MsiFileHash row for this file. + /// + public Row Hash { get; set; } + } +} diff --git a/src/WixToolset.Core/Bind/Databases/GetFileFacadesCommand.cs b/src/WixToolset.Core/Bind/Databases/GetFileFacadesCommand.cs new file mode 100644 index 00000000..b6bcd3af --- /dev/null +++ b/src/WixToolset.Core/Bind/Databases/GetFileFacadesCommand.cs @@ -0,0 +1,148 @@ +// 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. + +namespace WixToolset.Bind.Databases +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using WixToolset.Data; + using WixToolset.Data.Rows; + + internal class GetFileFacadesCommand : ICommand + { + public Table FileTable { private get; set; } + + public Table WixFileTable { private get; set; } + + public Table WixDeltaPatchFileTable { private get; set; } + + public Table WixDeltaPatchSymbolPathsTable { private get; set; } + + public List FileFacades { get; private set; } + + public void Execute() + { + List facades = new List(this.FileTable.Rows.Count); + + RowDictionary wixFiles = new RowDictionary(this.WixFileTable); + RowDictionary deltaPatchFiles = new RowDictionary(this.WixDeltaPatchFileTable); + + foreach (FileRow file in this.FileTable.Rows) + { + WixDeltaPatchFileRow deltaPatchFile = null; + + deltaPatchFiles.TryGetValue(file.File, out deltaPatchFile); + + facades.Add(new FileFacade(file, wixFiles[file.File], deltaPatchFile)); + } + + if (null != this.WixDeltaPatchSymbolPathsTable) + { + this.ResolveDeltaPatchSymbolPaths(deltaPatchFiles, facades); + } + + this.FileFacades = facades; + } + + /// + /// Merge data from the WixPatchSymbolPaths rows into the WixDeltaPatchFile rows. + /// + public RowDictionary ResolveDeltaPatchSymbolPaths(RowDictionary deltaPatchFiles, IEnumerable facades) + { + ILookup filesByComponent = null; + ILookup filesByDirectory = null; + ILookup filesByDiskId = null; + + foreach (WixDeltaPatchSymbolPathsRow row in this.WixDeltaPatchSymbolPathsTable.RowsAs().OrderBy(r => r.Type)) + { + switch (row.Type) + { + case SymbolPathType.File: + this.MergeSymbolPaths(row, deltaPatchFiles[row.Id]); + break; + + case SymbolPathType.Component: + if (null == filesByComponent) + { + filesByComponent = facades.ToLookup(f => f.File.Component); + } + + foreach (FileFacade facade in filesByComponent[row.Id]) + { + this.MergeSymbolPaths(row, deltaPatchFiles[facade.File.File]); + } + break; + + case SymbolPathType.Directory: + if (null == filesByDirectory) + { + filesByDirectory = facades.ToLookup(f => f.WixFile.Directory); + } + + foreach (FileFacade facade in filesByDirectory[row.Id]) + { + this.MergeSymbolPaths(row, deltaPatchFiles[facade.File.File]); + } + break; + + case SymbolPathType.Media: + if (null == filesByDiskId) + { + filesByDiskId = facades.ToLookup(f => f.WixFile.DiskId.ToString(CultureInfo.InvariantCulture)); + } + + foreach (FileFacade facade in filesByDiskId[row.Id]) + { + this.MergeSymbolPaths(row, deltaPatchFiles[facade.File.File]); + } + break; + + case SymbolPathType.Product: + foreach (WixDeltaPatchFileRow fileRow in deltaPatchFiles.Values) + { + this.MergeSymbolPaths(row, fileRow); + } + break; + + default: + // error + break; + } + } + + return deltaPatchFiles; + } + + /// + /// Merge data from a row in the WixPatchSymbolsPaths table into an associated WixDeltaPatchFile row. + /// + /// Row from the WixPatchSymbolsPaths table. + /// FileRow into which to set symbol information. + /// This includes PreviousData as well. + private void MergeSymbolPaths(WixDeltaPatchSymbolPathsRow row, WixDeltaPatchFileRow file) + { + if (null == file.Symbols) + { + file.Symbols = row.SymbolPaths; + } + else + { + file.Symbols = String.Concat(file.Symbols, ";", row.SymbolPaths); + } + + Field field = row.Fields[2]; + if (null != field.PreviousData) + { + if (null == file.PreviousSymbols) + { + file.PreviousSymbols = field.PreviousData; + } + else + { + file.PreviousSymbols = String.Concat(file.PreviousSymbols, ";", field.PreviousData); + } + } + } + } +} diff --git a/src/WixToolset.Core/Bind/Databases/MergeModulesCommand.cs b/src/WixToolset.Core/Bind/Databases/MergeModulesCommand.cs new file mode 100644 index 00000000..035ef059 --- /dev/null +++ b/src/WixToolset.Core/Bind/Databases/MergeModulesCommand.cs @@ -0,0 +1,350 @@ +// 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. + +namespace WixToolset.Bind.Databases +{ + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.ComponentModel; + using System.Diagnostics; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Runtime.InteropServices; + using System.Text; + using System.Xml; + using System.Xml.XPath; + using WixToolset.Clr.Interop; + using WixToolset.Data; + using WixToolset.Data.Rows; + using WixToolset.MergeMod; + using WixToolset.Msi; + using WixToolset.Core.Native; + + /// + /// Update file information. + /// + internal class MergeModulesCommand : ICommand + { + public IEnumerable FileFacades { private get; set; } + + public Output Output { private get; set; } + + public string OutputPath { private get; set; } + + public IEnumerable SuppressedTableNames { private get; set; } + + public string TempFilesLocation { private get; set; } + + public void Execute() + { + Debug.Assert(OutputType.Product == this.Output.Type); + + Table wixMergeTable = this.Output.Tables["WixMerge"]; + Table wixFeatureModulesTable = this.Output.Tables["WixFeatureModules"]; + + // check for merge rows to see if there is any work to do + if (null == wixMergeTable || 0 == wixMergeTable.Rows.Count) + { + return; + } + + IMsmMerge2 merge = null; + bool commit = true; + bool logOpen = false; + bool databaseOpen = false; + string logPath = null; + try + { + merge = MsmInterop.GetMsmMerge(); + + logPath = Path.Combine(this.TempFilesLocation, "merge.log"); + merge.OpenLog(logPath); + logOpen = true; + + merge.OpenDatabase(this.OutputPath); + databaseOpen = true; + + // process all the merge rows + foreach (WixMergeRow wixMergeRow in wixMergeTable.Rows) + { + bool moduleOpen = false; + + try + { + short mergeLanguage; + + try + { + mergeLanguage = Convert.ToInt16(wixMergeRow.Language, CultureInfo.InvariantCulture); + } + catch (System.FormatException) + { + Messaging.Instance.OnMessage(WixErrors.InvalidMergeLanguage(wixMergeRow.SourceLineNumbers, wixMergeRow.Id, wixMergeRow.Language)); + continue; + } + + Messaging.Instance.OnMessage(WixVerboses.OpeningMergeModule(wixMergeRow.SourceFile, mergeLanguage)); + merge.OpenModule(wixMergeRow.SourceFile, mergeLanguage); + moduleOpen = true; + + // If there is merge configuration data, create a callback object to contain it all. + ConfigurationCallback callback = null; + if (!String.IsNullOrEmpty(wixMergeRow.ConfigurationData)) + { + callback = new ConfigurationCallback(wixMergeRow.ConfigurationData); + } + + // merge the module into the database that's being built + Messaging.Instance.OnMessage(WixVerboses.MergingMergeModule(wixMergeRow.SourceFile)); + merge.MergeEx(wixMergeRow.Feature, wixMergeRow.Directory, callback); + + // connect any non-primary features + if (null != wixFeatureModulesTable) + { + foreach (Row row in wixFeatureModulesTable.Rows) + { + if (wixMergeRow.Id == (string)row[1]) + { + Messaging.Instance.OnMessage(WixVerboses.ConnectingMergeModule(wixMergeRow.SourceFile, (string)row[0])); + merge.Connect((string)row[0]); + } + } + } + } + catch (COMException) + { + commit = false; + } + finally + { + IMsmErrors mergeErrors = merge.Errors; + + // display all the errors encountered during the merge operations for this module + for (int i = 1; i <= mergeErrors.Count; i++) + { + IMsmError mergeError = mergeErrors[i]; + StringBuilder databaseKeys = new StringBuilder(); + StringBuilder moduleKeys = new StringBuilder(); + + // build a string of the database keys + for (int j = 1; j <= mergeError.DatabaseKeys.Count; j++) + { + if (1 != j) + { + databaseKeys.Append(';'); + } + databaseKeys.Append(mergeError.DatabaseKeys[j]); + } + + // build a string of the module keys + for (int j = 1; j <= mergeError.ModuleKeys.Count; j++) + { + if (1 != j) + { + moduleKeys.Append(';'); + } + moduleKeys.Append(mergeError.ModuleKeys[j]); + } + + // display the merge error based on the msm error type + switch (mergeError.Type) + { + case MsmErrorType.msmErrorExclusion: + Messaging.Instance.OnMessage(WixErrors.MergeExcludedModule(wixMergeRow.SourceLineNumbers, wixMergeRow.Id, moduleKeys.ToString())); + break; + case MsmErrorType.msmErrorFeatureRequired: + Messaging.Instance.OnMessage(WixErrors.MergeFeatureRequired(wixMergeRow.SourceLineNumbers, mergeError.ModuleTable, moduleKeys.ToString(), wixMergeRow.SourceFile, wixMergeRow.Id)); + break; + case MsmErrorType.msmErrorLanguageFailed: + Messaging.Instance.OnMessage(WixErrors.MergeLanguageFailed(wixMergeRow.SourceLineNumbers, mergeError.Language, wixMergeRow.SourceFile)); + break; + case MsmErrorType.msmErrorLanguageUnsupported: + Messaging.Instance.OnMessage(WixErrors.MergeLanguageUnsupported(wixMergeRow.SourceLineNumbers, mergeError.Language, wixMergeRow.SourceFile)); + break; + case MsmErrorType.msmErrorResequenceMerge: + Messaging.Instance.OnMessage(WixWarnings.MergeRescheduledAction(wixMergeRow.SourceLineNumbers, mergeError.DatabaseTable, databaseKeys.ToString(), wixMergeRow.SourceFile)); + break; + case MsmErrorType.msmErrorTableMerge: + if ("_Validation" != mergeError.DatabaseTable) // ignore merge errors in the _Validation table + { + Messaging.Instance.OnMessage(WixWarnings.MergeTableFailed(wixMergeRow.SourceLineNumbers, mergeError.DatabaseTable, databaseKeys.ToString(), wixMergeRow.SourceFile)); + } + break; + case MsmErrorType.msmErrorPlatformMismatch: + Messaging.Instance.OnMessage(WixErrors.MergePlatformMismatch(wixMergeRow.SourceLineNumbers, wixMergeRow.SourceFile)); + break; + default: + Messaging.Instance.OnMessage(WixErrors.UnexpectedException(String.Format(CultureInfo.CurrentUICulture, WixStrings.EXP_UnexpectedMergerErrorWithType, Enum.GetName(typeof(MsmErrorType), mergeError.Type), logPath), "InvalidOperationException", Environment.StackTrace)); + break; + } + } + + if (0 >= mergeErrors.Count && !commit) + { + Messaging.Instance.OnMessage(WixErrors.UnexpectedException(String.Format(CultureInfo.CurrentUICulture, WixStrings.EXP_UnexpectedMergerErrorInSourceFile, wixMergeRow.SourceFile, logPath), "InvalidOperationException", Environment.StackTrace)); + } + + if (moduleOpen) + { + merge.CloseModule(); + } + } + } + } + finally + { + if (databaseOpen) + { + merge.CloseDatabase(commit); + } + + if (logOpen) + { + merge.CloseLog(); + } + } + + // stop processing if an error previously occurred + if (Messaging.Instance.EncounteredError) + { + return; + } + + using (Database db = new Database(this.OutputPath, OpenDatabase.Direct)) + { + Table suppressActionTable = this.Output.Tables["WixSuppressAction"]; + + // suppress individual actions + if (null != suppressActionTable) + { + foreach (Row row in suppressActionTable.Rows) + { + if (db.TableExists((string)row[0])) + { + string query = String.Format(CultureInfo.InvariantCulture, "SELECT * FROM {0} WHERE `Action` = '{1}'", row[0].ToString(), (string)row[1]); + + using (View view = db.OpenExecuteView(query)) + { + using (Record record = view.Fetch()) + { + if (null != record) + { + Messaging.Instance.OnMessage(WixWarnings.SuppressMergedAction((string)row[1], row[0].ToString())); + view.Modify(ModifyView.Delete, record); + } + } + } + } + } + } + + // query for merge module actions in suppressed sequences and drop them + foreach (string tableName in this.SuppressedTableNames) + { + if (!db.TableExists(tableName)) + { + continue; + } + + using (View view = db.OpenExecuteView(String.Concat("SELECT `Action` FROM ", tableName))) + { + while (true) + { + using (Record resultRecord = view.Fetch()) + { + if (null == resultRecord) + { + break; + } + + Messaging.Instance.OnMessage(WixWarnings.SuppressMergedAction(resultRecord.GetString(1), tableName)); + } + } + } + + // drop suppressed sequences + using (View view = db.OpenExecuteView(String.Concat("DROP TABLE ", tableName))) + { + } + + // delete the validation rows + using (View view = db.OpenView(String.Concat("DELETE FROM _Validation WHERE `Table` = ?"))) + { + using (Record record = new Record(1)) + { + record.SetString(1, tableName); + view.Execute(record); + } + } + } + + // now update the Attributes column for the files from the Merge Modules + Messaging.Instance.OnMessage(WixVerboses.ResequencingMergeModuleFiles()); + using (View view = db.OpenView("SELECT `Sequence`, `Attributes` FROM `File` WHERE `File`=?")) + { + foreach (FileFacade file in this.FileFacades) + { + if (!file.FromModule) + { + continue; + } + + using (Record record = new Record(1)) + { + record.SetString(1, file.File.File); + view.Execute(record); + } + + using (Record recordUpdate = view.Fetch()) + { + if (null == recordUpdate) + { + throw new InvalidOperationException("Failed to fetch a File row from the database that was merged in from a module."); + } + + recordUpdate.SetInteger(1, file.File.Sequence); + + // update the file attributes to match the compression specified + // on the Merge element or on the Package element + int attributes = 0; + + // get the current value if its not null + if (!recordUpdate.IsNull(2)) + { + attributes = recordUpdate.GetInteger(2); + } + + if (YesNoType.Yes == file.File.Compressed) + { + // these are mutually exclusive + attributes |= MsiInterop.MsidbFileAttributesCompressed; + attributes &= ~MsiInterop.MsidbFileAttributesNoncompressed; + } + else if (YesNoType.No == file.File.Compressed) + { + // these are mutually exclusive + attributes |= MsiInterop.MsidbFileAttributesNoncompressed; + attributes &= ~MsiInterop.MsidbFileAttributesCompressed; + } + else // not specified + { + Debug.Assert(YesNoType.NotSet == file.File.Compressed); + + // clear any compression bits + attributes &= ~MsiInterop.MsidbFileAttributesCompressed; + attributes &= ~MsiInterop.MsidbFileAttributesNoncompressed; + } + + recordUpdate.SetInteger(2, attributes); + + view.Modify(ModifyView.Update, recordUpdate); + } + } + } + + db.Commit(); + } + } + } +} diff --git a/src/WixToolset.Core/Bind/Databases/ProcessUncompressedFilesCommand.cs b/src/WixToolset.Core/Bind/Databases/ProcessUncompressedFilesCommand.cs new file mode 100644 index 00000000..dd7b85b7 --- /dev/null +++ b/src/WixToolset.Core/Bind/Databases/ProcessUncompressedFilesCommand.cs @@ -0,0 +1,115 @@ +// 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. + +namespace WixToolset.Bind.Databases +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.IO; + using WixToolset.Data; + using WixToolset.Data.Rows; + using WixToolset.Msi; + using WixToolset.Core.Native; + + /// + /// Defines the file transfers necessary to layout the uncompressed files. + /// + internal class ProcessUncompressedFilesCommand : ICommand + { + public string DatabasePath { private get; set; } + + public IEnumerable FileFacades { private get; set; } + + public RowDictionary MediaRows { private get; set; } + + public string LayoutDirectory { private get; set; } + + public bool Compressed { private get; set; } + + public bool LongNamesInImage { private get; set; } + + public Func ResolveMedia { private get; set; } + + public Table WixMediaTable { private get; set; } + + public IEnumerable FileTransfers { get; private set; } + + public void Execute() + { + List fileTransfers = new List(); + + Hashtable directories = new Hashtable(); + + RowDictionary wixMediaRows = new RowDictionary(this.WixMediaTable); + + using (Database db = new Database(this.DatabasePath, OpenDatabase.ReadOnly)) + { + using (View directoryView = db.OpenExecuteView("SELECT `Directory`, `Directory_Parent`, `DefaultDir` FROM `Directory`")) + { + while (true) + { + using (Record directoryRecord = directoryView.Fetch()) + { + if (null == directoryRecord) + { + break; + } + + string sourceName = Installer.GetName(directoryRecord.GetString(3), true, this.LongNamesInImage); + + directories.Add(directoryRecord.GetString(1), new ResolvedDirectory(directoryRecord.GetString(2), sourceName)); + } + } + } + + using (View fileView = db.OpenView("SELECT `Directory_`, `FileName` FROM `Component`, `File` WHERE `Component`.`Component`=`File`.`Component_` AND `File`.`File`=?")) + { + using (Record fileQueryRecord = new Record(1)) + { + // for each file in the array of uncompressed files + foreach (FileFacade facade in this.FileFacades) + { + MediaRow mediaRow = this.MediaRows.Get(facade.WixFile.DiskId); + string relativeFileLayoutPath = null; + + WixMediaRow wixMediaRow = null; + string mediaLayoutFolder = null; + + if (wixMediaRows.TryGetValue(mediaRow.GetKey(), out wixMediaRow)) + { + mediaLayoutFolder = wixMediaRow.Layout; + } + + string mediaLayoutDirectory = this.ResolveMedia(mediaRow, mediaLayoutFolder, this.LayoutDirectory); + + // setup up the query record and find the appropriate file in the + // previously executed file view + fileQueryRecord[1] = facade.File.File; + fileView.Execute(fileQueryRecord); + + using (Record fileRecord = fileView.Fetch()) + { + if (null == fileRecord) + { + throw new WixException(WixErrors.FileIdentifierNotFound(facade.File.SourceLineNumbers, facade.File.File)); + } + + relativeFileLayoutPath = Binder.GetFileSourcePath(directories, fileRecord[1], fileRecord[2], this.Compressed, this.LongNamesInImage); + } + + // finally put together the base media layout path and the relative file layout path + string fileLayoutPath = Path.Combine(mediaLayoutDirectory, relativeFileLayoutPath); + FileTransfer transfer; + if (FileTransfer.TryCreate(facade.WixFile.Source, fileLayoutPath, false, "File", facade.File.SourceLineNumbers, out transfer)) + { + fileTransfers.Add(transfer); + } + } + } + } + } + + this.FileTransfers = fileTransfers; + } + } +} diff --git a/src/WixToolset.Core/Bind/Databases/UpdateControlTextCommand.cs b/src/WixToolset.Core/Bind/Databases/UpdateControlTextCommand.cs new file mode 100644 index 00000000..9e17ee02 --- /dev/null +++ b/src/WixToolset.Core/Bind/Databases/UpdateControlTextCommand.cs @@ -0,0 +1,80 @@ +// 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. + +namespace WixToolset.Bind.Databases +{ + using System; + using System.IO; + using WixToolset.Data; + using WixToolset.Data.Rows; + + internal class UpdateControlTextCommand : ICommand + { + public Table BBControlTable { private get; set; } + + public Table WixBBControlTable { private get; set; } + + public Table ControlTable { private get; set; } + + public Table WixControlTable { private get; set; } + + public void Execute() + { + if (null != this.WixBBControlTable) + { + RowDictionary bbControlRows = new RowDictionary(this.BBControlTable); + foreach (Row wixRow in this.WixBBControlTable.Rows) + { + BBControlRow bbControlRow = bbControlRows.Get(wixRow.GetPrimaryKey()); + bbControlRow.Text = this.ReadTextFile(bbControlRow.SourceLineNumbers, wixRow.FieldAsString(2)); + } + } + + if (null != this.WixControlTable) + { + RowDictionary controlRows = new RowDictionary(this.ControlTable); + foreach (Row wixRow in this.WixControlTable.Rows) + { + ControlRow controlRow = controlRows.Get(wixRow.GetPrimaryKey()); + controlRow.Text = this.ReadTextFile(controlRow.SourceLineNumbers, wixRow.FieldAsString(2)); + } + } + } + + /// + /// Reads a text file and returns the contents. + /// + /// Source line numbers for row from source. + /// Source path to file to read. + /// Text string read from file. + private string ReadTextFile(SourceLineNumber sourceLineNumbers, string source) + { + string text = null; + + try + { + using (StreamReader reader = new StreamReader(source)) + { + text = reader.ReadToEnd(); + } + } + catch (DirectoryNotFoundException e) + { + Messaging.Instance.OnMessage(WixErrors.BinderFileManagerMissingFile(sourceLineNumbers, e.Message)); + } + catch (FileNotFoundException e) + { + Messaging.Instance.OnMessage(WixErrors.BinderFileManagerMissingFile(sourceLineNumbers, e.Message)); + } + catch (IOException e) + { + Messaging.Instance.OnMessage(WixErrors.BinderFileManagerMissingFile(sourceLineNumbers, e.Message)); + } + catch (NotSupportedException) + { + Messaging.Instance.OnMessage(WixErrors.FileNotFound(sourceLineNumbers, source)); + } + + return text; + } + } +} diff --git a/src/WixToolset.Core/Bind/Databases/UpdateFileFacadesCommand.cs b/src/WixToolset.Core/Bind/Databases/UpdateFileFacadesCommand.cs new file mode 100644 index 00000000..36818afa --- /dev/null +++ b/src/WixToolset.Core/Bind/Databases/UpdateFileFacadesCommand.cs @@ -0,0 +1,532 @@ +// 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. + +namespace WixToolset.Bind.Databases +{ + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.ComponentModel; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Xml; + using System.Xml.XPath; + using WixToolset.Clr.Interop; + using WixToolset.Data; + using WixToolset.Data.Rows; + using WixToolset.Msi; + + /// + /// Update file information. + /// + internal class UpdateFileFacadesCommand : ICommand + { + public IEnumerable FileFacades { private get; set; } + + public IEnumerable UpdateFileFacades { private get; set; } + + public string ModularizationGuid { private get; set; } + + public Output Output { private get; set; } + + public bool OverwriteHash { private get; set; } + + public TableDefinitionCollection TableDefinitions { private get; set; } + + public IDictionary VariableCache { private get; set; } + + public void Execute() + { + foreach (FileFacade file in this.UpdateFileFacades) + { + this.UpdateFileFacade(file); + } + } + + private void UpdateFileFacade(FileFacade file) + { + FileInfo fileInfo = null; + try + { + fileInfo = new FileInfo(file.WixFile.Source); + } + catch (ArgumentException) + { + Messaging.Instance.OnMessage(WixErrors.InvalidFileName(file.File.SourceLineNumbers, file.WixFile.Source)); + return; + } + catch (PathTooLongException) + { + Messaging.Instance.OnMessage(WixErrors.InvalidFileName(file.File.SourceLineNumbers, file.WixFile.Source)); + return; + } + catch (NotSupportedException) + { + Messaging.Instance.OnMessage(WixErrors.InvalidFileName(file.File.SourceLineNumbers, file.WixFile.Source)); + return; + } + + if (!fileInfo.Exists) + { + Messaging.Instance.OnMessage(WixErrors.CannotFindFile(file.File.SourceLineNumbers, file.File.File, file.File.FileName, file.WixFile.Source)); + return; + } + + using (FileStream fileStream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + if (Int32.MaxValue < fileStream.Length) + { + throw new WixException(WixErrors.FileTooLarge(file.File.SourceLineNumbers, file.WixFile.Source)); + } + + file.File.FileSize = Convert.ToInt32(fileStream.Length, CultureInfo.InvariantCulture); + } + + string version = null; + string language = null; + try + { + Installer.GetFileVersion(fileInfo.FullName, out version, out language); + } + catch (Win32Exception e) + { + if (0x2 == e.NativeErrorCode) // ERROR_FILE_NOT_FOUND + { + throw new WixException(WixErrors.FileNotFound(file.File.SourceLineNumbers, fileInfo.FullName)); + } + else + { + throw new WixException(WixErrors.Win32Exception(e.NativeErrorCode, e.Message)); + } + } + + // If there is no version, it is assumed there is no language because it won't matter in the versioning of the install. + if (String.IsNullOrEmpty(version)) // unversioned files have their hashes added to the MsiFileHash table + { + if (!this.OverwriteHash) + { + // not overwriting hash, so don't do the rest of these options. + } + else if (null != file.File.Version) + { + // Search all of the file rows available to see if the specified version is actually a companion file. Yes, this looks + // 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. + // That's a reasonable thought but companion file usage is usually pretty rare so we'd be doing something expensive (indexing + // all the file rows) for a relatively uncommon situation. Let's not do that. + // + // Also, if we do not find a matching file identifier then the user provided a default version and is providing a version + // for unversioned file. That's allowed but generally a dangerous thing to do so let's point that out to the user. + if (!this.FileFacades.Any(r => file.File.Version.Equals(r.File.File, StringComparison.Ordinal))) + { + Messaging.Instance.OnMessage(WixWarnings.DefaultVersionUsedForUnversionedFile(file.File.SourceLineNumbers, file.File.Version, file.File.File)); + } + } + else + { + if (null != file.File.Language) + { + Messaging.Instance.OnMessage(WixWarnings.DefaultLanguageUsedForUnversionedFile(file.File.SourceLineNumbers, file.File.Language, file.File.File)); + } + + int[] hash; + try + { + Installer.GetFileHash(fileInfo.FullName, 0, out hash); + } + catch (Win32Exception e) + { + if (0x2 == e.NativeErrorCode) // ERROR_FILE_NOT_FOUND + { + throw new WixException(WixErrors.FileNotFound(file.File.SourceLineNumbers, fileInfo.FullName)); + } + else + { + throw new WixException(WixErrors.Win32Exception(e.NativeErrorCode, fileInfo.FullName, e.Message)); + } + } + + if (null == file.Hash) + { + Table msiFileHashTable = this.Output.EnsureTable(this.TableDefinitions["MsiFileHash"]); + file.Hash = msiFileHashTable.CreateRow(file.File.SourceLineNumbers); + } + + file.Hash[0] = file.File.File; + file.Hash[1] = 0; + file.Hash[2] = hash[0]; + file.Hash[3] = hash[1]; + file.Hash[4] = hash[2]; + file.Hash[5] = hash[3]; + } + } + else // update the file row with the version and language information. + { + // If no version was provided by the user, use the version from the file itself. + // This is the most common case. + if (String.IsNullOrEmpty(file.File.Version)) + { + file.File.Version = version; + } + else if (!this.FileFacades.Any(r => file.File.Version.Equals(r.File.File, StringComparison.Ordinal))) // this looks expensive, but see explanation below. + { + // The user provided a default version for the file row so we looked for a companion file (a file row with Id matching + // the version value). We didn't find it so, we will override the default version they provided with the actual + // version from the file itself. Now, I know it looks expensive to search through all the file rows trying to match + // on the Id. However, the alternative is to build a big index of all file rows to do look ups. Since this case + // where the file version is already present is rare (companion files are pretty uncommon), we'll do the more + // CPU intensive search to save on the memory intensive index that wouldn't be used much. + // + // Also note this case can occur when the file is being updated using the WixBindUpdatedFiles extension mechanism. + // That's typically even more rare than companion files so again, no index, just search. + file.File.Version = version; + } + + if (!String.IsNullOrEmpty(file.File.Language) && String.IsNullOrEmpty(language)) + { + Messaging.Instance.OnMessage(WixWarnings.DefaultLanguageUsedForVersionedFile(file.File.SourceLineNumbers, file.File.Language, file.File.File)); + } + else // override the default provided by the user (usually nothing) with the actual language from the file itself. + { + file.File.Language = language; + } + + // Populate the binder variables for this file information if requested. + if (null != this.VariableCache) + { + if (!String.IsNullOrEmpty(file.File.Version)) + { + string key = String.Format(CultureInfo.InvariantCulture, "fileversion.{0}", BindDatabaseCommand.Demodularize(this.Output.Type, this.ModularizationGuid, file.File.File)); + this.VariableCache[key] = file.File.Version; + } + + if (!String.IsNullOrEmpty(file.File.Language)) + { + string key = String.Format(CultureInfo.InvariantCulture, "filelanguage.{0}", BindDatabaseCommand.Demodularize(this.Output.Type, ModularizationGuid, file.File.File)); + this.VariableCache[key] = file.File.Language; + } + } + } + + // If this is a CLR assembly, load the assembly and get the assembly name information + if (FileAssemblyType.DotNetAssembly == file.WixFile.AssemblyType) + { + bool targetNetfx1 = false; + StringDictionary assemblyNameValues = new StringDictionary(); + + ClrInterop.IReferenceIdentity referenceIdentity = null; + Guid referenceIdentityGuid = ClrInterop.ReferenceIdentityGuid; + uint result = ClrInterop.GetAssemblyIdentityFromFile(fileInfo.FullName, ref referenceIdentityGuid, out referenceIdentity); + if (0 == result && null != referenceIdentity) + { + string imageRuntimeVersion = referenceIdentity.GetAttribute(null, "ImageRuntimeVersion"); + if (null != imageRuntimeVersion) + { + targetNetfx1 = imageRuntimeVersion.StartsWith("v1", StringComparison.OrdinalIgnoreCase); + } + + string culture = referenceIdentity.GetAttribute(null, "Culture") ?? "neutral"; + assemblyNameValues.Add("Culture", culture); + + string name = referenceIdentity.GetAttribute(null, "Name"); + if (null != name) + { + assemblyNameValues.Add("Name", name); + } + + string processorArchitecture = referenceIdentity.GetAttribute(null, "ProcessorArchitecture"); + if (null != processorArchitecture) + { + assemblyNameValues.Add("ProcessorArchitecture", processorArchitecture); + } + + string publicKeyToken = referenceIdentity.GetAttribute(null, "PublicKeyToken"); + if (null != publicKeyToken) + { + bool publicKeyIsNeutral = (String.Equals(publicKeyToken, "neutral", StringComparison.OrdinalIgnoreCase)); + + // Managed code expects "null" instead of "neutral", and + // this won't be installed to the GAC since it's not signed anyway. + assemblyNameValues.Add("publicKeyToken", publicKeyIsNeutral ? "null" : publicKeyToken.ToUpperInvariant()); + assemblyNameValues.Add("publicKeyTokenPreservedCase", publicKeyIsNeutral ? "null" : publicKeyToken); + } + else if (file.WixFile.AssemblyApplication == null) + { + throw new WixException(WixErrors.GacAssemblyNoStrongName(file.File.SourceLineNumbers, fileInfo.FullName, file.File.Component)); + } + + string assemblyVersion = referenceIdentity.GetAttribute(null, "Version"); + if (null != version) + { + assemblyNameValues.Add("Version", assemblyVersion); + } + } + else + { + Messaging.Instance.OnMessage(WixErrors.InvalidAssemblyFile(file.File.SourceLineNumbers, fileInfo.FullName, String.Format(CultureInfo.InvariantCulture, "HRESULT: 0x{0:x8}", result))); + return; + } + + Table assemblyNameTable = this.Output.EnsureTable(this.TableDefinitions["MsiAssemblyName"]); + if (assemblyNameValues.ContainsKey("name")) + { + this.SetMsiAssemblyName(assemblyNameTable, file, "name", assemblyNameValues["name"]); + } + + if (!String.IsNullOrEmpty(version)) + { + this.SetMsiAssemblyName(assemblyNameTable, file, "fileVersion", version); + } + + if (assemblyNameValues.ContainsKey("version")) + { + string assemblyVersion = assemblyNameValues["version"]; + + if (!targetNetfx1) + { + // There is a bug in v1 fusion that requires the assembly's "version" attribute + // to be equal to or longer than the "fileVersion" in length when its present; + // the workaround is to prepend zeroes to the last version number in the assembly + // version. + if (null != version && version.Length > assemblyVersion.Length) + { + string padding = new string('0', version.Length - assemblyVersion.Length); + string[] assemblyVersionNumbers = assemblyVersion.Split('.'); + + if (assemblyVersionNumbers.Length > 0) + { + assemblyVersionNumbers[assemblyVersionNumbers.Length - 1] = String.Concat(padding, assemblyVersionNumbers[assemblyVersionNumbers.Length - 1]); + assemblyVersion = String.Join(".", assemblyVersionNumbers); + } + } + } + + this.SetMsiAssemblyName(assemblyNameTable, file, "version", assemblyVersion); + } + + if (assemblyNameValues.ContainsKey("culture")) + { + this.SetMsiAssemblyName(assemblyNameTable, file, "culture", assemblyNameValues["culture"]); + } + + if (assemblyNameValues.ContainsKey("publicKeyToken")) + { + this.SetMsiAssemblyName(assemblyNameTable, file, "publicKeyToken", assemblyNameValues["publicKeyToken"]); + } + + if (!String.IsNullOrEmpty(file.WixFile.ProcessorArchitecture)) + { + this.SetMsiAssemblyName(assemblyNameTable, file, "processorArchitecture", file.WixFile.ProcessorArchitecture); + } + + if (assemblyNameValues.ContainsKey("processorArchitecture")) + { + this.SetMsiAssemblyName(assemblyNameTable, file, "processorArchitecture", assemblyNameValues["processorArchitecture"]); + } + + // add the assembly name to the information cache + if (null != this.VariableCache) + { + string fileId = BindDatabaseCommand.Demodularize(this.Output.Type, this.ModularizationGuid, file.File.File); + string key = String.Concat("assemblyfullname.", fileId); + string assemblyName = String.Concat(assemblyNameValues["name"], ", version=", assemblyNameValues["version"], ", culture=", assemblyNameValues["culture"], ", publicKeyToken=", String.IsNullOrEmpty(assemblyNameValues["publicKeyToken"]) ? "null" : assemblyNameValues["publicKeyToken"]); + if (assemblyNameValues.ContainsKey("processorArchitecture")) + { + assemblyName = String.Concat(assemblyName, ", processorArchitecture=", assemblyNameValues["processorArchitecture"]); + } + + this.VariableCache[key] = assemblyName; + + // Add entries with the preserved case publicKeyToken + string pcAssemblyNameKey = String.Concat("assemblyfullnamepreservedcase.", fileId); + this.VariableCache[pcAssemblyNameKey] = (assemblyNameValues["publicKeyToken"] == assemblyNameValues["publicKeyTokenPreservedCase"]) ? assemblyName : assemblyName.Replace(assemblyNameValues["publicKeyToken"], assemblyNameValues["publicKeyTokenPreservedCase"]); + + string pcPublicKeyTokenKey = String.Concat("assemblypublickeytokenpreservedcase.", fileId); + this.VariableCache[pcPublicKeyTokenKey] = assemblyNameValues["publicKeyTokenPreservedCase"]; + } + } + else if (FileAssemblyType.Win32Assembly == file.WixFile.AssemblyType) + { + // TODO: Consider passing in the this.FileFacades as an indexed collection instead of searching through + // all files like this. Even though this is a rare case it looks like we might be able to index the + // file earlier. + FileFacade fileManifest = this.FileFacades.SingleOrDefault(r => r.File.File.Equals(file.WixFile.AssemblyManifest, StringComparison.Ordinal)); + if (null == fileManifest) + { + Messaging.Instance.OnMessage(WixErrors.MissingManifestForWin32Assembly(file.File.SourceLineNumbers, file.File.File, file.WixFile.AssemblyManifest)); + } + + string win32Type = null; + string win32Name = null; + string win32Version = null; + string win32ProcessorArchitecture = null; + string win32PublicKeyToken = null; + + // loading the dom is expensive we want more performant APIs than the DOM + // Navigator is cheaper than dom. Perhaps there is a cheaper API still. + try + { + XPathDocument doc = new XPathDocument(fileManifest.WixFile.Source); + XPathNavigator nav = doc.CreateNavigator(); + nav.MoveToRoot(); + + // this assumes a particular schema for a win32 manifest and does not + // provide error checking if the file does not conform to schema. + // The fallback case here is that nothing is added to the MsiAssemblyName + // table for an out of tolerance Win32 manifest. Perhaps warnings needed. + if (nav.MoveToFirstChild()) + { + while (nav.NodeType != XPathNodeType.Element || nav.Name != "assembly") + { + nav.MoveToNext(); + } + + if (nav.MoveToFirstChild()) + { + bool hasNextSibling = true; + while (nav.NodeType != XPathNodeType.Element || nav.Name != "assemblyIdentity" && hasNextSibling) + { + hasNextSibling = nav.MoveToNext(); + } + if (!hasNextSibling) + { + Messaging.Instance.OnMessage(WixErrors.InvalidManifestContent(file.File.SourceLineNumbers, fileManifest.WixFile.Source)); + return; + } + + if (nav.MoveToAttribute("type", String.Empty)) + { + win32Type = nav.Value; + nav.MoveToParent(); + } + + if (nav.MoveToAttribute("name", String.Empty)) + { + win32Name = nav.Value; + nav.MoveToParent(); + } + + if (nav.MoveToAttribute("version", String.Empty)) + { + win32Version = nav.Value; + nav.MoveToParent(); + } + + if (nav.MoveToAttribute("processorArchitecture", String.Empty)) + { + win32ProcessorArchitecture = nav.Value; + nav.MoveToParent(); + } + + if (nav.MoveToAttribute("publicKeyToken", String.Empty)) + { + win32PublicKeyToken = nav.Value; + nav.MoveToParent(); + } + } + } + } + catch (FileNotFoundException fe) + { + Messaging.Instance.OnMessage(WixErrors.FileNotFound(new SourceLineNumber(fileManifest.WixFile.Source), fe.FileName, "AssemblyManifest")); + } + catch (XmlException xe) + { + Messaging.Instance.OnMessage(WixErrors.InvalidXml(new SourceLineNumber(fileManifest.WixFile.Source), "manifest", xe.Message)); + } + + Table assemblyNameTable = this.Output.EnsureTable(this.TableDefinitions["MsiAssemblyName"]); + if (!String.IsNullOrEmpty(win32Name)) + { + this.SetMsiAssemblyName(assemblyNameTable, file, "name", win32Name); + } + + if (!String.IsNullOrEmpty(win32Version)) + { + this.SetMsiAssemblyName(assemblyNameTable, file, "version", win32Version); + } + + if (!String.IsNullOrEmpty(win32Type)) + { + this.SetMsiAssemblyName(assemblyNameTable, file, "type", win32Type); + } + + if (!String.IsNullOrEmpty(win32ProcessorArchitecture)) + { + this.SetMsiAssemblyName(assemblyNameTable, file, "processorArchitecture", win32ProcessorArchitecture); + } + + if (!String.IsNullOrEmpty(win32PublicKeyToken)) + { + this.SetMsiAssemblyName(assemblyNameTable, file, "publicKeyToken", win32PublicKeyToken); + } + } + } + + /// + /// Set an MsiAssemblyName row. If it was directly authored, override the value, otherwise + /// create a new row. + /// + /// MsiAssemblyName table. + /// FileFacade containing the assembly read for the MsiAssemblyName row. + /// MsiAssemblyName name. + /// MsiAssemblyName value. + private void SetMsiAssemblyName(Table assemblyNameTable, FileFacade file, string name, string value) + { + // check for null value (this can occur when grabbing the file version from an assembly without one) + if (String.IsNullOrEmpty(value)) + { + Messaging.Instance.OnMessage(WixWarnings.NullMsiAssemblyNameValue(file.File.SourceLineNumbers, file.File.Component, name)); + } + else + { + Row assemblyNameRow = null; + + // override directly authored value + foreach (Row row in assemblyNameTable.Rows) + { + if ((string)row[0] == file.File.Component && (string)row[1] == name) + { + assemblyNameRow = row; + break; + } + } + + // 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. + if ("name" == name && FileAssemblyType.DotNetAssembly == file.WixFile.AssemblyType && + String.IsNullOrEmpty(file.WixFile.AssemblyApplication) && + !String.Equals(Path.GetFileNameWithoutExtension(file.File.LongFileName), value, StringComparison.OrdinalIgnoreCase)) + { + Messaging.Instance.OnMessage(WixErrors.GACAssemblyIdentityWarning(file.File.SourceLineNumbers, Path.GetFileNameWithoutExtension(file.File.LongFileName), value)); + } + + if (null == assemblyNameRow) + { + assemblyNameRow = assemblyNameTable.CreateRow(file.File.SourceLineNumbers); + assemblyNameRow[0] = file.File.Component; + assemblyNameRow[1] = name; + assemblyNameRow[2] = value; + + // put the MsiAssemblyName row in the same section as the related File row + assemblyNameRow.SectionId = file.File.SectionId; + + if (null == file.AssemblyNames) + { + file.AssemblyNames = new List(); + } + + file.AssemblyNames.Add(assemblyNameRow); + } + else + { + assemblyNameRow[2] = value; + } + + if (this.VariableCache != null) + { + string key = String.Format(CultureInfo.InvariantCulture, "assembly{0}.{1}", name, BindDatabaseCommand.Demodularize(this.Output.Type, this.ModularizationGuid, file.File.File)).ToLowerInvariant(); + this.VariableCache[key] = (string)assemblyNameRow[2]; + } + } + } + } +} diff --git a/src/WixToolset.Core/Bind/DelayedField.cs b/src/WixToolset.Core/Bind/DelayedField.cs new file mode 100644 index 00000000..181ac3e3 --- /dev/null +++ b/src/WixToolset.Core/Bind/DelayedField.cs @@ -0,0 +1,38 @@ +// 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. + +namespace WixToolset.Bind +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using WixToolset.Data; + + /// + /// Structure used to hold a row and field that contain binder variables, which need to be resolved + /// later, once the files have been resolved. + /// + internal class DelayedField + { + /// + /// Basic constructor for struct + /// + /// Row for the field. + /// Field needing further resolution. + public DelayedField(Row row, Field field) + { + this.Row = row; + this.Field = field; + } + + /// + /// The row containing the field. + /// + public Row Row { get; private set; } + + /// + /// The field needing further resolving. + /// + public Field Field { get; private set; } + } +} diff --git a/src/WixToolset.Core/Bind/ExtractEmbeddedFiles.cs b/src/WixToolset.Core/Bind/ExtractEmbeddedFiles.cs new file mode 100644 index 00000000..0ecd0096 --- /dev/null +++ b/src/WixToolset.Core/Bind/ExtractEmbeddedFiles.cs @@ -0,0 +1,83 @@ +// 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. + +namespace WixToolset.Bind +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Security.Cryptography; + using System.Text; + + /// + /// Internal helper class used to extract embedded files. + /// + internal sealed class ExtractEmbeddedFiles + { + private Dictionary> filesWithEmbeddedFiles = new Dictionary>(); + + public IEnumerable Uris { get { return this.filesWithEmbeddedFiles.Keys; } } + + /// + /// Adds an embedded file index to track and returns the path where the embedded file will be extracted. Duplicates will return the same extract path. + /// + /// Uri to file containing the embedded files. + /// Index of the embedded file to extract. + /// Path where temporary files should be placed. + /// The extract path for the embedded file. + public string AddEmbeddedFileIndex(Uri uri, int embeddedFileIndex, string tempPath) + { + string extractPath; + SortedList extracts; + + // If the uri to the file that contains the embedded file does not already have embedded files + // being extracted, create the dictionary to track that. + if (!filesWithEmbeddedFiles.TryGetValue(uri, out extracts)) + { + extracts = new SortedList(); + filesWithEmbeddedFiles.Add(uri, extracts); + } + + // If the embedded file is not already tracked in the dictionary of extracts, add it. + if (!extracts.TryGetValue(embeddedFileIndex, out extractPath)) + { + string localFileNameWithoutExtension = Path.GetFileNameWithoutExtension(uri.LocalPath); + string unique = this.HashUri(uri.AbsoluteUri); + string extractedName = String.Format(CultureInfo.InvariantCulture, @"{0}_{1}\{2}", localFileNameWithoutExtension, unique, embeddedFileIndex); + + extractPath = Path.Combine(tempPath, extractedName); + extracts.Add(embeddedFileIndex, extractPath); + } + + return extractPath; + } + + public IEnumerable GetExtractFilesForUri(Uri uri) + { + SortedList extracts; + if (!filesWithEmbeddedFiles.TryGetValue(uri, out extracts)) + { + extracts = new SortedList(); + } + + return extracts.Select(e => new ExtractFile() { EmbeddedFileIndex = e.Key, OutputPath = e.Value }); + } + + private string HashUri(string uri) + { + using (SHA1 sha1 = new SHA1CryptoServiceProvider()) + { + byte[] hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(uri)); + return Convert.ToBase64String(hash).TrimEnd('=').Replace('+', '-').Replace('/', '_'); + } + } + + internal struct ExtractFile + { + public int EmbeddedFileIndex { get; set; } + + public string OutputPath { get; set; } + } + } +} diff --git a/src/WixToolset.Core/Bind/ExtractEmbeddedFilesCommand.cs b/src/WixToolset.Core/Bind/ExtractEmbeddedFilesCommand.cs new file mode 100644 index 00000000..68bfd8d7 --- /dev/null +++ b/src/WixToolset.Core/Bind/ExtractEmbeddedFilesCommand.cs @@ -0,0 +1,53 @@ +// 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. + +namespace WixToolset.Bind +{ + using System.IO; + using System.Reflection; + using WixToolset.Data; + + internal class ExtractEmbeddedFilesCommand : ICommand + { + public ExtractEmbeddedFiles FilesWithEmbeddedFiles { private get; set; } + + public void Execute() + { + foreach (var baseUri in this.FilesWithEmbeddedFiles.Uris) + { + Stream stream = null; + try + { + // If the embedded files are stored in an assembly resource stream (usually + // a .wixlib embedded in a WixExtension). + if ("embeddedresource" == baseUri.Scheme) + { + string assemblyPath = Path.GetFullPath(baseUri.LocalPath); + string resourceName = baseUri.Fragment.TrimStart('#'); + + Assembly assembly = Assembly.LoadFile(assemblyPath); + stream = assembly.GetManifestResourceStream(resourceName); + } + else // normal file (usually a binary .wixlib on disk). + { + stream = File.OpenRead(baseUri.LocalPath); + } + + using (FileStructure fs = FileStructure.Read(stream)) + { + foreach (var embeddedFile in this.FilesWithEmbeddedFiles.GetExtractFilesForUri(baseUri)) + { + fs.ExtractEmbeddedFile(embeddedFile.EmbeddedFileIndex, embeddedFile.OutputPath); + } + } + } + finally + { + if (null != stream) + { + stream.Close(); + } + } + } + } + } +} diff --git a/src/WixToolset.Core/Bind/FileTransfer.cs b/src/WixToolset.Core/Bind/FileTransfer.cs new file mode 100644 index 00000000..64bbc5f1 --- /dev/null +++ b/src/WixToolset.Core/Bind/FileTransfer.cs @@ -0,0 +1,113 @@ +// 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. + +namespace WixToolset.Bind +{ + using System; + using System.IO; + using WixToolset; + using WixToolset.Data; + + /// + /// Structure used for all file transfer information. + /// + internal class FileTransfer + { + /// Source path to file. + public string Source { get; set; } + + /// Destination path for file. + public string Destination { get; set; } + + /// Flag if file should be moved (optimal). + public bool Move { get; set; } + + /// Optional source line numbers where this file transfer orginated. + public SourceLineNumber SourceLineNumbers { get; set; } + + /// Optional type of file this transfer is moving or copying. + public string Type { get; set; } + + /// Indicates whether the file transer was a built by this build or copied from other some build. + internal bool Built { get; set; } + + /// Set during layout of media when the file transfer when the source and target resolve to the same path. + internal bool Redundant { get; set; } + + /// + /// Prefer the TryCreate() method to create FileTransfer objects. + /// + /// Source path to file. + /// Destination path for file. + /// File if file should be moved (optimal). + /// Optional type of file this transfer is transferring. + /// Optional source line numbers wher this transfer originated. + public FileTransfer(string source, string destination, bool move, string type = null, SourceLineNumber sourceLineNumbers = null) + { + this.Source = source; + this.Destination = destination; + this.Move = move; + + this.Type = type; + this.SourceLineNumbers = sourceLineNumbers; + } + + /// + /// Creates a file transfer if the source and destination are different. + /// + /// Source path to file. + /// Destination path for file. + /// File if file should be moved (optimal). + /// Optional type of file this transfer is transferring. + /// Optional source line numbers wher this transfer originated. + /// true if the source and destination are the different, false if no file transfer is created. + public static bool TryCreate(string source, string destination, bool move, string type, SourceLineNumber sourceLineNumbers, out FileTransfer transfer) + { + string sourceFullPath = GetValidatedFullPath(sourceLineNumbers, source); + + string fileLayoutFullPath = GetValidatedFullPath(sourceLineNumbers, destination); + + // if the current source path (where we know that the file already exists) and the resolved + // path as dictated by the Directory table are not the same, then propagate the file. The + // image that we create may have already been done by some other process other than the linker, so + // there is no reason to copy the files to the resolved source if they are already there. + if (String.Equals(sourceFullPath, fileLayoutFullPath, StringComparison.OrdinalIgnoreCase)) + { + transfer = null; + return false; + } + + transfer = new FileTransfer(source, destination, move, type, sourceLineNumbers); + return true; + } + + private static string GetValidatedFullPath(SourceLineNumber sourceLineNumbers, string path) + { + string result; + + try + { + result = Path.GetFullPath(path); + + string filename = Path.GetFileName(result); + + foreach (string reservedName in Common.ReservedFileNames) + { + if (reservedName.Equals(filename, StringComparison.OrdinalIgnoreCase)) + { + throw new WixException(WixErrors.InvalidFileName(sourceLineNumbers, path)); + } + } + } + catch (System.ArgumentException) + { + throw new WixException(WixErrors.InvalidFileName(sourceLineNumbers, path)); + } + catch (System.IO.PathTooLongException) + { + throw new WixException(WixErrors.PathTooLong(sourceLineNumbers, path)); + } + + return result; + } + } +} diff --git a/src/WixToolset.Core/Bind/GenerateDatabaseCommand.cs b/src/WixToolset.Core/Bind/GenerateDatabaseCommand.cs new file mode 100644 index 00000000..fdf1ab32 --- /dev/null +++ b/src/WixToolset.Core/Bind/GenerateDatabaseCommand.cs @@ -0,0 +1,335 @@ +// 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. + +namespace WixToolset.Bind +{ + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.Globalization; + using System.IO; + using System.Text; + using WixToolset.Data; + using WixToolset.Extensibility; + using WixToolset.Msi; + using WixToolset.Core.Native; + + internal class GenerateDatabaseCommand : ICommand + { + public int Codepage { private get; set; } + + public IEnumerable Extensions { private get; set; } + + public IEnumerable FileManagers { private get; set; } + + /// + /// Whether to keep columns added in a transform. + /// + public bool KeepAddedColumns { private get; set; } + + public Output Output { private get; set; } + + public string OutputPath { private get; set; } + + public TableDefinitionCollection TableDefinitions { private get; set; } + + public string TempFilesLocation { private get; set; } + + /// + /// Whether to use a subdirectory based on the file name for intermediate files. + /// + public bool SuppressAddingValidationRows { private get; set; } + + public bool UseSubDirectory { private get; set; } + + public void Execute() + { + // Add the _Validation rows. + if (!this.SuppressAddingValidationRows) + { + Table validationTable = this.Output.EnsureTable(this.TableDefinitions["_Validation"]); + + foreach (Table table in this.Output.Tables) + { + if (!table.Definition.Unreal) + { + // Add the validation rows for this table. + table.Definition.AddValidationRows(validationTable); + } + } + } + + // Set the base directory. + string baseDirectory = this.TempFilesLocation; + + if (this.UseSubDirectory) + { + string filename = Path.GetFileNameWithoutExtension(this.OutputPath); + baseDirectory = Path.Combine(baseDirectory, filename); + + // make sure the directory exists + Directory.CreateDirectory(baseDirectory); + } + + try + { + OpenDatabase type = OpenDatabase.CreateDirect; + + // set special flag for patch files + if (OutputType.Patch == this.Output.Type) + { + type |= OpenDatabase.OpenPatchFile; + } + +#if DEBUG + Console.WriteLine("Opening database at: {0}", this.OutputPath); +#endif + + using (Database db = new Database(this.OutputPath, type)) + { + // Localize the codepage if a value was specified directly. + if (-1 != this.Codepage) + { + this.Output.Codepage = this.Codepage; + } + + // if we're not using the default codepage, import a new one into our + // database before we add any tables (or the tables would be added + // with the wrong codepage). + if (0 != this.Output.Codepage) + { + this.SetDatabaseCodepage(db, this.Output.Codepage); + } + + foreach (Table table in this.Output.Tables) + { + Table importTable = table; + bool hasBinaryColumn = false; + + // Skip all unreal tables other than _Streams. + if (table.Definition.Unreal && "_Streams" != table.Name) + { + continue; + } + + // Do not put the _Validation table in patches, it is not needed. + if (OutputType.Patch == this.Output.Type && "_Validation" == table.Name) + { + continue; + } + + // The only way to import binary data is to copy it to a local subdirectory first. + // To avoid this extra copying and perf hit, import an empty table with the same + // definition and later import the binary data from source using records. + foreach (ColumnDefinition columnDefinition in table.Definition.Columns) + { + if (ColumnType.Object == columnDefinition.Type) + { + importTable = new Table(table.Section, table.Definition); + hasBinaryColumn = true; + break; + } + } + + // Create the table via IDT import. + if ("_Streams" != importTable.Name) + { + try + { + db.ImportTable(this.Output.Codepage, importTable, baseDirectory, this.KeepAddedColumns); + } + catch (WixInvalidIdtException) + { + // If ValidateRows finds anything it doesn't like, it throws + importTable.ValidateRows(); + + // Otherwise we rethrow the InvalidIdt + throw; + } + } + + // insert the rows via SQL query if this table contains object fields + if (hasBinaryColumn) + { + StringBuilder query = new StringBuilder("SELECT "); + + // Build the query for the view. + bool firstColumn = true; + foreach (ColumnDefinition columnDefinition in table.Definition.Columns) + { + if (!firstColumn) + { + query.Append(","); + } + + query.AppendFormat(" `{0}`", columnDefinition.Name); + firstColumn = false; + } + query.AppendFormat(" FROM `{0}`", table.Name); + + using (View tableView = db.OpenExecuteView(query.ToString())) + { + // Import each row containing a stream + foreach (Row row in table.Rows) + { + using (Record record = new Record(table.Definition.Columns.Count)) + { + StringBuilder streamName = new StringBuilder(); + bool needStream = false; + + // the _Streams table doesn't prepend the table name (or a period) + if ("_Streams" != table.Name) + { + streamName.Append(table.Name); + } + + for (int i = 0; i < table.Definition.Columns.Count; i++) + { + ColumnDefinition columnDefinition = table.Definition.Columns[i]; + + switch (columnDefinition.Type) + { + case ColumnType.Localized: + case ColumnType.Preserved: + case ColumnType.String: + if (columnDefinition.PrimaryKey) + { + if (0 < streamName.Length) + { + streamName.Append("."); + } + streamName.Append((string)row[i]); + } + + record.SetString(i + 1, (string)row[i]); + break; + case ColumnType.Number: + record.SetInteger(i + 1, Convert.ToInt32(row[i], CultureInfo.InvariantCulture)); + break; + case ColumnType.Object: + if (null != row[i]) + { + needStream = true; + try + { + record.SetStream(i + 1, (string)row[i]); + } + catch (Win32Exception e) + { + if (0xA1 == e.NativeErrorCode) // ERROR_BAD_PATHNAME + { + throw new WixException(WixErrors.FileNotFound(row.SourceLineNumbers, (string)row[i])); + } + else + { + throw new WixException(WixErrors.Win32Exception(e.NativeErrorCode, e.Message)); + } + } + } + break; + } + } + + // stream names are created by concatenating the name of the table with the values + // of the primary key (delimited by periods) + // check for a stream name that is more than 62 characters long (the maximum allowed length) + if (needStream && MsiInterop.MsiMaxStreamNameLength < streamName.Length) + { + Messaging.Instance.OnMessage(WixErrors.StreamNameTooLong(row.SourceLineNumbers, table.Name, streamName.ToString(), streamName.Length)); + } + else // add the row to the database + { + tableView.Modify(ModifyView.Assign, record); + } + } + } + } + + // Remove rows from the _Streams table for wixpdbs. + if ("_Streams" == table.Name) + { + table.Rows.Clear(); + } + } + } + + // Insert substorages (usually transforms inside a patch or instance transforms in a package). + if (0 < this.Output.SubStorages.Count) + { + using (View storagesView = new View(db, "SELECT `Name`, `Data` FROM `_Storages`")) + { + foreach (SubStorage subStorage in this.Output.SubStorages) + { + string transformFile = Path.Combine(this.TempFilesLocation, String.Concat(subStorage.Name, ".mst")); + + // Bind the transform. + this.BindTransform(subStorage.Data, transformFile); + + if (Messaging.Instance.EncounteredError) + { + continue; + } + + // add the storage + using (Record record = new Record(2)) + { + record.SetString(1, subStorage.Name); + record.SetStream(2, transformFile); + storagesView.Modify(ModifyView.Assign, record); + } + } + } + } + + // We're good, commit the changes to the new database. + db.Commit(); + } + } + catch (IOException) + { + // TODO: this error message doesn't seem specific enough + throw new WixFileNotFoundException(new SourceLineNumber(this.OutputPath), this.OutputPath); + } + } + + private void BindTransform(Output transform, string outputPath) + { + BindTransformCommand command = new BindTransformCommand(); + command.Extensions = this.Extensions; + command.FileManagers = this.FileManagers; + command.TempFilesLocation = this.TempFilesLocation; + command.Transform = transform; + command.OutputPath = outputPath; + command.TableDefinitions = this.TableDefinitions; + command.Execute(); + } + + /// + /// Sets the codepage of a database. + /// + /// Database to set codepage into. + /// Output with the codepage for the database. + private void SetDatabaseCodepage(Database db, int codepage) + { + // write out the _ForceCodepage IDT file + string idtPath = Path.Combine(this.TempFilesLocation, "_ForceCodepage.idt"); + using (StreamWriter idtFile = new StreamWriter(idtPath, false, Encoding.ASCII)) + { + idtFile.WriteLine(); // dummy column name record + idtFile.WriteLine(); // dummy column definition record + idtFile.Write(codepage); + idtFile.WriteLine("\t_ForceCodepage"); + } + + // try to import the table into the MSI + try + { + db.Import(idtPath); + } + catch (WixInvalidIdtException) + { + // the IDT should be valid, so an invalid code page was given + throw new WixException(WixErrors.IllegalCodepage(codepage)); + } + } + } +} diff --git a/src/WixToolset.Core/Bind/ResolveDelayedFieldsCommand.cs b/src/WixToolset.Core/Bind/ResolveDelayedFieldsCommand.cs new file mode 100644 index 00000000..4ffe9e82 --- /dev/null +++ b/src/WixToolset.Core/Bind/ResolveDelayedFieldsCommand.cs @@ -0,0 +1,121 @@ +// 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. + +namespace WixToolset.Bind +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Text; + using WixToolset.Data; + + /// + /// Resolves the fields which had variables that needed to be resolved after the file information + /// was loaded. + /// + internal class ResolveDelayedFieldsCommand : ICommand + { + public OutputType OutputType { private get; set;} + + public IEnumerable DelayedFields { private get; set;} + + public IDictionary VariableCache { private get; set; } + + public string ModularizationGuid { private get; set; } + + /// Internal representation of the msi database to operate upon. + /// The fields which had resolution delayed. + /// The file information to use when resolving variables. + /// The modularization guid (used in case of a merge module). + public void Execute() + { + List deferredFields = new List(); + + foreach (DelayedField delayedField in this.DelayedFields) + { + try + { + Row propertyRow = delayedField.Row; + + // process properties first in case they refer to other binder variables + if ("Property" == propertyRow.Table.Name) + { + string value = WixVariableResolver.ResolveDelayedVariables(propertyRow.SourceLineNumbers, (string)delayedField.Field.Data, this.VariableCache); + + // update the variable cache with the new value + string key = String.Concat("property.", BindDatabaseCommand.Demodularize(this.OutputType, this.ModularizationGuid, (string)propertyRow[0])); + this.VariableCache[key] = value; + + // update the field data + delayedField.Field.Data = value; + } + else + { + deferredFields.Add(delayedField); + } + } + catch (WixException we) + { + Messaging.Instance.OnMessage(we.Error); + continue; + } + } + + // add specialization for ProductVersion fields + string keyProductVersion = "property.ProductVersion"; + if (this.VariableCache.ContainsKey(keyProductVersion)) + { + string value = this.VariableCache[keyProductVersion]; + Version productVersion = null; + + try + { + productVersion = new Version(value); + + // Don't add the variable if it already exists (developer defined a property with the same name). + string fieldKey = String.Concat(keyProductVersion, ".Major"); + if (!this.VariableCache.ContainsKey(fieldKey)) + { + this.VariableCache[fieldKey] = productVersion.Major.ToString(CultureInfo.InvariantCulture); + } + + fieldKey = String.Concat(keyProductVersion, ".Minor"); + if (!this.VariableCache.ContainsKey(fieldKey)) + { + this.VariableCache[fieldKey] = productVersion.Minor.ToString(CultureInfo.InvariantCulture); + } + + fieldKey = String.Concat(keyProductVersion, ".Build"); + if (!this.VariableCache.ContainsKey(fieldKey)) + { + this.VariableCache[fieldKey] = productVersion.Build.ToString(CultureInfo.InvariantCulture); + } + + fieldKey = String.Concat(keyProductVersion, ".Revision"); + if (!this.VariableCache.ContainsKey(fieldKey)) + { + this.VariableCache[fieldKey] = productVersion.Revision.ToString(CultureInfo.InvariantCulture); + } + } + catch + { + // Ignore the error introduced by new behavior. + } + } + + // process the remaining fields in case they refer to property binder variables + foreach (DelayedField delayedField in deferredFields) + { + try + { + delayedField.Field.Data = WixVariableResolver.ResolveDelayedVariables(delayedField.Row.SourceLineNumbers, (string)delayedField.Field.Data, this.VariableCache); + } + catch (WixException we) + { + Messaging.Instance.OnMessage(we.Error); + continue; + } + } + } + } +} diff --git a/src/WixToolset.Core/Bind/ResolveFieldsCommand.cs b/src/WixToolset.Core/Bind/ResolveFieldsCommand.cs new file mode 100644 index 00000000..4caec9b4 --- /dev/null +++ b/src/WixToolset.Core/Bind/ResolveFieldsCommand.cs @@ -0,0 +1,215 @@ +// 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. + +namespace WixToolset.Bind +{ + using System.Collections.Generic; + using WixToolset.Data; + using WixToolset.Extensibility; + + /// + /// Resolve source fields in the tables included in the output + /// + internal class ResolveFieldsCommand : ICommand + { + public TableIndexedCollection Tables { private get; set; } + + public ExtractEmbeddedFiles FilesWithEmbeddedFiles { private get; set; } + + public BinderFileManagerCore FileManagerCore { private get; set; } + + public IEnumerable FileManagers { private get; set; } + + public bool SupportDelayedResolution { private get; set; } + + public string TempFilesLocation { private get; set; } + + public WixVariableResolver WixVariableResolver { private get; set; } + + public IEnumerable DelayedFields { get; private set; } + + public void Execute() + { + List delayedFields = this.SupportDelayedResolution ? new List() : null; + + foreach (Table table in this.Tables) + { + foreach (Row row in table.Rows) + { + foreach (Field field in row.Fields) + { + bool isDefault = true; + bool delayedResolve = false; + + // Check to make sure we're in a scenario where we can handle variable resolution. + if (null != delayedFields) + { + // resolve localization and wix variables + if (field.Data is string) + { + field.Data = this.WixVariableResolver.ResolveVariables(row.SourceLineNumbers, field.AsString(), false, ref isDefault, ref delayedResolve); + if (delayedResolve) + { + delayedFields.Add(new DelayedField(row, field)); + } + } + } + + // Move to next row if we've hit an error resolving variables. + if (Messaging.Instance.EncounteredError) // TODO: make this error handling more specific to just the failure to resolve variables in this field. + { + continue; + } + + // Resolve file paths + if (ColumnType.Object == field.Column.Type) + { + ObjectField objectField = (ObjectField)field; + + // Skip file resolution if the file is to be deleted. + if (RowOperation.Delete == row.Operation) + { + continue; + } + + // File is embedded and path to it was not modified above. + if (objectField.EmbeddedFileIndex.HasValue && isDefault) + { + string extractPath = this.FilesWithEmbeddedFiles.AddEmbeddedFileIndex(objectField.BaseUri, objectField.EmbeddedFileIndex.Value, this.TempFilesLocation); + + // Set the path to the embedded file once where it will be extracted. + objectField.Data = extractPath; + } + else if (null != objectField.Data) // non-compressed file (or localized value) + { + try + { + if (OutputType.Patch != this.FileManagerCore.Output.Type) // Normal binding for non-Patch scenario such as link (light.exe) + { + // keep a copy of the un-resolved data for future replay. This will be saved into wixpdb file + if (null == objectField.UnresolvedData) + { + objectField.UnresolvedData = (string)objectField.Data; + } + + // resolve the path to the file + objectField.Data = this.ResolveFile((string)objectField.Data, table.Name, row.SourceLineNumbers, BindStage.Normal); + } + else if (!(this.FileManagerCore.RebaseTarget || this.FileManagerCore.RebaseUpdated)) // Normal binding for Patch Scenario (normal patch, no re-basing logic) + { + // resolve the path to the file + objectField.Data = this.ResolveFile((string)objectField.Data, table.Name, row.SourceLineNumbers, BindStage.Normal); + } + else // Re-base binding path scenario caused by pyro.exe -bt -bu + { + // by default, use the resolved Data for file lookup + string filePathToResolve = (string)objectField.Data; + + // if -bu is used in pyro command, this condition holds true and the tool + // will use pre-resolved source for new wixpdb file + if (this.FileManagerCore.RebaseUpdated) + { + // try to use the unResolved Source if it exists. + // New version of wixpdb file keeps a copy of pre-resolved Source. i.e. !(bindpath.test)\foo.dll + // Old version of winpdb file does not contain this attribute and the value is null. + if (null != objectField.UnresolvedData) + { + filePathToResolve = objectField.UnresolvedData; + } + } + + objectField.Data = this.ResolveFile(filePathToResolve, table.Name, row.SourceLineNumbers, BindStage.Updated); + } + } + catch (WixFileNotFoundException) + { + // display the error with source line information + Messaging.Instance.OnMessage(WixErrors.FileNotFound(row.SourceLineNumbers, (string)objectField.Data)); + } + } + + isDefault = true; + if (null != objectField.PreviousData) + { + objectField.PreviousData = this.WixVariableResolver.ResolveVariables(row.SourceLineNumbers, objectField.PreviousData, false, ref isDefault); + if (!Messaging.Instance.EncounteredError) // TODO: make this error handling more specific to just the failure to resolve variables in this field. + { + // file is compressed in a cabinet (and not modified above) + if (objectField.PreviousEmbeddedFileIndex.HasValue && isDefault) + { + // when loading transforms from disk, PreviousBaseUri may not have been set + if (null == objectField.PreviousBaseUri) + { + objectField.PreviousBaseUri = objectField.BaseUri; + } + + string extractPath = this.FilesWithEmbeddedFiles.AddEmbeddedFileIndex(objectField.PreviousBaseUri, objectField.PreviousEmbeddedFileIndex.Value, this.TempFilesLocation); + + // set the path to the file once its extracted from the cabinet + objectField.PreviousData = extractPath; + } + else if (null != objectField.PreviousData) // non-compressed file (or localized value) + { + try + { + if (!this.FileManagerCore.RebaseTarget && !this.FileManagerCore.RebaseUpdated) + { + // resolve the path to the file + objectField.PreviousData = this.ResolveFile((string)objectField.PreviousData, table.Name, row.SourceLineNumbers, BindStage.Normal); + } + else + { + if (this.FileManagerCore.RebaseTarget) + { + // if -bt is used, it come here + // Try to use the original unresolved source from either target build or update build + // If both target and updated are of old wixpdb, it behaves the same as today, no re-base logic here + // If target is old version and updated is new version, it uses unresolved path from updated build + // If both target and updated are of new versions, it uses unresolved path from target build + if (null != objectField.UnresolvedPreviousData || null != objectField.UnresolvedData) + { + objectField.PreviousData = objectField.UnresolvedPreviousData ?? objectField.UnresolvedData; + } + } + + // resolve the path to the file + objectField.PreviousData = this.ResolveFile((string)objectField.PreviousData, table.Name, row.SourceLineNumbers, BindStage.Target); + + } + } + catch (WixFileNotFoundException) + { + // display the error with source line information + Messaging.Instance.OnMessage(WixErrors.FileNotFound(row.SourceLineNumbers, (string)objectField.PreviousData)); + } + } + } + } + } + } + } + } + + this.DelayedFields = delayedFields; + } + + private string ResolveFile(string source, string type, SourceLineNumber sourceLineNumbers, BindStage bindStage = BindStage.Normal) + { + string path = null; + foreach (IBinderFileManager fileManager in this.FileManagers) + { + path = fileManager.ResolveFile(source, type, sourceLineNumbers, bindStage); + if (null != path) + { + break; + } + } + + if (null == path) + { + throw new WixFileNotFoundException(sourceLineNumbers, source, type); + } + + return path; + } + } +} diff --git a/src/WixToolset.Core/Bind/ResolvedDirectory.cs b/src/WixToolset.Core/Bind/ResolvedDirectory.cs new file mode 100644 index 00000000..6985f95d --- /dev/null +++ b/src/WixToolset.Core/Bind/ResolvedDirectory.cs @@ -0,0 +1,31 @@ +// 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. + +namespace WixToolset.Bind +{ + /// + /// Structure used for resolved directory information. + /// + internal struct ResolvedDirectory + { + /// The directory parent. + public string DirectoryParent; + + /// The name of this directory. + public string Name; + + /// The path of this directory. + public string Path; + + /// + /// Constructor for ResolvedDirectory. + /// + /// Parent directory. + /// The directory name. + public ResolvedDirectory(string directoryParent, string name) + { + this.DirectoryParent = directoryParent; + this.Name = name; + this.Path = null; + } + } +} diff --git a/src/WixToolset.Core/Bind/TransferFilesCommand.cs b/src/WixToolset.Core/Bind/TransferFilesCommand.cs new file mode 100644 index 00000000..719b8b20 --- /dev/null +++ b/src/WixToolset.Core/Bind/TransferFilesCommand.cs @@ -0,0 +1,214 @@ +// 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. + +namespace WixToolset.Bind +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Security.AccessControl; + using WixToolset.Data; + using WixToolset.Extensibility; + + internal class TransferFilesCommand : ICommand + { + public IEnumerable FileManagers { private get; set; } + + public IEnumerable FileTransfers { private get; set; } + + public bool SuppressAclReset { private get; set; } + + public void Execute() + { + List destinationFiles = new List(); + + foreach (FileTransfer fileTransfer in this.FileTransfers) + { + string fileSource = this.ResolveFile(fileTransfer.Source, fileTransfer.Type, fileTransfer.SourceLineNumbers, BindStage.Normal); + + // If the source and destination are identical, then there's nothing to do here + if (0 == String.Compare(fileSource, fileTransfer.Destination, StringComparison.OrdinalIgnoreCase)) + { + fileTransfer.Redundant = true; + continue; + } + + bool retry = false; + do + { + try + { + if (fileTransfer.Move) + { + Messaging.Instance.OnMessage(WixVerboses.MoveFile(fileSource, fileTransfer.Destination)); + this.TransferFile(true, fileSource, fileTransfer.Destination); + } + else + { + Messaging.Instance.OnMessage(WixVerboses.CopyFile(fileSource, fileTransfer.Destination)); + this.TransferFile(false, fileSource, fileTransfer.Destination); + } + + retry = false; + destinationFiles.Add(fileTransfer.Destination); + } + catch (FileNotFoundException e) + { + throw new WixFileNotFoundException(e.FileName); + } + catch (DirectoryNotFoundException) + { + // if we already retried, give up + if (retry) + { + throw; + } + + string directory = Path.GetDirectoryName(fileTransfer.Destination); + Messaging.Instance.OnMessage(WixVerboses.CreateDirectory(directory)); + Directory.CreateDirectory(directory); + retry = true; + } + catch (UnauthorizedAccessException) + { + // if we already retried, give up + if (retry) + { + throw; + } + + if (File.Exists(fileTransfer.Destination)) + { + Messaging.Instance.OnMessage(WixVerboses.RemoveDestinationFile(fileTransfer.Destination)); + + // try to ensure the file is not read-only + FileAttributes attributes = File.GetAttributes(fileTransfer.Destination); + try + { + File.SetAttributes(fileTransfer.Destination, attributes & ~FileAttributes.ReadOnly); + } + catch (ArgumentException) // thrown for unauthorized access errors + { + throw new WixException(WixErrors.UnauthorizedAccess(fileTransfer.Destination)); + } + + // try to delete the file + try + { + File.Delete(fileTransfer.Destination); + } + catch (IOException) + { + throw new WixException(WixErrors.FileInUse(null, fileTransfer.Destination)); + } + + retry = true; + } + else // no idea what just happened, bail + { + throw; + } + } + catch (IOException) + { + // if we already retried, give up + if (retry) + { + throw; + } + + if (File.Exists(fileTransfer.Destination)) + { + Messaging.Instance.OnMessage(WixVerboses.RemoveDestinationFile(fileTransfer.Destination)); + + // ensure the file is not read-only, then delete it + FileAttributes attributes = File.GetAttributes(fileTransfer.Destination); + File.SetAttributes(fileTransfer.Destination, attributes & ~FileAttributes.ReadOnly); + try + { + File.Delete(fileTransfer.Destination); + } + catch (IOException) + { + throw new WixException(WixErrors.FileInUse(null, fileTransfer.Destination)); + } + + retry = true; + } + else // no idea what just happened, bail + { + throw; + } + } + } while (retry); + } + + // Finally, if there were any files remove the ACL that may have been added to + // during the file transfer process. + if (0 < destinationFiles.Count && !this.SuppressAclReset) + { + var aclReset = new FileSecurity(); + aclReset.SetAccessRuleProtection(false, false); + + try + { + //WixToolset.Core.Native.NativeMethods.ResetAcls(destinationFiles.ToArray(), (uint)destinationFiles.Count); + + foreach (var file in destinationFiles) + { + new FileInfo(file).SetAccessControl(aclReset); + } + } + catch + { + Messaging.Instance.OnMessage(WixWarnings.UnableToResetAcls()); + } + } + } + + private string ResolveFile(string source, string type, SourceLineNumber sourceLineNumbers, BindStage bindStage) + { + string path = null; + foreach (IBinderFileManager fileManager in this.FileManagers) + { + path = fileManager.ResolveFile(source, type, sourceLineNumbers, bindStage); + if (null != path) + { + break; + } + } + + if (null == path) + { + throw new WixFileNotFoundException(sourceLineNumbers, source, type); + } + + return path; + } + + private void TransferFile(bool move, string source, string destination) + { + bool complete = false; + foreach (IBinderFileManager fileManager in this.FileManagers) + { + if (move) + { + complete = fileManager.MoveFile(source, destination, true); + } + else + { + complete = fileManager.CopyFile(source, destination, true); + } + + if (complete) + { + break; + } + } + + if (!complete) + { + throw new InvalidOperationException(); // TODO: something needs to be said here that none of the binder file managers returned a result. + } + } + } +} -- cgit v1.2.3-55-g6feb