// 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.WindowsInstaller.Bind
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using WixToolset.Data;
using WixToolset.Data.Symbols;
using WixToolset.Extensibility.Data;
using WixToolset.Extensibility.Services;
///
/// Set the guids for components with generatable guids and validate all are appropriately unique.
///
internal class FinalizeComponentGuids
{
internal FinalizeComponentGuids(IMessaging messaging, IBackendHelper helper, IPathResolver pathResolver, IntermediateSection section, Platform platform)
{
this.Messaging = messaging;
this.BackendHelper = helper;
this.PathResolver = pathResolver;
this.Section = section;
this.Platform = platform;
}
private IMessaging Messaging { get; }
private IBackendHelper BackendHelper { get; }
private IPathResolver PathResolver { get; }
private IntermediateSection Section { get; }
private Platform Platform { get; }
private Dictionary ComponentIdGenSeeds { get; set; }
private ILookup FilesByComponentId { get; set; }
private Dictionary RegistrySymbolsById { get; set; }
private Dictionary TargetPathsByDirectoryId { get; set; }
public void Execute()
{
var componentGuidConditions = new Dictionary>(StringComparer.OrdinalIgnoreCase);
var guidCollisions = new HashSet(StringComparer.OrdinalIgnoreCase);
foreach (var componentSymbol in this.Section.Symbols.OfType())
{
if (componentSymbol.ComponentId == "*")
{
this.GenerateComponentGuid(componentSymbol);
}
// Now check for GUID collisions, but we don't care about unmanaged components and
// if there's a * GUID remaining, there's already an error that explained why it
// was not replaced with a real GUID.
if (!String.IsNullOrEmpty(componentSymbol.ComponentId) && componentSymbol.ComponentId != "*")
{
if (!componentGuidConditions.TryGetValue(componentSymbol.ComponentId, out var components))
{
components = new List();
componentGuidConditions.Add(componentSymbol.ComponentId, components);
}
components.Add(componentSymbol);
if (components.Count > 1)
{
guidCollisions.Add(componentSymbol.ComponentId);
}
}
}
if (guidCollisions.Count > 0)
{
this.ReportGuidCollisions(guidCollisions, componentGuidConditions);
}
}
private void GenerateComponentGuid(ComponentSymbol componentSymbol)
{
if (String.IsNullOrEmpty(componentSymbol.KeyPath) || ComponentKeyPathType.OdbcDataSource == componentSymbol.KeyPathType)
{
this.Messaging.Write(ErrorMessages.IllegalComponentWithAutoGeneratedGuid(componentSymbol.SourceLineNumbers));
return;
}
if (ComponentKeyPathType.Registry == componentSymbol.KeyPathType)
{
if (this.RegistrySymbolsById is null)
{
this.RegistrySymbolsById = this.Section.Symbols.OfType().ToDictionary(t => t.Id.Id);
}
if (this.RegistrySymbolsById.TryGetValue(componentSymbol.KeyPath, out var registrySymbol))
{
var bitness = componentSymbol.Win64 ? "64" : String.Empty;
var regkey = String.Concat(bitness, registrySymbol.Root, "\\", registrySymbol.Key, "\\", registrySymbol.Name);
componentSymbol.ComponentId = this.BackendHelper.CreateGuid(BindDatabaseCommand.WixComponentGuidNamespace, regkey.ToLowerInvariant());
}
}
else // must be a File KeyPath.
{
// If the directory table hasn't been loaded into an indexed hash
// of directory ids to target names do that now.
if (this.TargetPathsByDirectoryId is null)
{
this.TargetPathsByDirectoryId = this.ResolveDirectoryTargetPaths();
}
// If the component id generation seeds have not been indexed
// from the Directory symbols do that now.
if (this.ComponentIdGenSeeds is null)
{
// If there are any Directory symbols, build up the Component Guid
// generation seeds indexed by Directory/@Id.
this.ComponentIdGenSeeds = this.Section.Symbols.OfType()
.Where(t => !String.IsNullOrEmpty(t.ComponentGuidGenerationSeed))
.ToDictionary(t => t.Id.Id, t => t.ComponentGuidGenerationSeed);
}
// If the file symbols have not been indexed by File's ComponentRef yet
// then do that now.
if (this.FilesByComponentId is null)
{
this.FilesByComponentId = this.Section.Symbols.OfType().ToLookup(f => f.ComponentRef);
}
// validate component meets all the conditions to have a generated guid
var currentComponentFiles = this.FilesByComponentId[componentSymbol.Id.Id];
var numFilesInComponent = currentComponentFiles.Count();
string path = null;
foreach (var fileSymbol in currentComponentFiles)
{
if (fileSymbol.Id.Id == componentSymbol.KeyPath)
{
// calculate the key file's canonical target path
var directoryPath = this.PathResolver.GetCanonicalDirectoryPath(this.TargetPathsByDirectoryId, this.ComponentIdGenSeeds, componentSymbol.DirectoryRef, this.Platform);
var fileName = this.BackendHelper.GetMsiFileName(fileSymbol.Name, false, true).ToLowerInvariant();
path = Path.Combine(directoryPath, fileName);
// find paths that are not canonicalized
if (path.StartsWith(@"PersonalFolder\my pictures", StringComparison.Ordinal) ||
path.StartsWith(@"ProgramFilesFolder\common files", StringComparison.Ordinal) ||
path.StartsWith(@"ProgramMenuFolder\startup", StringComparison.Ordinal) ||
path.StartsWith("TARGETDIR", StringComparison.Ordinal) ||
path.StartsWith(@"StartMenuFolder\programs", StringComparison.Ordinal) ||
path.StartsWith(@"WindowsFolder\fonts", StringComparison.Ordinal))
{
this.Messaging.Write(ErrorMessages.IllegalPathForGeneratedComponentGuid(componentSymbol.SourceLineNumbers, fileSymbol.ComponentRef, path));
}
// if component has more than one file, the key path must be versioned
if (1 < numFilesInComponent && String.IsNullOrEmpty(fileSymbol.Version))
{
this.Messaging.Write(ErrorMessages.IllegalGeneratedGuidComponentUnversionedKeypath(componentSymbol.SourceLineNumbers));
}
}
else
{
// not a key path, so it must be an unversioned file if component has more than one file
if (1 < numFilesInComponent && !String.IsNullOrEmpty(fileSymbol.Version))
{
this.Messaging.Write(ErrorMessages.IllegalGeneratedGuidComponentVersionedNonkeypath(componentSymbol.SourceLineNumbers));
}
}
}
// if the rules were followed, reward with a generated guid
if (!this.Messaging.EncounteredError)
{
componentSymbol.ComponentId = this.BackendHelper.CreateGuid(BindDatabaseCommand.WixComponentGuidNamespace, path);
}
}
}
private void ReportGuidCollisions(HashSet guidCollisions, Dictionary> componentGuidConditions)
{
Dictionary fileSymbolsById = null;
foreach (var guid in guidCollisions)
{
var collidingComponents = componentGuidConditions[guid];
var allComponentsHaveConditions = collidingComponents.All(c => !String.IsNullOrEmpty(c.Condition));
foreach (var componentSymbol in collidingComponents)
{
string path;
string type;
if (componentSymbol.KeyPathType == ComponentKeyPathType.File)
{
if (fileSymbolsById is null)
{
fileSymbolsById = this.Section.Symbols.OfType().ToDictionary(t => t.Id.Id);
}
path = fileSymbolsById.TryGetValue(componentSymbol.KeyPath, out var fileSymbol) ? fileSymbol.Source.Path : componentSymbol.KeyPath;
type = "source path";
}
else if (componentSymbol.KeyPathType == ComponentKeyPathType.Registry)
{
if (this.RegistrySymbolsById is null)
{
this.RegistrySymbolsById = this.Section.Symbols.OfType().ToDictionary(t => t.Id.Id);
}
path = this.RegistrySymbolsById.TryGetValue(componentSymbol.KeyPath, out var registrySymbol) ? String.Concat(registrySymbol.Key, "\\", registrySymbol.Name) : componentSymbol.KeyPath;
type = "registry path";
}
else
{
if (this.TargetPathsByDirectoryId is null)
{
this.TargetPathsByDirectoryId = this.ResolveDirectoryTargetPaths();
}
path = this.PathResolver.GetCanonicalDirectoryPath(this.TargetPathsByDirectoryId, componentIdGenSeeds: null, componentSymbol.DirectoryRef, this.Platform);
type = "directory";
}
if (allComponentsHaveConditions)
{
this.Messaging.Write(WarningMessages.DuplicateComponentGuidsMustHaveMutuallyExclusiveConditions(componentSymbol.SourceLineNumbers, componentSymbol.Id.Id, componentSymbol.ComponentId, type, path));
}
else
{
this.Messaging.Write(ErrorMessages.DuplicateComponentGuids(componentSymbol.SourceLineNumbers, componentSymbol.Id.Id, componentSymbol.ComponentId, type, path));
}
}
}
}
private Dictionary ResolveDirectoryTargetPaths()
{
var directories = this.Section.Symbols.OfType().ToList();
var targetPathsByDirectoryId = new Dictionary(directories.Count);
// Get the target paths for all directories.
foreach (var directory in directories)
{
// If the directory Id already exists, we will skip it here since
// checking for duplicate primary keys is done later when importing tables
// into database
if (targetPathsByDirectoryId.ContainsKey(directory.Id.Id))
{
continue;
}
var resolvedDirectory = this.BackendHelper.CreateResolvedDirectory(directory.ParentDirectoryRef, directory.Name);
targetPathsByDirectoryId.Add(directory.Id.Id, resolvedDirectory);
}
return targetPathsByDirectoryId;
}
}
}