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));