diff --git a/src/Microsoft.DotNet.Build.Tasks.Installers/Microsoft.DotNet.Build.Tasks.Installers.csproj b/src/Microsoft.DotNet.Build.Tasks.Installers/Microsoft.DotNet.Build.Tasks.Installers.csproj index 1053542f595..a65bb9b4b8a 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Installers/Microsoft.DotNet.Build.Tasks.Installers.csproj +++ b/src/Microsoft.DotNet.Build.Tasks.Installers/Microsoft.DotNet.Build.Tasks.Installers.csproj @@ -19,6 +19,11 @@ + + + + + "any", + "i386" => "x86", + "i486" => "x86", + "i586" => "x86", + "i686" => "x86", + "x86_64" => "x64", + "armv6hl" => "arm", + "armv7hl" => "arm", + "aarch64" => "arm64", + _ => rpmPackageArchitecture + }; + } + private static short GetRpmOS(OSPlatform os) { // See /usr/lib/rpm/rpmrc for the canonical OS mapping @@ -204,8 +221,8 @@ public RpmPackage Build() foreach (var script in _scripts) { - entries.Add(new((RpmHeaderTag)Enum.Parse(typeof(RpmHeaderTag), script.Key), RpmHeaderEntryType.String, "/bin/sh")); - entries.Add(new((RpmHeaderTag)Enum.Parse(typeof(RpmHeaderTag), $"{script.Key}prog"), RpmHeaderEntryType.String, script.Value)); + entries.Add(new((RpmHeaderTag)Enum.Parse(typeof(RpmHeaderTag), script.Key), RpmHeaderEntryType.String, script.Value)); + entries.Add(new((RpmHeaderTag)Enum.Parse(typeof(RpmHeaderTag), $"{script.Key}prog"), RpmHeaderEntryType.String, "/bin/sh")); } MemoryStream cpioArchive = new(); diff --git a/src/Microsoft.DotNet.Build.Tasks.Installers/src/RpmHeaderTag.cs b/src/Microsoft.DotNet.Build.Tasks.Installers/src/RpmHeaderTag.cs index e3a1352e0ea..23985d65a56 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Installers/src/RpmHeaderTag.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Installers/src/RpmHeaderTag.cs @@ -31,7 +31,9 @@ public enum RpmHeaderTag Url = 1020, OperatingSystem = 1021, Architecture = 1022, + Prein = 1023, Postin = 1024, + Preun = 1025, Postun = 1026, FileSizes = 1028, FileModes = 1030, @@ -55,7 +57,9 @@ public enum RpmHeaderTag ChangelogTimestamp = 1080, ChangelogName = 1081, ChangelogText = 1082, + Preinprog = 1085, Postinprog = 1086, + Preunprog = 1087, Postunprog = 1088, FileDevices = 1095, FileInode = 1096, diff --git a/src/Microsoft.DotNet.SignTool.Tests/FakeSignTool.cs b/src/Microsoft.DotNet.SignTool.Tests/FakeSignTool.cs index 893620354be..59526325d32 100644 --- a/src/Microsoft.DotNet.SignTool.Tests/FakeSignTool.cs +++ b/src/Microsoft.DotNet.SignTool.Tests/FakeSignTool.cs @@ -65,6 +65,11 @@ public override bool VerifySignedDeb(TaskLoggingHelper log, string filePath) return true; } + public override bool VerifySignedRpm(TaskLoggingHelper log, string filePath) + { + return true; + } + public override bool VerifySignedPowerShellFile(string filePath) { return true; diff --git a/src/Microsoft.DotNet.SignTool.Tests/Resources/test.rpm b/src/Microsoft.DotNet.SignTool.Tests/Resources/test.rpm new file mode 100644 index 00000000000..25f662be03a Binary files /dev/null and b/src/Microsoft.DotNet.SignTool.Tests/Resources/test.rpm differ diff --git a/src/Microsoft.DotNet.SignTool.Tests/SignToolTests.cs b/src/Microsoft.DotNet.SignTool.Tests/SignToolTests.cs index 8a47b644f4c..b3595c36452 100644 --- a/src/Microsoft.DotNet.SignTool.Tests/SignToolTests.cs +++ b/src/Microsoft.DotNet.SignTool.Tests/SignToolTests.cs @@ -15,6 +15,7 @@ using Microsoft.Build.Utilities; using Xunit; using Xunit.Abstractions; +using Microsoft.DotNet.Build.Tasks.Installers; namespace Microsoft.DotNet.SignTool.Tests { @@ -34,6 +35,7 @@ public class SignToolTests : IDisposable {".psc1", new List{ new SignInfo("PSCCertificate") } }, {".dylib", new List{ new SignInfo("DylibCertificate") } }, {".deb", new List{ new SignInfo("LinuxSign") } }, + {".rpm", new List{ new SignInfo("LinuxSign") } }, {".dll", new List{ new SignInfo("Microsoft400") } }, // lgtm [cs/common-default-passwords] Safe, these are certificate names {".exe", new List{ new SignInfo("Microsoft400") } }, // lgtm [cs/common-default-passwords] Safe, these are certificate names {".msi", new List{ new SignInfo("Microsoft400") } }, // lgtm [cs/common-default-passwords] Safe, these are certificate names @@ -447,6 +449,60 @@ private string ExtractArchiveFromDebPackage(string debianPackage, string archive File.WriteAllBytes(archive, ((MemoryStream)content).ToArray()); return archive; } + + private void ValidateProducedRpmContent( + string rpmPackage, + (string, string)[] expectedFilesOriginalHashes, + string[] signableFiles, + string originalUncompressedPayloadChecksum) + { + string tempDir = Path.Combine(_tmpDir, "verification"); + Directory.CreateDirectory(tempDir); + + string layout = Path.Combine(tempDir, "layout"); + Directory.CreateDirectory(layout); + + ZipData.ExtractRpmPayloadContents(rpmPackage, layout); + + // Checks: + // Expected files are present + // Signed files have hashes different than original + foreach ((string targetSystemFilePath, string originalHash) in expectedFilesOriginalHashes) + { + string layoutFilePath = Path.Combine(layout, targetSystemFilePath); + File.Exists(layoutFilePath).Should().BeTrue(); + + using MD5 md5 = MD5.Create(); // lgtm [cs/weak-crypto] Azure Storage specifies use of MD5 + using FileStream fileStream = File.OpenRead(layoutFilePath); + string newHash = Convert.ToHexString(md5.ComputeHash(fileStream)); + + if (signableFiles.Contains(targetSystemFilePath)) + { + newHash.Should().NotBe(originalHash); + } + else + { + newHash.Should().Be(originalHash); + } + } + + // Checks: + // Header payload digest matches the hash of the payload + // Header payload digest is different than the hash of the original payload + IReadOnlyList.Entry> headerEntries = ZipData.GetRpmHeaderEntries(rpmPackage); + string uncompressedPayloadDigest = ((string[])headerEntries.FirstOrDefault(e => e.Tag == RpmHeaderTag.UncompressedPayloadDigest).Value)[0].ToString(); + uncompressedPayloadDigest.Should().NotBe(originalUncompressedPayloadChecksum); + + using var stream = File.Open(rpmPackage, FileMode.Open); + using RpmPackage package = RpmPackage.Read(stream); + package.ArchiveStream.Seek(0, SeekOrigin.Begin); + using (SHA256 sha256 = SHA256.Create()) + { + byte[] hash = sha256.ComputeHash(package.ArchiveStream); + string checksum = Convert.ToHexString(hash).ToLower(); + checksum.Should().Be(uncompressedPayloadDigest); + } + } #endif [Fact] public void EmptySigningList() @@ -1421,6 +1477,48 @@ public void CheckDebSigning() ValidateProducedDebContent(Path.Combine(_tmpDir, "test.deb"), expectedFilesOriginalHashes, signableFiles, expectedControlFileContent); } + [LinuxOnlyFact] + public void CheckRpmSigning() + { + // List of files to be considered for signing + var itemsToSign = new ITaskItem[] + { + new TaskItem(GetResourcePath("test.rpm")) + }; + + // Default signing information + var strongNameSignInfo = new Dictionary>(); + + // Overriding information + var fileSignInfo = new Dictionary(); + + ValidateFileSignInfos(itemsToSign, strongNameSignInfo, fileSignInfo, s_fileExtensionSignInfo, new[] + { + "File 'mscorlib.dll' TargetFramework='.NETCoreApp,Version=v10.0' Certificate='Microsoft400'", + "File 'test.rpm' Certificate='LinuxSign'" + }); + + ValidateGeneratedProject(itemsToSign, strongNameSignInfo, fileSignInfo, s_fileExtensionSignInfo, new[] + { +$@" + Microsoft400 +", +$@" + LinuxSign +" + }); + + var expectedFilesOriginalHashes = new (string, string)[] + { + ("usr/local/bin/hello", "644981BBD6F4ED1B3CF68CD0F47981AA"), + ("usr/local/bin/mscorlib.dll", "B80EEBA2B8616B7C37E49B004D69BBB7") + }; + string[] signableFiles = ["usr/local/bin/mscorlib.dll"]; + string originalUncompressedPayloadChecksum = "216c2a99006d2e14d28a40c0f14a63f6462f533e89789a6f294186e0a0aad3fd"; + + ValidateProducedRpmContent(Path.Combine(_tmpDir, "test.rpm"), expectedFilesOriginalHashes, signableFiles, originalUncompressedPayloadChecksum); + } + [Fact] public void VerifyDebIntegrity() { diff --git a/src/Microsoft.DotNet.SignTool/src/BatchSignUtil.cs b/src/Microsoft.DotNet.SignTool/src/BatchSignUtil.cs index 7c5e6e92ed9..bab9051aee8 100644 --- a/src/Microsoft.DotNet.SignTool/src/BatchSignUtil.cs +++ b/src/Microsoft.DotNet.SignTool/src/BatchSignUtil.cs @@ -509,6 +509,17 @@ private void VerifyCertificates(TaskLoggingHelper log) log.LogError($"Deb package {fileName} must be signed with a LinuxSign certificate."); } } + else if (fileName.IsRpm()) + { + if (isInvalidEmptyCertificate) + { + log.LogError($"Rpm package {fileName} should have a certificate name."); + } + if (!IsLinuxSignCertificate(fileName.SignInfo.Certificate)) + { + log.LogError($"Rpm package {fileName} must be signed with a LinuxSign certificate."); + } + } else if (fileName.IsNupkg()) { if(isInvalidEmptyCertificate) @@ -577,8 +588,27 @@ private void VerifyAfterSign(TaskLoggingHelper log, FileSignInfo file) { _log.LogError($"Deb package {file.FullPath} is not signed properly."); } + else + { + _log.LogMessage(MessageImportance.Low, $"Deb package {file.FullPath} is signed properly"); + } #endif } + else if (file.IsRpm()) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + _log.LogMessage(MessageImportance.Low, $"Skipping signature verification of {file.FullPath} on non-Linux platform."); + } + else if (!_signTool.VerifySignedRpm(log, file.FullPath)) + { + _log.LogError($"Rpm package {file.FullPath} is not signed properly."); + } + else + { + _log.LogMessage(MessageImportance.Low, $"Rpm package {file.FullPath} is signed properly"); + } + } else if (file.IsPowerShellScript()) { if (!_signTool.VerifySignedPowerShellFile(file.FullPath)) diff --git a/src/Microsoft.DotNet.SignTool/src/Configuration.cs b/src/Microsoft.DotNet.SignTool/src/Configuration.cs index c03765b1749..48f530c4676 100644 --- a/src/Microsoft.DotNet.SignTool/src/Configuration.cs +++ b/src/Microsoft.DotNet.SignTool/src/Configuration.cs @@ -465,7 +465,19 @@ private FileSignInfo ExtractSignInfo( _log.LogMessage(MessageImportance.Low, $"File {file.FullPath} is signed."); } } - else if(FileSignInfo.IsPowerShellScript(file.FullPath)) + else if (FileSignInfo.IsRpm(file.FullPath)) + { + isAlreadyAuthenticodeSigned = VerifySignatures.VerifySignedRpm(_log, file.FullPath); + if (!isAlreadyAuthenticodeSigned) + { + _log.LogMessage(MessageImportance.Low, $"File {file.FullPath} is not signed."); + } + else + { + _log.LogMessage(MessageImportance.Low, $"File {file.FullPath} is signed."); + } + } + else if (FileSignInfo.IsPowerShellScript(file.FullPath)) { isAlreadyAuthenticodeSigned = VerifySignatures.VerifySignedPowerShellFile(file.FullPath); if (!isAlreadyAuthenticodeSigned) diff --git a/src/Microsoft.DotNet.SignTool/src/FileSignInfo.cs b/src/Microsoft.DotNet.SignTool/src/FileSignInfo.cs index 820663130a0..c7694c1d1a7 100644 --- a/src/Microsoft.DotNet.SignTool/src/FileSignInfo.cs +++ b/src/Microsoft.DotNet.SignTool/src/FileSignInfo.cs @@ -24,6 +24,9 @@ internal readonly struct FileSignInfo internal static bool IsDeb(string path) => Path.GetExtension(path) == ".deb"; + internal static bool IsRpm(string path) + => Path.GetExtension(path) == ".rpm"; + internal static bool IsPEFile(string path) => Path.GetExtension(path) == ".exe" || Path.GetExtension(path) == ".dll"; @@ -60,10 +63,12 @@ internal static bool IsPackage(string path) => IsVsix(path) || IsNupkg(path); internal static bool IsZipContainer(string path) - => IsPackage(path) || IsMPack(path) || IsZip(path) || IsTarGZip(path) || IsDeb(path); + => IsPackage(path) || IsMPack(path) || IsZip(path) || IsTarGZip(path) || IsDeb(path) || IsRpm(path); internal bool IsDeb() => IsDeb(FileName); + internal bool IsRpm() => IsRpm(FileName); + internal bool IsPEFile() => IsPEFile(FileName); internal bool IsManaged() => ContentUtil.IsManaged(FullPath); diff --git a/src/Microsoft.DotNet.SignTool/src/RealSignTool.cs b/src/Microsoft.DotNet.SignTool/src/RealSignTool.cs index 0ed82c06343..0113603d1e5 100644 --- a/src/Microsoft.DotNet.SignTool/src/RealSignTool.cs +++ b/src/Microsoft.DotNet.SignTool/src/RealSignTool.cs @@ -110,6 +110,11 @@ public override bool VerifySignedDeb(TaskLoggingHelper log, string filePath) return VerifySignatures.VerifySignedDeb(log, filePath); } + public override bool VerifySignedRpm(TaskLoggingHelper log, string filePath) + { + return VerifySignatures.VerifySignedRpm(log, filePath); + } + public override bool VerifySignedPowerShellFile(string filePath) { return VerifySignatures.VerifySignedPowerShellFile(filePath); diff --git a/src/Microsoft.DotNet.SignTool/src/SignTool.cs b/src/Microsoft.DotNet.SignTool/src/SignTool.cs index cac8b16df06..cb512121fee 100644 --- a/src/Microsoft.DotNet.SignTool/src/SignTool.cs +++ b/src/Microsoft.DotNet.SignTool/src/SignTool.cs @@ -33,6 +33,7 @@ internal SignTool(SignToolArgs args, TaskLoggingHelper log) public abstract bool LocalStrongNameSign(IBuildEngine buildEngine, int round, IEnumerable files); public abstract bool VerifySignedDeb(TaskLoggingHelper log, string filePath); + public abstract bool VerifySignedRpm(TaskLoggingHelper log, string filePath); public abstract bool VerifySignedPEFile(Stream stream); public abstract bool VerifySignedPowerShellFile(string filePath); public abstract bool VerifySignedNugetFileMarker(string filePath); diff --git a/src/Microsoft.DotNet.SignTool/src/SignToolConstants.cs b/src/Microsoft.DotNet.SignTool/src/SignToolConstants.cs index 8a55ed8bc28..619bfb0a42d 100644 --- a/src/Microsoft.DotNet.SignTool/src/SignToolConstants.cs +++ b/src/Microsoft.DotNet.SignTool/src/SignToolConstants.cs @@ -32,7 +32,7 @@ internal static class SignToolConstants /// /// List of known signable extensions. Copied, removing duplicates, from here: /// https://microsoft.sharepoint.com/teams/prss/Codesign/SitePages/Signable%20Files.aspx - /// ".deb" is not in the list linked above, but it is a known signable extension. + /// ".deb" and ".rpm" are not in the list linked above, but they are known signable extension. /// public static readonly HashSet SignableExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) { @@ -113,6 +113,7 @@ internal static class SignToolConstants ".pyd", ".deb", + ".rpm", }; /// diff --git a/src/Microsoft.DotNet.SignTool/src/ValidationOnlySignTool.cs b/src/Microsoft.DotNet.SignTool/src/ValidationOnlySignTool.cs index 4423c51089c..2e037a5c679 100644 --- a/src/Microsoft.DotNet.SignTool/src/ValidationOnlySignTool.cs +++ b/src/Microsoft.DotNet.SignTool/src/ValidationOnlySignTool.cs @@ -46,6 +46,9 @@ public override void RemoveStrongNameSign(string assemblyPath) public override bool VerifySignedDeb(TaskLoggingHelper log, string filePath) => true; + public override bool VerifySignedRpm(TaskLoggingHelper log, string filePath) + => true; + public override bool VerifySignedPEFile(Stream assemblyStream) => true; diff --git a/src/Microsoft.DotNet.SignTool/src/VerifySignatures.cs b/src/Microsoft.DotNet.SignTool/src/VerifySignatures.cs index b0f28fd0697..4fda36bfd28 100644 --- a/src/Microsoft.DotNet.SignTool/src/VerifySignatures.cs +++ b/src/Microsoft.DotNet.SignTool/src/VerifySignatures.cs @@ -82,6 +82,13 @@ internal static bool VerifySignedDeb(TaskLoggingHelper log, string filePath) # endif } + internal static bool VerifySignedRpm(TaskLoggingHelper log, string filePath) + { + // RPM signature verification is not yet implemented + log.LogMessage(MessageImportance.Low, $"Skipping signature verification of {filePath} - not yet implemented."); + return true; + } + internal static bool VerifySignedPowerShellFile(string filePath) { return File.ReadLines(filePath).Any(line => line.IndexOf("# SIG # Begin Signature Block", StringComparison.OrdinalIgnoreCase) >= 0); diff --git a/src/Microsoft.DotNet.SignTool/src/ZipData.cs b/src/Microsoft.DotNet.SignTool/src/ZipData.cs index 3ac9ea06cd9..2893c60f3f6 100644 --- a/src/Microsoft.DotNet.SignTool/src/ZipData.cs +++ b/src/Microsoft.DotNet.SignTool/src/ZipData.cs @@ -12,6 +12,7 @@ using System.Data; using System.Diagnostics; using Microsoft.DotNet.Build.Tasks.Installers; +using System.Runtime.InteropServices; #if NET472 using System.IO.Packaging; @@ -72,6 +73,19 @@ internal ZipData(FileSignInfo fileSignInfo, ImmutableDictionary return ReadDebContainerEntries(archivePath, "data.tar"); #endif } + else if (FileSignInfo.IsRpm(archivePath)) + { +#if NET472 + throw new NotImplementedException("RPM signing is not supported on .NET Framework"); +#else + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + throw new NotImplementedException("RPM signing is only supported on Linux platform"); + } + + return ReadRpmContainerEntries(archivePath); +#endif + } return ReadZipEntries(archivePath); } @@ -102,6 +116,19 @@ public void Repack(TaskLoggingHelper log, string tempDir, string wixToolsPath, s throw new NotImplementedException("Debian signing is not supported on .NET Framework"); #else RepackDebContainer(log, tempDir); +#endif + } + else if (FileSignInfo.IsRpm()) + { +#if NET472 + throw new NotImplementedException("RPM signing is not supported on .NET Framework"); +#else + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + throw new NotImplementedException("RPM signing is only supported on Linux platform"); + } + + RepackRpmContainer(log, tempDir); #endif } else @@ -527,6 +554,136 @@ internal static void ExtractTarballContents(string file, string destination, boo } } } + + private static IEnumerable<(string relativePath, Stream content, long contentSize)> ReadRpmContainerEntries(string archivePath) + { + using var stream = File.Open(archivePath, FileMode.Open); + using RpmPackage rpmPackage = RpmPackage.Read(stream); + using var dataStream = File.OpenWrite(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString())); + using var archive = new CpioReader(rpmPackage.ArchiveStream, leaveOpen: false); + + while (archive.GetNextEntry() is CpioEntry entry) + { + yield return (entry.Name, entry.DataStream, entry.DataStream.Length); + } + } + + private void RepackRpmContainer(TaskLoggingHelper log, string tempDir) + { + // Unpack original package - create the layout + string workingDir = Path.Combine(tempDir, Guid.NewGuid().ToString().Split('-')[0]); + Directory.CreateDirectory(workingDir); + string layout = Path.Combine(workingDir, "layout"); + Directory.CreateDirectory(layout); + ExtractRpmPayloadContents(FileSignInfo.FullPath, layout); + + // Update signed files in layout + foreach (var signedPart in NestedParts.Values) + { + File.Copy(signedPart.FileSignInfo.FullPath, Path.Combine(layout, signedPart.RelativeName), overwrite: true); + } + + // Create payload.cpio + string payload = Path.Combine(workingDir, "payload.cpio"); + + RunExternalProcess("bash", $"-c \"find . -depth ! -wholename '.' -print | cpio -H newc -o --quiet > '{payload}'\"", out string _, layout); + + // Collect file types for all files in layout + RunExternalProcess("bash", $"-c \"find . -depth ! -wholename '.' -exec file {{}} \\;\"", out string output, layout); + ITaskItem[] rawPayloadFileKinds = + output.Split('\n', StringSplitOptions.RemoveEmptyEntries) + .Select(t => new TaskItem(t)) + .ToArray(); + + IReadOnlyList.Entry> headerEntries = GetRpmHeaderEntries(FileSignInfo.FullPath); + string[] requireNames = (string[])headerEntries.FirstOrDefault(e => e.Tag == RpmHeaderTag.RequireName).Value; + string[] requireVersions = (string[])headerEntries.FirstOrDefault(e => e.Tag == RpmHeaderTag.RequireVersion).Value; + string[] changelogLines = (string[])headerEntries.FirstOrDefault(e => e.Tag == RpmHeaderTag.ChangelogText).Value; + string[] conflictNames = (string[])headerEntries.FirstOrDefault(e => e.Tag == RpmHeaderTag.ConflictName).Value; + + List scripts = []; + foreach (var scriptTag in new[] { RpmHeaderTag.Prein, RpmHeaderTag.Preun, RpmHeaderTag.Postin, RpmHeaderTag.Postun }) + { + string contents = (string)headerEntries.FirstOrDefault(e => e.Tag == scriptTag).Value; + if (contents != null) + { + string kind = Enum.GetName(scriptTag); + string file = Path.Combine(workingDir, kind); + File.WriteAllText(file, contents); + scripts.Add(new TaskItem(file, new Dictionary { { "Kind", kind } })); + } + } + + // Create RPM package + CreateRpmPackage createRpmPackageTask = new() + { + OutputRpmPackagePath = FileSignInfo.FullPath, + Vendor = headerEntries.FirstOrDefault(e => e.Tag == RpmHeaderTag.Vendor).Value.ToString(), + Packager = headerEntries.FirstOrDefault(e => e.Tag == RpmHeaderTag.Packager).Value.ToString(), + PackageName = headerEntries.FirstOrDefault(e => e.Tag == RpmHeaderTag.PackageName).Value.ToString(), + PackageVersion = headerEntries.FirstOrDefault(e => e.Tag == RpmHeaderTag.PackageVersion).Value.ToString(), + PackageRelease = headerEntries.FirstOrDefault(e => e.Tag == RpmHeaderTag.PackageRelease).Value.ToString(), + PackageOS = headerEntries.FirstOrDefault(e => e.Tag == RpmHeaderTag.OperatingSystem).Value.ToString(), + PackageArchitecture = RpmBuilder.GetDotNetArchitectureFromRpmHeaderArchitecture(headerEntries.FirstOrDefault(e => e.Tag == RpmHeaderTag.Architecture).Value.ToString()), + Payload = payload, + RawPayloadFileKinds = rawPayloadFileKinds, + Requires = requireNames != null ? requireNames.Zip(requireVersions, (name, version) => new TaskItem($"{name}", new Dictionary { { "Version", version } })).Where(t => !t.ItemSpec.StartsWith("rpmlib")).ToArray() : [], + Conflicts = conflictNames != null ? conflictNames.Select(c => new TaskItem(c)).ToArray() : [], + OwnedDirectories = ((string[])headerEntries.FirstOrDefault(e => e.Tag == RpmHeaderTag.DirectoryNames).Value).Select(d => new TaskItem(d)).ToArray(), + ChangelogLines = changelogLines != null ? changelogLines.Select(c => new TaskItem(c)).ToArray() : [], + License = headerEntries.FirstOrDefault(e => e.Tag == RpmHeaderTag.License).Value.ToString(), + Summary = headerEntries.FirstOrDefault(e => e.Tag == RpmHeaderTag.Summary).Value.ToString(), + Description = headerEntries.FirstOrDefault(e => e.Tag == RpmHeaderTag.Description).Value.ToString(), + PackageUrl = headerEntries.FirstOrDefault(e => e.Tag == RpmHeaderTag.Url).Value.ToString(), + Scripts = scripts.ToArray(), + }; + + if (!createRpmPackageTask.Execute()) + { + throw new Exception($"Failed to create RPM package: {FileSignInfo.FileName}"); + } + } + + internal static IReadOnlyList.Entry> GetRpmHeaderEntries(string rpmPackage) + { + using var stream = File.Open(rpmPackage, FileMode.Open); + return RpmPackage.Read(stream).Header.Entries; + } + + internal static void ExtractRpmPayloadContents(string rpmPackage, string layout) + { + foreach (var (relativePath, content, contentSize) in ReadRpmContainerEntries(rpmPackage)) + { + string outputPath = Path.Combine(layout, relativePath); + Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!); + + if (content != null) + { + using FileStream outputFileStream = File.Create(outputPath); + content.CopyTo(outputFileStream); + } + } + } + + private static bool RunExternalProcess(string cmd, string args, out string output, string workingDir = null) + { + ProcessStartInfo psi = new() + { + FileName = cmd, + Arguments = args, + RedirectStandardOutput = true, + RedirectStandardError = false, + UseShellExecute = false, + CreateNoWindow = true, + WorkingDirectory = workingDir + }; + + using Process process = Process.Start(psi); + output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + + return process.ExitCode == 0; + } #endif } }