diff --git a/Directory.Packages.props b/Directory.Packages.props index 4131bbc7..2441fdcd 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -33,5 +33,6 @@ + \ No newline at end of file diff --git a/NuGet.Config b/NuGet.Config index 0ef48526..ae0df653 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -6,6 +6,8 @@ + + diff --git a/src/Sign.Core/Containers/ContainerProvider.cs b/src/Sign.Core/Containers/ContainerProvider.cs index 73218c72..d1a8e6f1 100644 --- a/src/Sign.Core/Containers/ContainerProvider.cs +++ b/src/Sign.Core/Containers/ContainerProvider.cs @@ -17,6 +17,7 @@ internal sealed class ContainerProvider : IContainerProvider private readonly IMakeAppxCli _makeAppxCli; private readonly HashSet _nuGetExtensions; private readonly HashSet _zipExtensions; + private readonly HashSet _msiExtensions; // Dependency injection requires a public constructor. public ContainerProvider( @@ -68,6 +69,12 @@ public ContainerProvider( ".vsix", ".zip" }; + + _msiExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) + { + ".msi", + ".msm" + }; } public bool IsAppxBundleContainer(FileInfo file) @@ -98,6 +105,13 @@ public bool IsZipContainer(FileInfo file) return _zipExtensions.Contains(file.Extension); } + public bool IsMsiContainer(FileInfo file) + { + ArgumentNullException.ThrowIfNull(file, nameof(file)); + + return _msiExtensions.Contains(file.Extension); + } + public IContainer? GetContainer(FileInfo file) { ArgumentNullException.ThrowIfNull(file, nameof(file)); @@ -122,6 +136,11 @@ public bool IsZipContainer(FileInfo file) return new NuGetContainer(file, _directoryService, _fileMatcher, _logger); } + if (IsMsiContainer(file)) + { + return new MsiContainer(file, _directoryService, _fileMatcher, _logger); + } + return null; } } diff --git a/src/Sign.Core/Containers/IContainerProvider.cs b/src/Sign.Core/Containers/IContainerProvider.cs index 77c26aa8..99599778 100644 --- a/src/Sign.Core/Containers/IContainerProvider.cs +++ b/src/Sign.Core/Containers/IContainerProvider.cs @@ -10,6 +10,7 @@ internal interface IContainerProvider bool IsAppxContainer(FileInfo file); bool IsNuGetContainer(FileInfo file); bool IsZipContainer(FileInfo file); + bool IsMsiContainer(FileInfo file); IContainer? GetContainer(FileInfo file); } } \ No newline at end of file diff --git a/src/Sign.Core/Containers/MsiContainer.cs b/src/Sign.Core/Containers/MsiContainer.cs new file mode 100644 index 00000000..ca7e091d --- /dev/null +++ b/src/Sign.Core/Containers/MsiContainer.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE.txt file in the project root for more information. + +using Microsoft.Extensions.Logging; +using WixToolset.Dtf.WindowsInstaller; +using WixToolset.Dtf.WindowsInstaller.Package; + +namespace Sign.Core +{ + internal class MsiContainer : Container + { + private readonly IDirectoryService _directoryService; + private readonly ILogger _logger; + private readonly FileInfo _msiFile; + + public MsiContainer( + FileInfo msiFile, + IDirectoryService directoryService, + IFileMatcher fileMatcher, + ILogger logger) + : base(fileMatcher) + { + ArgumentNullException.ThrowIfNull(msiFile, nameof(msiFile)); + ArgumentNullException.ThrowIfNull(directoryService, nameof(directoryService)); + ArgumentNullException.ThrowIfNull(logger, nameof(logger)); + + _directoryService = directoryService; + _logger = logger; + _msiFile = msiFile; + } + + public override ValueTask OpenAsync() + { + if (TemporaryDirectory is not null) + { + throw new InvalidOperationException(); + } + + TemporaryDirectory = new TemporaryDirectory(_directoryService); + + _logger.LogInformation( + Resources.OpeningContainer, + _msiFile.FullName, + TemporaryDirectory.Directory.FullName); + + using var package = new InstallPackage( + _msiFile.FullName, + DatabaseOpenMode.ReadOnly, + sourceDir: null, + TemporaryDirectory.Directory.FullName); + + package.ExtractFiles(); + + return ValueTask.CompletedTask; + } + + public override ValueTask SaveAsync() + { + if (TemporaryDirectory is null) + { + throw new InvalidOperationException(); + } + + _logger.LogInformation( + Resources.SavingContainer, + _msiFile.FullName, + TemporaryDirectory.Directory.FullName); + + using var package = new InstallPackage( + _msiFile.FullName, + DatabaseOpenMode.Direct, + sourceDir: null, + TemporaryDirectory.Directory.FullName); + + package.UpdateFiles(); + + return ValueTask.CompletedTask; + } + } +} diff --git a/src/Sign.Core/DataFormatSigners/AggregatingSigner.cs b/src/Sign.Core/DataFormatSigners/AggregatingSigner.cs index fe0361f5..ded093c9 100644 --- a/src/Sign.Core/DataFormatSigners/AggregatingSigner.cs +++ b/src/Sign.Core/DataFormatSigners/AggregatingSigner.cs @@ -179,6 +179,36 @@ where _containerProvider.IsAppxBundleContainer(file) containers.Clear(); } + List msis = (from file in files + where _containerProvider.IsMsiContainer(file) + select file).ToList(); + + try + { + foreach (FileInfo msi in msis) + { + IContainer container = _containerProvider.GetContainer(msi)!; + + await container.OpenAsync(); + + containers.Add(container); + } + + List allFiles = containers.SelectMany(c => GetFiles(c, options)).ToList(); + + if (allFiles.Count > 0) + { + await SignAsync(allFiles, options); + + await Parallel.ForEachAsync(containers, (c, cancellationToken) => c.SaveAsync()); + } + } + finally + { + containers.ForEach(c => c.Dispose()); + containers.Clear(); + } + // split by code sign service and fallback to default var grouped = (from signer in _signers diff --git a/src/Sign.Core/Sign.Core.csproj b/src/Sign.Core/Sign.Core.csproj index 7bd59ccd..46fc393f 100644 --- a/src/Sign.Core/Sign.Core.csproj +++ b/src/Sign.Core/Sign.Core.csproj @@ -19,6 +19,7 @@ + diff --git a/test/Sign.Core.Test/TestInfrastructure/ContainerProviderStub.cs b/test/Sign.Core.Test/TestInfrastructure/ContainerProviderStub.cs index b67bfbe7..10c71c22 100644 --- a/test/Sign.Core.Test/TestInfrastructure/ContainerProviderStub.cs +++ b/test/Sign.Core.Test/TestInfrastructure/ContainerProviderStub.cs @@ -43,6 +43,11 @@ public bool IsZipContainer(FileInfo file) return _containerProvider.IsZipContainer(file); } + public bool IsMsiContainer(FileInfo file) + { + return _containerProvider.IsMsiContainer(file); + } + public IContainer? GetContainer(FileInfo file) { ArgumentNullException.ThrowIfNull(file, nameof(file));