diff --git a/Directory.Packages.props b/Directory.Packages.props index 4131bbc7..8b26b3a3 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..a666927e 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -6,6 +6,8 @@ + + diff --git a/src/Sign.Core/Containers/CabContainer.cs b/src/Sign.Core/Containers/CabContainer.cs new file mode 100644 index 00000000..8e958604 --- /dev/null +++ b/src/Sign.Core/Containers/CabContainer.cs @@ -0,0 +1,84 @@ +// 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.Compression; +using WixToolset.Dtf.Compression.Cab; + +namespace Sign.Core +{ + internal class CabContainer : Container + { + private readonly IDirectoryService _directoryService; + private readonly ILogger _logger; + private readonly FileInfo _cabFile; + + internal CabContainer( + FileInfo cabFile, + IDirectoryService directoryService, + IFileMatcher fileMatcher, + ILogger logger) + : base(fileMatcher) + { + ArgumentNullException.ThrowIfNull(cabFile, nameof(cabFile)); + ArgumentNullException.ThrowIfNull(directoryService, nameof(directoryService)); + ArgumentNullException.ThrowIfNull(logger, nameof(logger)); + + _directoryService = directoryService; + _logger = logger; + _cabFile = cabFile; + } + + public override ValueTask OpenAsync() + { + if (TemporaryDirectory is not null) + { + throw new InvalidOperationException(); + } + + TemporaryDirectory = new TemporaryDirectory(_directoryService); + + _logger.LogInformation( + Resources.OpeningContainer, + _cabFile.FullName, + TemporaryDirectory.Directory.FullName); + + new CabInfo(_cabFile.FullName).Unpack(TemporaryDirectory.Directory.FullName); + + return ValueTask.CompletedTask; + } + + public override ValueTask SaveAsync() + { + if (TemporaryDirectory is null) + { + throw new InvalidOperationException(); + } + + _logger.LogInformation( + Resources.SavingContainer, + _cabFile.FullName, + TemporaryDirectory.Directory.FullName); + + using (TemporaryDirectory temporaryDirectory = new(_directoryService)) + { + string destinationFilePath = Path.Combine(temporaryDirectory.Directory.FullName, _cabFile.Name); + + new CabInfo(destinationFilePath).Pack( + TemporaryDirectory.Directory.FullName, + includeSubdirectories: true, + CompressionLevel.Max, + progressHandler: null); + + _cabFile.Delete(); + + File.Move(destinationFilePath, _cabFile.FullName, overwrite: true); + + _cabFile.Refresh(); + } + + return ValueTask.CompletedTask; + } + } +} diff --git a/src/Sign.Core/Containers/ContainerProvider.cs b/src/Sign.Core/Containers/ContainerProvider.cs index 73218c72..c91bb69b 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 const string _cabExtension = ".cab"; // Dependency injection requires a public constructor. public ContainerProvider( @@ -98,6 +99,13 @@ public bool IsZipContainer(FileInfo file) return _zipExtensions.Contains(file.Extension); } + public bool IsCabContainer(FileInfo file) + { + ArgumentNullException.ThrowIfNull(file, nameof(file)); + + return string.Equals(file.Extension, _cabExtension, StringComparison.OrdinalIgnoreCase); + } + public IContainer? GetContainer(FileInfo file) { ArgumentNullException.ThrowIfNull(file, nameof(file)); @@ -122,6 +130,11 @@ public bool IsZipContainer(FileInfo file) return new NuGetContainer(file, _directoryService, _fileMatcher, _logger); } + if (IsCabContainer(file)) + { + return new CabContainer(file, _directoryService, _fileMatcher, _logger); + } + return null; } } diff --git a/src/Sign.Core/Containers/IContainerProvider.cs b/src/Sign.Core/Containers/IContainerProvider.cs index 77c26aa8..d9aa0c57 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 IsCabContainer(FileInfo file); IContainer? GetContainer(FileInfo file); } } \ No newline at end of file diff --git a/src/Sign.Core/DataFormatSigners/AggregatingSigner.cs b/src/Sign.Core/DataFormatSigners/AggregatingSigner.cs index fe0361f5..1115d808 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 cabs = (from file in files + where _containerProvider.IsCabContainer(file) + select file).ToList(); + + try + { + foreach (FileInfo cab in cabs) + { + IContainer container = _containerProvider.GetContainer(cab)!; + + await container.OpenAsync(); + + containers.Add(container); + } + + List allFiles = containers.SelectMany(c => c.GetFiles()).ToList(); + + if (allFiles.Count > 0) + { + await SignAsync(allFiles, options); + + await Parallel.ForEachAsync(containers, (container, cancellationToken) => container.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..7fe60cf1 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/Containers/CabContainerTests.cs b/test/Sign.Core.Test/Containers/CabContainerTests.cs new file mode 100644 index 00000000..c984898e --- /dev/null +++ b/test/Sign.Core.Test/Containers/CabContainerTests.cs @@ -0,0 +1,77 @@ +// 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 System.Text; +using Microsoft.Extensions.Logging; +using Moq; +using WixToolset.Dtf.Compression.Cab; + +namespace Sign.Core.Test +{ + public class CabContainerTests + { + [Fact] + public async Task OpenAsync_ExtractsCabToDirectory() + { + string[] expectedFileNames = [".a", "b", "c.d"]; + FileInfo cabFile = CreateCabFile(expectedFileNames); + + using (DirectoryServiceStub directoryService = new()) + using (CabContainer container = new(cabFile, directoryService, Mock.Of(), Mock.Of())) + { + await container.OpenAsync(); + + FileInfo[] actualFiles = directoryService.Directories[0].GetFiles("*", SearchOption.AllDirectories); + string[] actualFileNames = actualFiles + .Select(file => file.FullName.Substring(directoryService.Directories[0].FullName.Length + 1)) + .ToArray(); + + Assert.Equal(expectedFileNames, actualFileNames); + } + } + + [Fact] + public async Task SaveAsync_CompressesCabFromDirectory() + { + string[] fileNames = ["a"]; + FileInfo cabFile = CreateCabFile(fileNames); + + using (DirectoryServiceStub directoryService = new()) + using (CabContainer container = new(cabFile, directoryService, Mock.Of(), Mock.Of())) + { + await container.OpenAsync(); + + File.WriteAllText(Path.Combine(directoryService.Directories[0].FullName, "b"), "b"); + + await container.SaveAsync(); + } + + var cab = new CabInfo(cabFile.FullName); + var files = cab.GetFiles(); + Assert.Equal(2, files.Count); + Assert.Contains(files, e => e.Name == "a"); + Assert.Contains(files, e => e.Name == "b"); + } + + private static FileInfo CreateCabFile(params string[] entryNames) + { + FileInfo file = new(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())); + + var cab = new CabInfo(file.FullName); + + var sourceFiles = new List(); + foreach (string entryName in entryNames) + { + var sourceFile = Path.GetTempFileName(); + File.WriteAllBytes(sourceFile, Encoding.UTF8.GetBytes(entryName)); + + sourceFiles.Add(sourceFile); + } + + cab.PackFiles(sourceDirectory: null, sourceFiles, entryNames); + + return file; + } + } +} diff --git a/test/Sign.Core.Test/TestInfrastructure/ContainerProviderStub.cs b/test/Sign.Core.Test/TestInfrastructure/ContainerProviderStub.cs index b67bfbe7..6d9038eb 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 IsCabContainer(FileInfo file) + { + return _containerProvider.IsCabContainer(file); + } + public IContainer? GetContainer(FileInfo file) { ArgumentNullException.ThrowIfNull(file, nameof(file));