From dbde9e7104b907bbbaea17e21247d8cafc8b3a4c Mon Sep 17 00:00:00 2001 From: Rob Mensching Date: Sat, 14 Oct 2017 16:12:07 -0700 Subject: Massive refactoring to introduce the concept of IBackend --- src/WixToolset.Core.Burn/BackendFactory.cs | 30 + src/WixToolset.Core.Burn/Bind/BindBundleCommand.cs | 942 +++++++++++++++++++++ .../Bind/ProvidesDependency.cs | 108 +++ .../Bind/ProvidesDependencyCollection.cs | 64 ++ .../Bind/WixComponentSearchInfo.cs | 64 ++ src/WixToolset.Core.Burn/Bind/WixFileSearchInfo.cs | 54 ++ .../Bind/WixProductSearchInfo.cs | 67 ++ .../Bind/WixRegistrySearchInfo.cs | 92 ++ src/WixToolset.Core.Burn/Bind/WixSearchInfo.cs | 53 ++ src/WixToolset.Core.Burn/BundleBackend.cs | 58 ++ .../AutomaticallySlipstreamPatchesCommand.cs | 112 +++ src/WixToolset.Core.Burn/Bundles/BurnCommon.cs | 378 +++++++++ src/WixToolset.Core.Burn/Bundles/BurnReader.cs | 220 +++++ src/WixToolset.Core.Burn/Bundles/BurnWriter.cs | 239 ++++++ ...CreateBootstrapperApplicationManifestCommand.cs | 241 ++++++ .../Bundles/CreateBurnManifestCommand.cs | 686 +++++++++++++++ .../Bundles/CreateContainerCommand.cs | 68 ++ .../Bundles/GetPackageFacadesCommand.cs | 62 ++ .../OrderPackagesAndRollbackBoundariesCommand.cs | 145 ++++ src/WixToolset.Core.Burn/Bundles/PackageFacade.cs | 58 ++ .../Bundles/ProcessExePackageCommand.cs | 33 + .../Bundles/ProcessMsiPackageCommand.cs | 576 +++++++++++++ .../Bundles/ProcessMspPackageCommand.cs | 189 +++++ .../Bundles/ProcessMsuPackageCommand.cs | 30 + .../Bundles/ProcessPayloadsCommand.cs | 161 ++++ .../Bundles/VerifyPayloadsWithCatalogCommand.cs | 148 ++++ .../Inscribe/InscribeBundleCommand.cs | 53 ++ .../Inscribe/InscribeBundleEngineCommand.cs | 61 ++ src/WixToolset.Core.Burn/VerifyInterop.cs | 68 ++ .../WixToolset.Core.Burn.csproj | 36 + 30 files changed, 5096 insertions(+) create mode 100644 src/WixToolset.Core.Burn/BackendFactory.cs create mode 100644 src/WixToolset.Core.Burn/Bind/BindBundleCommand.cs create mode 100644 src/WixToolset.Core.Burn/Bind/ProvidesDependency.cs create mode 100644 src/WixToolset.Core.Burn/Bind/ProvidesDependencyCollection.cs create mode 100644 src/WixToolset.Core.Burn/Bind/WixComponentSearchInfo.cs create mode 100644 src/WixToolset.Core.Burn/Bind/WixFileSearchInfo.cs create mode 100644 src/WixToolset.Core.Burn/Bind/WixProductSearchInfo.cs create mode 100644 src/WixToolset.Core.Burn/Bind/WixRegistrySearchInfo.cs create mode 100644 src/WixToolset.Core.Burn/Bind/WixSearchInfo.cs create mode 100644 src/WixToolset.Core.Burn/BundleBackend.cs create mode 100644 src/WixToolset.Core.Burn/Bundles/AutomaticallySlipstreamPatchesCommand.cs create mode 100644 src/WixToolset.Core.Burn/Bundles/BurnCommon.cs create mode 100644 src/WixToolset.Core.Burn/Bundles/BurnReader.cs create mode 100644 src/WixToolset.Core.Burn/Bundles/BurnWriter.cs create mode 100644 src/WixToolset.Core.Burn/Bundles/CreateBootstrapperApplicationManifestCommand.cs create mode 100644 src/WixToolset.Core.Burn/Bundles/CreateBurnManifestCommand.cs create mode 100644 src/WixToolset.Core.Burn/Bundles/CreateContainerCommand.cs create mode 100644 src/WixToolset.Core.Burn/Bundles/GetPackageFacadesCommand.cs create mode 100644 src/WixToolset.Core.Burn/Bundles/OrderPackagesAndRollbackBoundariesCommand.cs create mode 100644 src/WixToolset.Core.Burn/Bundles/PackageFacade.cs create mode 100644 src/WixToolset.Core.Burn/Bundles/ProcessExePackageCommand.cs create mode 100644 src/WixToolset.Core.Burn/Bundles/ProcessMsiPackageCommand.cs create mode 100644 src/WixToolset.Core.Burn/Bundles/ProcessMspPackageCommand.cs create mode 100644 src/WixToolset.Core.Burn/Bundles/ProcessMsuPackageCommand.cs create mode 100644 src/WixToolset.Core.Burn/Bundles/ProcessPayloadsCommand.cs create mode 100644 src/WixToolset.Core.Burn/Bundles/VerifyPayloadsWithCatalogCommand.cs create mode 100644 src/WixToolset.Core.Burn/Inscribe/InscribeBundleCommand.cs create mode 100644 src/WixToolset.Core.Burn/Inscribe/InscribeBundleEngineCommand.cs create mode 100644 src/WixToolset.Core.Burn/VerifyInterop.cs create mode 100644 src/WixToolset.Core.Burn/WixToolset.Core.Burn.csproj (limited to 'src/WixToolset.Core.Burn') diff --git a/src/WixToolset.Core.Burn/BackendFactory.cs b/src/WixToolset.Core.Burn/BackendFactory.cs new file mode 100644 index 00000000..042fa254 --- /dev/null +++ b/src/WixToolset.Core.Burn/BackendFactory.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.Core.Burn +{ + using System; + using System.IO; + using WixToolset.Extensibility; + + internal class BackendFactory : IBackendFactory + { + public bool TryCreateBackend(string outputType, string outputFile, IBindContext context, out IBackend backend) + { + if (String.IsNullOrEmpty(outputType)) + { + outputType = Path.GetExtension(outputFile); + } + + switch (outputType.ToLowerInvariant()) + { + case "bundle": + case ".exe": + backend = new BundleBackend(); + return true; + } + + backend = null; + return false; + } + } +} diff --git a/src/WixToolset.Core.Burn/Bind/BindBundleCommand.cs b/src/WixToolset.Core.Burn/Bind/BindBundleCommand.cs new file mode 100644 index 00000000..212b1e81 --- /dev/null +++ b/src/WixToolset.Core.Burn/Bind/BindBundleCommand.cs @@ -0,0 +1,942 @@ +// 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.Core.Burn +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Reflection; + using WixToolset.Bind; + using WixToolset.Core.Bind; + using WixToolset.Core.Burn.Bundles; + using WixToolset.Data; + using WixToolset.Data.Bind; + using WixToolset.Data.Rows; + using WixToolset.Extensibility; + + // TODO: (4.0) Refactor so that these don't need to be copied. + // Copied verbatim from ext\UtilExtension\wixext\UtilCompiler.cs + [Flags] + internal enum WixFileSearchAttributes + { + Default = 0x001, + MinVersionInclusive = 0x002, + MaxVersionInclusive = 0x004, + MinSizeInclusive = 0x008, + MaxSizeInclusive = 0x010, + MinDateInclusive = 0x020, + MaxDateInclusive = 0x040, + WantVersion = 0x080, + WantExists = 0x100, + IsDirectory = 0x200, + } + + [Flags] + internal enum WixRegistrySearchAttributes + { + Raw = 0x01, + Compatible = 0x02, + ExpandEnvironmentVariables = 0x04, + WantValue = 0x08, + WantExists = 0x10, + Win64 = 0x20, + } + + internal enum WixComponentSearchAttributes + { + KeyPath = 0x1, + State = 0x2, + WantDirectory = 0x4, + } + + [Flags] + internal enum WixProductSearchAttributes + { + Version = 0x1, + Language = 0x2, + State = 0x4, + Assignment = 0x8, + UpgradeCode = 0x10, + } + + /// + /// Binds a this.bundle. + /// + internal class BindBundleCommand + { + public BindBundleCommand(IBindContext context) + { + this.TableDefinitions = WindowsInstallerStandard.GetTableDefinitions(); + + this.DelayedFields = context.DelayedFields; + this.ExpectedEmbeddedFiles = context.ExpectedEmbeddedFiles; + + this.BackendExtensions = context.ExtensionManager.Create(); + } + + public CompressionLevel DefaultCompressionLevel { private get; set; } + + public IEnumerable DelayedFields { get; } + + public IEnumerable ExpectedEmbeddedFiles { get; } + + private IEnumerable BackendExtensions { get; } + + public IEnumerable Extensions { 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 IntermediateFolder { private get; set; } + + public IBindVariableResolver 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; + } + + // If there are any fields to resolve later, create the cache to populate during bind. + IDictionary variableCache = null; + if (this.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). + { + var extractEmbeddedFilesCommand = new ExtractEmbeddedFilesCommand(); + extractEmbeddedFilesCommand.FilesWithEmbeddedFiles = ExpectedEmbeddedFiles; + 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: + { + var command = new ProcessMsiPackageCommand(); + command.AuthoredPayloads = payloads; + command.Facade = facade; + command.BackendExtensions = this.BackendExtensions; + 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 (this.DelayedFields.Any()) + { + var resolveDelayedFieldsCommand = new ResolveDelayedFieldsCommand(); + resolveDelayedFieldsCommand.OutputType = this.Output.Type; + resolveDelayedFieldsCommand.DelayedFields = this.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.IntermediateFolder; + command.Execute(); + + WixBundlePayloadRow baManifestPayload = command.BootstrapperApplicationManifestPayloadRow; + payloads.Add(baManifestPayload); + } + + //foreach (BinderExtension extension in this.Extensions) + //{ + // extension.PostBind(this.Context); + //} + + // 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.IntermediateFolder, 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.IntermediateFolder, 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.IntermediateFolder, "bundle-manifest.xml"); + { + var command = new CreateBurnManifestCommand(); + command.BackendExtensions = this.BackendExtensions; + 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.IntermediateFolder, 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.Burn/Bind/ProvidesDependency.cs b/src/WixToolset.Core.Burn/Bind/ProvidesDependency.cs new file mode 100644 index 00000000..e64773b4 --- /dev/null +++ b/src/WixToolset.Core.Burn/Bind/ProvidesDependency.cs @@ -0,0 +1,108 @@ +// 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.Core.Burn +{ + using System; + using System.Xml; + using WixToolset.Data; + + /// + /// Represents an authored or imported dependency provider. + /// + internal sealed class ProvidesDependency + { + /// + /// Creates a new instance of the class from a . + /// + /// The from which data is imported. + internal ProvidesDependency(Row row) + : this((string)row[2], (string)row[3], (string)row[4], (int?)row[5]) + { + } + + /// + /// Creates a new instance of the class. + /// + /// The unique key of the dependency. + /// Additional attributes for the dependency. + internal ProvidesDependency(string key, string version, string displayName, int? attributes) + { + this.Key = key; + this.Version = version; + this.DisplayName = displayName; + this.Attributes = attributes; + } + + /// + /// Gets or sets the unique key of the package provider. + /// + internal string Key { get; set; } + + /// + /// Gets or sets the version of the package provider. + /// + internal string Version { get; set; } + + /// + /// Gets or sets the display name of the package provider. + /// + internal string DisplayName { get; set; } + + /// + /// Gets or sets the attributes for the dependency. + /// + internal int? Attributes { get; set; } + + /// + /// Gets or sets whether the dependency was imported from the package. + /// + internal bool Imported { get; set; } + + /// + /// Gets whether certain properties are the same. + /// + /// Another to compare. + /// This is not the same as object equality, but only checks a subset of properties + /// to determine if the objects are similar and could be merged into a collection. + /// True if certain properties are the same. + internal bool Equals(ProvidesDependency other) + { + if (null != other) + { + return this.Key == other.Key && + this.Version == other.Version && + this.DisplayName == other.DisplayName; + } + + return false; + } + + /// + /// Writes the dependency to the bundle XML manifest. + /// + /// The for the bundle XML manifest. + internal void WriteXml(XmlTextWriter writer) + { + writer.WriteStartElement("Provides"); + writer.WriteAttributeString("Key", this.Key); + + if (!String.IsNullOrEmpty(this.Version)) + { + writer.WriteAttributeString("Version", this.Version); + } + + if (!String.IsNullOrEmpty(this.DisplayName)) + { + writer.WriteAttributeString("DisplayName", this.DisplayName); + } + + if (this.Imported) + { + // The package dependency was explicitly authored into the manifest. + writer.WriteAttributeString("Imported", "yes"); + } + + writer.WriteEndElement(); + } + } +} diff --git a/src/WixToolset.Core.Burn/Bind/ProvidesDependencyCollection.cs b/src/WixToolset.Core.Burn/Bind/ProvidesDependencyCollection.cs new file mode 100644 index 00000000..668b81d3 --- /dev/null +++ b/src/WixToolset.Core.Burn/Bind/ProvidesDependencyCollection.cs @@ -0,0 +1,64 @@ +// 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.Core.Burn +{ + using System; + using System.Collections.ObjectModel; + + /// + /// A case-insensitive collection of unique objects. + /// + internal sealed class ProvidesDependencyCollection : KeyedCollection + { + /// + /// Creates a case-insensitive collection of unique objects. + /// + internal ProvidesDependencyCollection() + : base(StringComparer.InvariantCultureIgnoreCase) + { + } + + /// + /// Adds the to the collection if it doesn't already exist. + /// + /// The to add to the collection. + /// True if the was added to the collection; otherwise, false. + /// The parameter is null. + internal bool Merge(ProvidesDependency dependency) + { + if (null == dependency) + { + throw new ArgumentNullException("dependency"); + } + + // If the dependency key is already in the collection, verify equality for a subset of properties. + if (this.Contains(dependency.Key)) + { + ProvidesDependency current = this[dependency.Key]; + if (!current.Equals(dependency)) + { + return false; + } + } + + base.Add(dependency); + return true; + } + + /// + /// Gets the for the . + /// + /// The dependency to index. + /// The parameter is null. + /// The for the . + protected override string GetKeyForItem(ProvidesDependency dependency) + { + if (null == dependency) + { + throw new ArgumentNullException("dependency"); + } + + return dependency.Key; + } + } +} diff --git a/src/WixToolset.Core.Burn/Bind/WixComponentSearchInfo.cs b/src/WixToolset.Core.Burn/Bind/WixComponentSearchInfo.cs new file mode 100644 index 00000000..f605d7c7 --- /dev/null +++ b/src/WixToolset.Core.Burn/Bind/WixComponentSearchInfo.cs @@ -0,0 +1,64 @@ +// 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.Core.Burn +{ + using System; + using System.Xml; + using WixToolset.Data; + + /// + /// Utility class for all WixComponentSearches. + /// + internal class WixComponentSearchInfo : WixSearchInfo + { + public WixComponentSearchInfo(Row row) + : this((string)row[0], (string)row[1], (string)row[2], (int)row[3]) + { + } + + public WixComponentSearchInfo(string id, string guid, string productCode, int attributes) + : base(id) + { + this.Guid = guid; + this.ProductCode = productCode; + this.Attributes = (WixComponentSearchAttributes)attributes; + } + + public string Guid { get; private set; } + public string ProductCode { get; private set; } + public WixComponentSearchAttributes Attributes { get; private set; } + + /// + /// Generates Burn manifest and ParameterInfo-style markup for a component search. + /// + /// + public override void WriteXml(XmlTextWriter writer) + { + writer.WriteStartElement("MsiComponentSearch"); + this.WriteWixSearchAttributes(writer); + + writer.WriteAttributeString("ComponentId", this.Guid); + + if (!String.IsNullOrEmpty(this.ProductCode)) + { + writer.WriteAttributeString("ProductCode", this.ProductCode); + } + + if (0 != (this.Attributes & WixComponentSearchAttributes.KeyPath)) + { + writer.WriteAttributeString("Type", "keyPath"); + } + else if (0 != (this.Attributes & WixComponentSearchAttributes.State)) + { + writer.WriteAttributeString("Type", "state"); + } + else if (0 != (this.Attributes & WixComponentSearchAttributes.WantDirectory)) + { + writer.WriteAttributeString("Type", "directory"); + } + + writer.WriteEndElement(); + } + } + +} diff --git a/src/WixToolset.Core.Burn/Bind/WixFileSearchInfo.cs b/src/WixToolset.Core.Burn/Bind/WixFileSearchInfo.cs new file mode 100644 index 00000000..ea955db4 --- /dev/null +++ b/src/WixToolset.Core.Burn/Bind/WixFileSearchInfo.cs @@ -0,0 +1,54 @@ +// 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.Core.Burn +{ + using System; + using System.Xml; + using WixToolset.Data; + + /// + /// Utility class for all WixFileSearches (file and directory searches). + /// + internal class WixFileSearchInfo : WixSearchInfo + { + public WixFileSearchInfo(Row row) + : this((string)row[0], (string)row[1], (int)row[9]) + { + } + + public WixFileSearchInfo(string id, string path, int attributes) + : base(id) + { + this.Path = path; + this.Attributes = (WixFileSearchAttributes)attributes; + } + + public string Path { get; private set; } + public WixFileSearchAttributes Attributes { get; private set; } + + /// + /// Generates Burn manifest and ParameterInfo-style markup for a file/directory search. + /// + /// + public override void WriteXml(XmlTextWriter writer) + { + writer.WriteStartElement((0 == (this.Attributes & WixFileSearchAttributes.IsDirectory)) ? "FileSearch" : "DirectorySearch"); + this.WriteWixSearchAttributes(writer); + writer.WriteAttributeString("Path", this.Path); + if (WixFileSearchAttributes.WantExists == (this.Attributes & WixFileSearchAttributes.WantExists)) + { + writer.WriteAttributeString("Type", "exists"); + } + else if (WixFileSearchAttributes.WantVersion == (this.Attributes & WixFileSearchAttributes.WantVersion)) + { + // Can never get here for DirectorySearch. + writer.WriteAttributeString("Type", "version"); + } + else + { + writer.WriteAttributeString("Type", "path"); + } + writer.WriteEndElement(); + } + } +} diff --git a/src/WixToolset.Core.Burn/Bind/WixProductSearchInfo.cs b/src/WixToolset.Core.Burn/Bind/WixProductSearchInfo.cs new file mode 100644 index 00000000..b3bf5fee --- /dev/null +++ b/src/WixToolset.Core.Burn/Bind/WixProductSearchInfo.cs @@ -0,0 +1,67 @@ +// 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.Core.Burn +{ + using System; + using System.Xml; + using WixToolset.Data; + + /// + /// Utility class for all WixProductSearches. + /// + internal class WixProductSearchInfo : WixSearchInfo + { + public WixProductSearchInfo(Row row) + : this((string)row[0], (string)row[1], (int)row[2]) + { + } + + public WixProductSearchInfo(string id, string guid, int attributes) + : base(id) + { + this.Guid = guid; + this.Attributes = (WixProductSearchAttributes)attributes; + } + + public string Guid { get; private set; } + public WixProductSearchAttributes Attributes { get; private set; } + + /// + /// Generates Burn manifest and ParameterInfo-style markup for a product search. + /// + /// + public override void WriteXml(XmlTextWriter writer) + { + writer.WriteStartElement("MsiProductSearch"); + this.WriteWixSearchAttributes(writer); + + if (0 != (this.Attributes & WixProductSearchAttributes.UpgradeCode)) + { + writer.WriteAttributeString("UpgradeCode", this.Guid); + } + else + { + writer.WriteAttributeString("ProductCode", this.Guid); + } + + if (0 != (this.Attributes & WixProductSearchAttributes.Version)) + { + writer.WriteAttributeString("Type", "version"); + } + else if (0 != (this.Attributes & WixProductSearchAttributes.Language)) + { + writer.WriteAttributeString("Type", "language"); + } + else if (0 != (this.Attributes & WixProductSearchAttributes.State)) + { + writer.WriteAttributeString("Type", "state"); + } + else if (0 != (this.Attributes & WixProductSearchAttributes.Assignment)) + { + writer.WriteAttributeString("Type", "assignment"); + } + + writer.WriteEndElement(); + } + } +} diff --git a/src/WixToolset.Core.Burn/Bind/WixRegistrySearchInfo.cs b/src/WixToolset.Core.Burn/Bind/WixRegistrySearchInfo.cs new file mode 100644 index 00000000..e25f25f4 --- /dev/null +++ b/src/WixToolset.Core.Burn/Bind/WixRegistrySearchInfo.cs @@ -0,0 +1,92 @@ +// 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.Core.Burn +{ + using System; + using System.Xml; + using WixToolset.Data; + + /// + /// Utility class for all WixRegistrySearches. + /// + internal class WixRegistrySearchInfo : WixSearchInfo + { + public WixRegistrySearchInfo(Row row) + : this((string)row[0], (int)row[1], (string)row[2], (string)row[3], (int)row[4]) + { + } + + public WixRegistrySearchInfo(string id, int root, string key, string value, int attributes) + : base(id) + { + this.Root = root; + this.Key = key; + this.Value = value; + this.Attributes = (WixRegistrySearchAttributes)attributes; + } + + public int Root { get; private set; } + public string Key { get; private set; } + public string Value { get; private set; } + public WixRegistrySearchAttributes Attributes { get; private set; } + + /// + /// Generates Burn manifest and ParameterInfo-style markup for a registry search. + /// + /// + public override void WriteXml(XmlTextWriter writer) + { + writer.WriteStartElement("RegistrySearch"); + this.WriteWixSearchAttributes(writer); + + switch (this.Root) + { + case Core.Native.MsiInterop.MsidbRegistryRootClassesRoot: + writer.WriteAttributeString("Root", "HKCR"); + break; + case Core.Native.MsiInterop.MsidbRegistryRootCurrentUser: + writer.WriteAttributeString("Root", "HKCU"); + break; + case Core.Native.MsiInterop.MsidbRegistryRootLocalMachine: + writer.WriteAttributeString("Root", "HKLM"); + break; + case Core.Native.MsiInterop.MsidbRegistryRootUsers: + writer.WriteAttributeString("Root", "HKU"); + break; + } + + writer.WriteAttributeString("Key", this.Key); + + if (!String.IsNullOrEmpty(this.Value)) + { + writer.WriteAttributeString("Value", this.Value); + } + + bool existenceOnly = 0 != (this.Attributes & WixRegistrySearchAttributes.WantExists); + + writer.WriteAttributeString("Type", existenceOnly ? "exists" : "value"); + + if (0 != (this.Attributes & WixRegistrySearchAttributes.Win64)) + { + writer.WriteAttributeString("Win64", "yes"); + } + + if (!existenceOnly) + { + if (0 != (this.Attributes & WixRegistrySearchAttributes.ExpandEnvironmentVariables)) + { + writer.WriteAttributeString("ExpandEnvironment", "yes"); + } + + // We *always* say this is VariableType="string". If we end up + // needing to be more specific, we will have to expand the "Format" + // attribute to allow "number" and "version". + + writer.WriteAttributeString("VariableType", "string"); + } + + writer.WriteEndElement(); + } + } + +} diff --git a/src/WixToolset.Core.Burn/Bind/WixSearchInfo.cs b/src/WixToolset.Core.Burn/Bind/WixSearchInfo.cs new file mode 100644 index 00000000..9ebca4ae --- /dev/null +++ b/src/WixToolset.Core.Burn/Bind/WixSearchInfo.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.Core.Burn +{ + using System; + using System.Diagnostics; + using System.Xml; + using WixToolset.Data; + + /// + /// Utility base class for all WixSearches. + /// + internal abstract class WixSearchInfo + { + public WixSearchInfo(string id) + { + this.Id = id; + } + + public void AddWixSearchRowInfo(Row row) + { + Debug.Assert((string)row[0] == Id); + Variable = (string)row[1]; + Condition = (string)row[2]; + } + + public string Id { get; private set; } + public string Variable { get; private set; } + public string Condition { get; private set; } + + /// + /// Generates Burn manifest and ParameterInfo-style markup a search. + /// + /// + public virtual void WriteXml(XmlTextWriter writer) + { + } + + /// + /// Writes attributes common to all WixSearch elements. + /// + /// + protected void WriteWixSearchAttributes(XmlTextWriter writer) + { + writer.WriteAttributeString("Id", this.Id); + writer.WriteAttributeString("Variable", this.Variable); + if (!String.IsNullOrEmpty(this.Condition)) + { + writer.WriteAttributeString("Condition", this.Condition); + } + } + } +} diff --git a/src/WixToolset.Core.Burn/BundleBackend.cs b/src/WixToolset.Core.Burn/BundleBackend.cs new file mode 100644 index 00000000..ef4d362c --- /dev/null +++ b/src/WixToolset.Core.Burn/BundleBackend.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.Core.Burn +{ + using System; + using System.IO; + using WixToolset.Core.Burn.Bundles; + using WixToolset.Core.Burn.Inscribe; + using WixToolset.Data; + using WixToolset.Data.Bind; + using WixToolset.Extensibility; + + internal class BundleBackend : IBackend + { + public BindResult Bind(IBindContext context) + { + BindBundleCommand command = new BindBundleCommand(context); + //command.DefaultCompressionLevel = context.DefaultCompressionLevel; + //command.Extensions = context.Extensions; + //command.IntermediateFolder = context.IntermediateFolder; + //command.Output = context.IntermediateRepresentation; + //command.OutputPath = context.OutputPath; + //command.PdbFile = context.OutputPdbPath; + //command.WixVariableResolver = context.WixVariableResolver; + command.Execute(); + + return new BindResult(command.FileTransfers, command.ContentFilePaths); + } + + public bool Inscribe(IInscribeContext context) + { + if (String.IsNullOrEmpty(context.SignedEngineFile)) + { + var command = new InscribeBundleCommand(context); + return command.Execute(); + } + else + { + var command = new InscribeBundleEngineCommand(context); + return command.Execute(); + } + } + + public Output Unbind(IUnbindContext context) + { + string uxExtractPath = Path.Combine(context.ExportBasePath, "UX"); + string acExtractPath = Path.Combine(context.ExportBasePath, "AttachedContainer"); + + using (BurnReader reader = BurnReader.Open(context.InputFilePath)) + { + reader.ExtractUXContainer(uxExtractPath, context.IntermediateFolder); + reader.ExtractAttachedContainer(acExtractPath, context.IntermediateFolder); + } + + return null; + } + } +} diff --git a/src/WixToolset.Core.Burn/Bundles/AutomaticallySlipstreamPatchesCommand.cs b/src/WixToolset.Core.Burn/Bundles/AutomaticallySlipstreamPatchesCommand.cs new file mode 100644 index 00000000..bac8633b --- /dev/null +++ b/src/WixToolset.Core.Burn/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.Core.Burn.Bundles +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using WixToolset.Data; + using WixToolset.Data.Rows; + + internal class AutomaticallySlipstreamPatchesCommand + { + 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.Burn/Bundles/BurnCommon.cs b/src/WixToolset.Core.Burn/Bundles/BurnCommon.cs new file mode 100644 index 00000000..0baa6094 --- /dev/null +++ b/src/WixToolset.Core.Burn/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.Core.Burn.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.Burn/Bundles/BurnReader.cs b/src/WixToolset.Core.Burn/Bundles/BurnReader.cs new file mode 100644 index 00000000..261ef7b4 --- /dev/null +++ b/src/WixToolset.Core.Burn/Bundles/BurnReader.cs @@ -0,0 +1,220 @@ +// 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.Core.Burn.Bundles +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.IO; + using System.Xml; + using WixToolset.Core.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; + } + } + + internal static BurnReader Open(object inputFilePath) + { + throw new NotImplementedException(); + } + + /// + /// 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 (var 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; + } + + internal void ExtractUXContainer(string uxExtractPath, object intermediateFolder) + { + throw new NotImplementedException(); + } + + /// + /// 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.Burn/Bundles/BurnWriter.cs b/src/WixToolset.Core.Burn/Bundles/BurnWriter.cs new file mode 100644 index 00000000..e7365212 --- /dev/null +++ b/src/WixToolset.Core.Burn/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.Core.Burn.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.Burn/Bundles/CreateBootstrapperApplicationManifestCommand.cs b/src/WixToolset.Core.Burn/Bundles/CreateBootstrapperApplicationManifestCommand.cs new file mode 100644 index 00000000..58814efc --- /dev/null +++ b/src/WixToolset.Core.Burn/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.Core.Burn.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 + { + 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.Burn/Bundles/CreateBurnManifestCommand.cs b/src/WixToolset.Core.Burn/Bundles/CreateBurnManifestCommand.cs new file mode 100644 index 00000000..772265a0 --- /dev/null +++ b/src/WixToolset.Core.Burn/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.Core.Burn.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 + { + public IEnumerable BackendExtensions { 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 (var extension in this.BackendExtensions) + { + resolved = extension.ResolveUrl(url, fallbackUrl, packageId, payloadId, fileName); + if (!String.IsNullOrEmpty(resolved)) + { + break; + } + } + + return resolved; + } + } +} diff --git a/src/WixToolset.Core.Burn/Bundles/CreateContainerCommand.cs b/src/WixToolset.Core.Burn/Bundles/CreateContainerCommand.cs new file mode 100644 index 00000000..75379713 --- /dev/null +++ b/src/WixToolset.Core.Burn/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.Core.Burn.Bundles +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + using System.Linq; + using WixToolset.Core.Cab; + using WixToolset.Data; + using WixToolset.Data.Rows; + + /// + /// Creates cabinet files. + /// + internal class CreateContainerCommand + { + 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 (var 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.Burn/Bundles/GetPackageFacadesCommand.cs b/src/WixToolset.Core.Burn/Bundles/GetPackageFacadesCommand.cs new file mode 100644 index 00000000..7485758c --- /dev/null +++ b/src/WixToolset.Core.Burn/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.Core.Burn.Bundles +{ + using System.Collections.Generic; + using WixToolset.Data; + using WixToolset.Data.Rows; + + internal class GetPackageFacadesCommand + { + 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.Burn/Bundles/OrderPackagesAndRollbackBoundariesCommand.cs b/src/WixToolset.Core.Burn/Bundles/OrderPackagesAndRollbackBoundariesCommand.cs new file mode 100644 index 00000000..cb6e2748 --- /dev/null +++ b/src/WixToolset.Core.Burn/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.Core.Burn.Bundles +{ + using System; + using System.Collections.Generic; + using WixToolset.Data; + using WixToolset.Data.Rows; + + internal class OrderPackagesAndRollbackBoundariesCommand + { + 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.Burn/Bundles/PackageFacade.cs b/src/WixToolset.Core.Burn/Bundles/PackageFacade.cs new file mode 100644 index 00000000..3f2e184d --- /dev/null +++ b/src/WixToolset.Core.Burn/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.Core.Burn.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.Burn/Bundles/ProcessExePackageCommand.cs b/src/WixToolset.Core.Burn/Bundles/ProcessExePackageCommand.cs new file mode 100644 index 00000000..11512c39 --- /dev/null +++ b/src/WixToolset.Core.Burn/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.Core.Burn.Bundles +{ + using System; + using WixToolset.Data; + using WixToolset.Data.Rows; + + /// + /// Initializes package state from the Exe contents. + /// + internal class ProcessExePackageCommand + { + 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.Burn/Bundles/ProcessMsiPackageCommand.cs b/src/WixToolset.Core.Burn/Bundles/ProcessMsiPackageCommand.cs new file mode 100644 index 00000000..322187f9 --- /dev/null +++ b/src/WixToolset.Core.Burn/Bundles/ProcessMsiPackageCommand.cs @@ -0,0 +1,576 @@ +// 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.Core.Burn.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.Core.Native; + using Dtf = WixToolset.Dtf.WindowsInstaller; + using WixToolset.Bind; + using WixToolset.Data.Bind; + + /// + /// Initializes package state from the MSI contents. + /// + internal class ProcessMsiPackageCommand + { + private const string PropertySqlFormat = "SELECT `Value` FROM `Property` WHERE `Property` = '{0}'"; + + public RowDictionary AuthoredPayloads { private get; set; } + + public PackageFacade Facade { private get; set; } + + public IEnumerable BackendExtensions { 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 = this.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 = Common.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 = this.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); + } + } + } + } + } + + private string ResolveRelatedFile(string sourceFile, string relatedSource, string type, SourceLineNumber sourceLineNumbers, BindStage stage) + { + foreach (var extension in this.BackendExtensions) + { + var relatedFile = extension.ResolveRelatedFile(sourceFile, relatedSource, type, sourceLineNumbers, stage); + + if (!String.IsNullOrEmpty(relatedFile)) + { + return relatedFile; + } + } + + return null; + } + + /// + /// 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.Burn/Bundles/ProcessMspPackageCommand.cs b/src/WixToolset.Core.Burn/Bundles/ProcessMspPackageCommand.cs new file mode 100644 index 00000000..2d849d03 --- /dev/null +++ b/src/WixToolset.Core.Burn/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.Core.Burn.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 + { + 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.Burn/Bundles/ProcessMsuPackageCommand.cs b/src/WixToolset.Core.Burn/Bundles/ProcessMsuPackageCommand.cs new file mode 100644 index 00000000..fcfc780c --- /dev/null +++ b/src/WixToolset.Core.Burn/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.Core.Burn.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 + { + 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.Burn/Bundles/ProcessPayloadsCommand.cs b/src/WixToolset.Core.Burn/Bundles/ProcessPayloadsCommand.cs new file mode 100644 index 00000000..5dbd6aaa --- /dev/null +++ b/src/WixToolset.Core.Burn/Bundles/ProcessPayloadsCommand.cs @@ -0,0 +1,161 @@ +// 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.Core.Burn.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.Bind; + using WixToolset.Data; + using WixToolset.Data.Bind; + using WixToolset.Data.Rows; + + internal class ProcessPayloadsCommand + { + 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.Burn/Bundles/VerifyPayloadsWithCatalogCommand.cs b/src/WixToolset.Core.Burn/Bundles/VerifyPayloadsWithCatalogCommand.cs new file mode 100644 index 00000000..9919f777 --- /dev/null +++ b/src/WixToolset.Core.Burn/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.Core.Burn.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 + { + 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.Burn/Inscribe/InscribeBundleCommand.cs b/src/WixToolset.Core.Burn/Inscribe/InscribeBundleCommand.cs new file mode 100644 index 00000000..5eb76479 --- /dev/null +++ b/src/WixToolset.Core.Burn/Inscribe/InscribeBundleCommand.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.Core.Burn.Inscribe +{ + using System.IO; + using WixToolset.Core.Burn.Bundles; + using WixToolset.Extensibility; + + internal class InscribeBundleCommand + { + public InscribeBundleCommand(IInscribeContext context) + { + this.Context = context; + } + + private IInscribeContext Context { get; } + + public bool Execute() + { + bool inscribed = false; + string tempFile = Path.Combine(this.Context.IntermediateFolder, "bundle_engine_signed.exe"); + + using (BurnReader reader = BurnReader.Open(this.Context.InputFilePath)) + { + File.Copy(this.Context.SignedEngineFile, tempFile, true); + + // If there was an attached container on the original (unsigned) bundle, put it back. + if (reader.AttachedContainerSize > 0) + { + reader.Stream.Seek(reader.AttachedContainerAddress, SeekOrigin.Begin); + + using (BurnWriter writer = BurnWriter.Open(tempFile)) + { + writer.RememberThenResetSignature(); + writer.AppendContainer(reader.Stream, reader.AttachedContainerSize, BurnCommon.Container.Attached); + inscribed = true; + } + } + } + + Directory.CreateDirectory(Path.GetDirectoryName(this.Context.OutputFile)); + if (File.Exists(this.Context.OutputFile)) + { + File.Delete(this.Context.OutputFile); + } + + File.Move(tempFile, this.Context.OutputFile); + WixToolset.Core.Native.NativeMethods.ResetAcls(new string[] { this.Context.OutputFile }, 1); + + return inscribed; + } + } +} diff --git a/src/WixToolset.Core.Burn/Inscribe/InscribeBundleEngineCommand.cs b/src/WixToolset.Core.Burn/Inscribe/InscribeBundleEngineCommand.cs new file mode 100644 index 00000000..26af056b --- /dev/null +++ b/src/WixToolset.Core.Burn/Inscribe/InscribeBundleEngineCommand.cs @@ -0,0 +1,61 @@ +// 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.Core.Burn.Inscribe +{ + using System; + using System.IO; + using WixToolset.Core.Burn.Bundles; + using WixToolset.Extensibility; + + internal class InscribeBundleEngineCommand + { + public InscribeBundleEngineCommand(IInscribeContext context) + { + this.Context = context; + } + + private IInscribeContext Context { get; } + + public bool Execute() + { + string tempFile = Path.Combine(this.Context.IntermediateFolder, "bundle_engine_unsigned.exe"); + + using (BurnReader reader = BurnReader.Open(this.Context.InputFilePath)) + using (FileStream writer = File.Open(tempFile, FileMode.Create, FileAccess.Write, FileShare.Read | FileShare.Delete)) + { + reader.Stream.Seek(0, SeekOrigin.Begin); + + byte[] buffer = new byte[4 * 1024]; + int total = 0; + int read = 0; + do + { + read = Math.Min(buffer.Length, (int)reader.EngineSize - total); + + read = reader.Stream.Read(buffer, 0, read); + writer.Write(buffer, 0, read); + + total += read; + } while (total < reader.EngineSize && 0 < read); + + if (total != reader.EngineSize) + { + throw new InvalidOperationException("Failed to copy engine out of bundle."); + } + + // TODO: update writer with detached container signatures. + } + + Directory.CreateDirectory(Path.GetDirectoryName(this.Context.OutputFile)); + if (File.Exists(this.Context.OutputFile)) + { + File.Delete(this.Context.OutputFile); + } + + File.Move(tempFile, this.Context.OutputFile); + WixToolset.Core.Native.NativeMethods.ResetAcls(new string[] { this.Context.OutputFile }, 1); + + return true; + } + } +} diff --git a/src/WixToolset.Core.Burn/VerifyInterop.cs b/src/WixToolset.Core.Burn/VerifyInterop.cs new file mode 100644 index 00000000..81fbec65 --- /dev/null +++ b/src/WixToolset.Core.Burn/VerifyInterop.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 +{ + using System; + using System.Collections; + using System.Runtime.CompilerServices; + using System.Runtime.InteropServices; + + internal class VerifyInterop + { + internal const string GenericVerify2 = "00AAC56B-CD44-11d0-8CC2-00C04FC295EE"; + internal const uint WTD_UI_NONE = 2; + internal const uint WTD_REVOKE_NONE = 0; + internal const uint WTD_CHOICE_CATALOG = 2; + internal const uint WTD_STATEACTION_VERIFY = 1; + internal const uint WTD_REVOCATION_CHECK_NONE = 0x10; + internal const int ErrorInsufficientBuffer = 122; + + [StructLayout(LayoutKind.Sequential)] + internal struct WinTrustData + { + internal uint cbStruct; + internal IntPtr pPolicyCallbackData; + internal IntPtr pSIPClientData; + internal uint dwUIChoice; + internal uint fdwRevocationChecks; + internal uint dwUnionChoice; + internal IntPtr pCatalog; + internal uint dwStateAction; + internal IntPtr hWVTStateData; + [MarshalAs(UnmanagedType.LPWStr)] + internal string pwszURLReference; + internal uint dwProvFlags; + internal uint dwUIContext; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct WinTrustCatalogInfo + { + internal uint cbStruct; + internal uint dwCatalogVersion; + [MarshalAs(UnmanagedType.LPWStr)] + internal string pcwszCatalogFilePath; + [MarshalAs(UnmanagedType.LPWStr)] + internal string pcwszMemberTag; + [MarshalAs(UnmanagedType.LPWStr)] + internal string pcwszMemberFilePath; + internal IntPtr hMemberFile; + internal IntPtr pbCalculatedFileHash; + internal uint cbCalculatedFileHash; + internal IntPtr pcCatalogContext; + } + + [DllImport("wintrust.dll", SetLastError = true)] + internal static extern long WinVerifyTrust(IntPtr windowHandle, ref Guid actionGuid, ref WinTrustData trustData); + + [DllImport("wintrust.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool CryptCATAdminCalcHashFromFileHandle( + IntPtr fileHandle, + [In, Out] + ref uint hashSize, + [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] + byte[] hashBytes, + uint flags); + } +} diff --git a/src/WixToolset.Core.Burn/WixToolset.Core.Burn.csproj b/src/WixToolset.Core.Burn/WixToolset.Core.Burn.csproj new file mode 100644 index 00000000..878ac200 --- /dev/null +++ b/src/WixToolset.Core.Burn/WixToolset.Core.Burn.csproj @@ -0,0 +1,36 @@ + + + + + + netstandard2.0 + Core Burn + WiX Toolset Core Burn + + + + NU1701 + + + + + + + + + + + + + + + + + + + + + + + + -- cgit v1.2.3-55-g6feb