// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.DotNet.Build.Tasks.Installers
{
internal sealed class RpmBuilder(string packageName, string version, string releaseVersion, Architecture architecture, OSPlatform os)
{
private readonly List<(string capability, string version)> _provides = [];
private readonly List<string> _conflicts = [];
private readonly List<(string name, string text)> _changelogLines = [];
private readonly List<(string name, int flags, string version)> _requires = [
("rpmlib(CompressedFileNames)", 16777226, "3.0.4-1"),
("rpmlib(PayloadFilesHavePrefix)", 16777226, "4.0-1"),
("rpmlib(FileDigests)", 16777226, "4.6.0-1")
];
private RpmLead Lead { get; } = new()
{
Major = 3,
Minor = 0,
Type = 0, // Binary package
Architecture = GetRpmLeadArchitecture(architecture),
OperatingSystem = GetRpmOS(os),
SignatureType = 5 // Signature in a Header Structure
};
private List<RpmHeader<RpmHeaderTag>.Entry> PackageEntries { get; } = [
new RpmHeader<RpmHeaderTag>.Entry(RpmHeaderTag.I18nTable, RpmHeaderEntryType.StringArray, new[] { "C" }),
new RpmHeader<RpmHeaderTag>.Entry(RpmHeaderTag.PackageName, RpmHeaderEntryType.String, packageName),
new RpmHeader<RpmHeaderTag>.Entry(RpmHeaderTag.PackageVersion, RpmHeaderEntryType.String, version),
new RpmHeader<RpmHeaderTag>.Entry(RpmHeaderTag.PackageRelease, RpmHeaderEntryType.String, releaseVersion),
new RpmHeader<RpmHeaderTag>.Entry(RpmHeaderTag.PayloadCompressor, RpmHeaderEntryType.String, "gzip"),
new RpmHeader<RpmHeaderTag>.Entry(RpmHeaderTag.PayloadCompressorLevel, RpmHeaderEntryType.String, "9"),
new RpmHeader<RpmHeaderTag>.Entry(RpmHeaderTag.PayloadFormat, RpmHeaderEntryType.String, "cpio"),
new RpmHeader<RpmHeaderTag>.Entry(RpmHeaderTag.OperatingSystem, RpmHeaderEntryType.String, os.ToString()),
new RpmHeader<RpmHeaderTag>.Entry(RpmHeaderTag.Architecture, RpmHeaderEntryType.String, GetRpmHeaderArchitecture(architecture)),
new RpmHeader<RpmHeaderTag>.Entry(RpmHeaderTag.Encoding, RpmHeaderEntryType.String, "utf-8"),
new RpmHeader<RpmHeaderTag>.Entry(RpmHeaderTag.RpmVersion, RpmHeaderEntryType.String, "4.18.2"), // Report that the package was built with the RPM version from the last version of rpmbuild we built packages with.
new RpmHeader<RpmHeaderTag>.Entry(RpmHeaderTag.Platform, RpmHeaderEntryType.String, "x86_64-azl-linux"), // Report that the package was built on Azure Linux 3.0.
new RpmHeader<RpmHeaderTag>.Entry(RpmHeaderTag.BuildHost, RpmHeaderEntryType.String, Dns.GetHostName()),
new RpmHeader<RpmHeaderTag>.Entry(RpmHeaderTag.Group, RpmHeaderEntryType.String, "default"),
];
private static short GetRpmLeadArchitecture(Architecture architecture)
{
// See /usr/lib/rpm/rpmrc for the canonical architecture mapping
return architecture switch
{
Architecture.X86 => 1,
Architecture.X64 => 1,
Architecture.Arm => 12,
Architecture.Arm64 => 19,
Architecture.Armv6 => 12,
Architecture.S390x => 15,
Architecture.Ppc64le => 16,
Architecture.RiscV64 => 22,
Architecture.LoongArch64 => 23,
_ => throw new ArgumentException("Unsupported architecture", nameof(architecture))
};
}
public static string GetRpmHeaderArchitecture(Architecture architecture)
{
// See /usr/lib/rpm/rpmrc for valida architecture values
return architecture switch
{
Architecture.X86 => "i686",
Architecture.X64 => "x86_64",
Architecture.Arm => "armv7hl",
Architecture.Arm64 => "aarch64",
Architecture.Armv6 => "armv6hl",
Architecture.S390x => "s390x",
Architecture.Ppc64le => "ppc64le",
Architecture.RiscV64 => "riscv64",
Architecture.LoongArch64 => "loongarch64",
_ => throw new ArgumentException("Unsupported architecture", nameof(architecture))
};
}
public static string GetDotNetArchitectureFromRpmHeaderArchitecture(string rpmPackageArchitecture)
{
return rpmPackageArchitecture switch
{
"noarch" => "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
if (os.Equals(OSPlatform.Linux))
{
return 1;
}
else if (os.Equals(OSPlatform.Create("FREEBSD")))
{
return 8;
}
else
{
throw new ArgumentException("Unsupported OS", nameof(os));
}
}
public void AddProvidedCapability(string capability, string version)
{
_provides.Add((capability, version));
}
public void AddConflict(string name)
{
_conflicts.Add(name);
}
public void AddRequiredCapability(string capability, string version)
{
if (string.IsNullOrEmpty(version))
{
_requires.Add((capability, 0x0, ""));
}
else
{
_requires.Add((capability, 0xC, version));
}
}
public void AddChangelogLine(string name, string text)
{
_changelogLines.Add((name, text));
}
private readonly List<(CpioEntry file, string fileKind, bool isGhost)> _files = [];
public void AddFile(CpioEntry file, string fileKind)
{
_files.Add((file, fileKind, isGhost: false));
}
/// <summary>
/// Adds a "ghost" file: a path that the package owns but does not ship in the payload.
/// Ghost files get an RPM file header record (with the <c>RPMFILE_GHOST</c> flag) but are
/// omitted from the CPIO archive. They are typically materialized/repaired by a scriptlet
/// or file trigger. See <c>%ghost</c> in the RPM spec documentation.
/// </summary>
public void AddGhostFile(CpioEntry file, string fileKind)
{
_files.Add((file, fileKind, isGhost: true));
}
private readonly List<(string script, int senseFlags, IReadOnlyList<string> paths)> _fileTriggers = [];
private readonly List<(string script, int senseFlags, IReadOnlyList<string> paths)> _transFileTriggers = [];
/// <summary>
/// Adds a file trigger scriptlet that runs when any package on the system installs, upgrades,
/// or removes a file under one of the specified <paramref name="paths"/> prefixes.
/// </summary>
/// <param name="kind">
/// One of <c>FileTriggerIn</c>, <c>FileTriggerUn</c>, <c>FileTriggerPostUn</c>,
/// <c>TransFileTriggerIn</c>, <c>TransFileTriggerUn</c>, or <c>TransFileTriggerPostUn</c>.
/// </param>
/// <param name="script">The shell script body to execute.</param>
/// <param name="paths">The file path prefixes that arm the trigger. Must be non-empty.</param>
public void AddFileTrigger(string kind, string script, IReadOnlyList<string> paths)
{
(int senseFlags, bool isTransactionTrigger) = kind switch
{
"FileTriggerIn" => (RpmSenseTriggerIn, false),
"FileTriggerUn" => (RpmSenseTriggerUn, false),
"FileTriggerPostUn" => (RpmSenseTriggerPostUn, false),
"TransFileTriggerIn" => (RpmSenseTriggerIn, true),
"TransFileTriggerUn" => (RpmSenseTriggerUn, true),
"TransFileTriggerPostUn" => (RpmSenseTriggerPostUn, true),
_ => throw new ArgumentException(
$"Unknown file trigger kind: '{kind}'. Expected 'FileTriggerIn', 'FileTriggerUn', " +
"'FileTriggerPostUn', 'TransFileTriggerIn', 'TransFileTriggerUn', or 'TransFileTriggerPostUn'.",
nameof(kind))
};
if (paths is null || paths.Count == 0)
{
throw new ArgumentException("A file trigger must specify at least one path.", nameof(paths));
}
(isTransactionTrigger ? _transFileTriggers : _fileTriggers).Add((script, senseFlags, paths));
}
private readonly Dictionary<string, string> _scripts = [];
public void AddScript(string kind, string script)
{
_scripts.Add(kind, script);
}
public string Url { get; set; } = "";
public string Vendor { get; set; } = "";
public string License { get; set; } = "";
public string Packager { get; set; } = "";
public string Summary { get; set; } = "";
public string Description { get; set; } = "";
private static readonly DateTimeOffset UnixEpoch = new(1970, 1, 1, 0, 0, 0, TimeSpan.Zero);
internal static readonly int[] Sha256DigestAlgorithmValue = new[] { 8 };
// RPM file flags (see rpmfiles.h).
private const int RpmFileDoc = 1 << 1; // RPMFILE_DOC
private const int RpmFileGhost = 1 << 6; // RPMFILE_GHOST: owned by the package but not shipped in the payload.
// RPM dependency sense flags (see rpmds.h) used to classify file triggers.
private const int RpmSenseTriggerIn = 1 << 16; // RPMSENSE_TRIGGERIN
private const int RpmSenseTriggerUn = 1 << 17; // RPMSENSE_TRIGGERUN
private const int RpmSenseTriggerPostUn = 1 << 18; // RPMSENSE_TRIGGERPOSTUN
// Default priority rpm assigns to a file trigger when none is specified (RPMTRIGGER_DEFAULT_PRIORITY).
private const int RpmDefaultFileTriggerPriority = 1000000;
private static void AddFileTriggerEntries(
List<RpmHeader<RpmHeaderTag>.Entry> entries,
List<(string script, int senseFlags, IReadOnlyList<string> paths)> triggers,
bool isTransactionTrigger)
{
if (triggers.Count == 0)
{
return;
}
// RPM stores trigger scriptlets and their matching path conditions in parallel arrays.
List<string> triggerScripts = [];
List<string> triggerScriptProgs = [];
List<int> triggerScriptFlags = [];
List<int> triggerPriorities = [];
List<string> triggerNames = [];
List<string> triggerVersions = [];
List<int> triggerFlags = [];
List<int> triggerIndices = [];
for (int tix = 0; tix < triggers.Count; tix++)
{
(string script, int senseFlags, IReadOnlyList<string> paths) = triggers[tix];
triggerScripts.Add(script);
triggerScriptProgs.Add("/bin/sh");
triggerScriptFlags.Add(0);
triggerPriorities.Add(RpmDefaultFileTriggerPriority);
foreach (string path in paths)
{
triggerNames.Add(path);
triggerVersions.Add("");
triggerFlags.Add(senseFlags);
triggerIndices.Add(tix);
}
}
entries.Add(new(
isTransactionTrigger ? RpmHeaderTag.TransFileTriggerScripts : RpmHeaderTag.FileTriggerScripts,
RpmHeaderEntryType.StringArray,
triggerScripts.ToArray()));
entries.Add(new(
isTransactionTrigger ? RpmHeaderTag.TransFileTriggerScriptProg : RpmHeaderTag.FileTriggerScriptProg,
RpmHeaderEntryType.StringArray,
triggerScriptProgs.ToArray()));
entries.Add(new(
isTransactionTrigger ? RpmHeaderTag.TransFileTriggerScriptFlags : RpmHeaderTag.FileTriggerScriptFlags,
RpmHeaderEntryType.Int32,
triggerScriptFlags.ToArray()));
entries.Add(new(
isTransactionTrigger ? RpmHeaderTag.TransFileTriggerPriorities : RpmHeaderTag.FileTriggerPriorities,
RpmHeaderEntryType.Int32,
triggerPriorities.ToArray()));
entries.Add(new(
isTransactionTrigger ? RpmHeaderTag.TransFileTriggerName : RpmHeaderTag.FileTriggerName,
RpmHeaderEntryType.StringArray,
triggerNames.ToArray()));
entries.Add(new(
isTransactionTrigger ? RpmHeaderTag.TransFileTriggerVersion : RpmHeaderTag.FileTriggerVersion,
RpmHeaderEntryType.StringArray,
triggerVersions.ToArray()));
entries.Add(new(
isTransactionTrigger ? RpmHeaderTag.TransFileTriggerFlags : RpmHeaderTag.FileTriggerFlags,
RpmHeaderEntryType.Int32,
triggerFlags.ToArray()));
entries.Add(new(
isTransactionTrigger ? RpmHeaderTag.TransFileTriggerIndex : RpmHeaderTag.FileTriggerIndex,
RpmHeaderEntryType.Int32,
triggerIndices.ToArray()));
}
public RpmPackage Build()
{
// Build the header first.
List<RpmHeader<RpmHeaderTag>.Entry> entries = [..PackageEntries];
if (_provides.Count != 0)
{
entries.Add(new(RpmHeaderTag.ProvideName, RpmHeaderEntryType.StringArray, _provides.Select(p => p.capability).ToArray()));
entries.Add(new(RpmHeaderTag.ProvideVersion, RpmHeaderEntryType.StringArray, _provides.Select(p => p.version).ToArray()));
entries.Add(new(RpmHeaderTag.ProvideFlags, RpmHeaderEntryType.Int32, _provides.Select(_ => 0).ToArray()));
}
if (_conflicts.Count != 0)
{
entries.Add(new(RpmHeaderTag.ConflictName, RpmHeaderEntryType.StringArray, _conflicts.ToArray()));
entries.Add(new(RpmHeaderTag.ConflictFlags, RpmHeaderEntryType.Int32, _conflicts.Select(_ => 0).ToArray()));
entries.Add(new(RpmHeaderTag.ConflictVersion, RpmHeaderEntryType.StringArray, _conflicts.Select(_ => "").ToArray()));
}
if (_requires.Count != 0)
{
entries.Add(new(RpmHeaderTag.RequireName, RpmHeaderEntryType.StringArray, _requires.Select(r => r.name).ToArray()));
entries.Add(new(RpmHeaderTag.RequireVersion, RpmHeaderEntryType.StringArray, _requires.Select(r => r.version).ToArray()));
entries.Add(new(RpmHeaderTag.RequireFlags, RpmHeaderEntryType.Int32, _requires.Select(r => r.flags).ToArray()));
}
if (_changelogLines.Count != 0)
{
entries.Add(new(RpmHeaderTag.ChangelogName, RpmHeaderEntryType.StringArray, _changelogLines.Select(l => l.name).ToArray()));
entries.Add(new(RpmHeaderTag.ChangelogText, RpmHeaderEntryType.StringArray, _changelogLines.Select(l => l.text).ToArray()));
entries.Add(new(RpmHeaderTag.ChangelogText, RpmHeaderEntryType.StringArray, _changelogLines.Select(l => l.text).ToArray()));
entries.Add(new(RpmHeaderTag.ChangelogTimestamp, RpmHeaderEntryType.Int32, _changelogLines.Select(_ => (int)(DateTimeOffset.UtcNow - UnixEpoch).TotalSeconds).ToArray()));
}
entries.Add(new(RpmHeaderTag.BuildTime, RpmHeaderEntryType.Int32, new[] { (int)(DateTimeOffset.UtcNow - UnixEpoch).TotalSeconds }));
entries.Add(new(RpmHeaderTag.Prefixes, RpmHeaderEntryType.StringArray, new[] { "/" }));
entries.Add(new(RpmHeaderTag.Vendor, RpmHeaderEntryType.String, Vendor));
entries.Add(new(RpmHeaderTag.License, RpmHeaderEntryType.String, License));
entries.Add(new(RpmHeaderTag.Packager, RpmHeaderEntryType.String, Packager));
entries.Add(new(RpmHeaderTag.Url, RpmHeaderEntryType.String, Url));
entries.Add(new(RpmHeaderTag.Summary, RpmHeaderEntryType.I18NString, Summary));
entries.Add(new(RpmHeaderTag.Description, RpmHeaderEntryType.I18NString, Description));
foreach (var script in _scripts)
{
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"));
}
AddFileTriggerEntries(entries, _fileTriggers, isTransactionTrigger: false);
AddFileTriggerEntries(entries, _transFileTriggers, isTransactionTrigger: true);
MemoryStream cpioArchive = new();
using (CpioWriter writer = new(cpioArchive, leaveOpen: true))
using (SHA256 sha256 = SHA256.Create())
{
List<string> fileDigests = [];
List<string> baseNames = [];
List<int> directoryNameIndices = [];
List<string> directories = [];
List<int> fileClassIndices = [];
List<string> fileClassDictionary = [];
List<int> inodes = [];
List<int> fileSizes = [];
List<string> fileUserAndGroupNames = [];
List<short> fileModes = [];
List<short> deviceFileIds = [];
List<int> fileTimestamps = [];
List<int> fileVerifyFlags = [];
List<int> fileDevices = [];
List<string> fileLangs = [];
List<int> fileColors = [];
List<int> fileFlags = [];
List<string> fileLinkTos = [];
int installedSize = 0;
entries.Add(new(RpmHeaderTag.FileDigestAlgorithm, RpmHeaderEntryType.Int32, Sha256DigestAlgorithmValue));
foreach ((CpioEntry file, string fileKind, bool isGhost) in _files)
{
// Ghost files are owned by the package but not shipped in the payload, so they
// must not be written to the CPIO archive. They still contribute a file header record.
if (!isGhost)
{
writer.WriteNextEntry(file);
}
file.DataStream.Position = 0;
// If the entry is a regular file, compute its digest.
// Ghost files ship no content, so their digest is always empty.
if (!isGhost && (file.Mode & CpioEntry.FileKindMask) == CpioEntry.RegularFile)
{
fileDigests.Add(HexConverter.ToHexStringLower(sha256.ComputeHash(file.DataStream)));
file.DataStream.Position = 0;
}
else
{
// Otherwise the digest is an empty string.
fileDigests.Add("");
}
if ((file.Mode & CpioEntry.FileKindMask) == CpioEntry.SymbolicLink)
{
// For symbolic links, the contents of the file is the link target.
using StreamReader reader = new(file.DataStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: -1, leaveOpen: true);
fileLinkTos.Add(reader.ReadToEnd().TrimEnd());
file.DataStream.Position = 0;
}
else
{
fileLinkTos.Add("");
}
baseNames.Add(Path.GetFileName(file.Name));
string dirName = Path.GetDirectoryName(file.Name)!;
if (dirName.StartsWith("./"))
{
// The cpio entries must have './', but the RPM header entries must have
// the actual (non-relative) pathing.
dirName = dirName.Substring(1);
}
// RPM requires the directory names to end with the directory separator.
dirName += '/';
int directoryIndex = directories.IndexOf(dirName);
if (directoryIndex == -1)
{
directoryIndex = directories.Count;
directories.Add(dirName);
}
directoryNameIndices.Add(directoryIndex);
int fileClassIndex = fileClassDictionary.IndexOf(fileKind);
if (fileClassIndex == -1)
{
fileClassIndex = fileClassDictionary.Count;
fileClassDictionary.Add(fileKind);
}
fileClassIndices.Add(fileClassIndex);
inodes.Add((int)file.Inode);
installedSize += (int)file.DataStream.Length;
fileSizes.Add((int)file.DataStream.Length);
fileUserAndGroupNames.Add("root");
fileModes.Add((short)file.Mode);
deviceFileIds.Add(0);
fileTimestamps.Add((int)file.Timestamp);
fileVerifyFlags.Add(-1);
fileDevices.Add(1);
fileLangs.Add("");
if (fileKind.Contains("ELF 64-bit LSB"))
{
fileColors.Add(2);
}
else if (fileKind.Contains("ELF 32-bit LSB"))
{
fileColors.Add(1);
}
else
{
fileColors.Add(0);
}
if (file.Name.StartsWith("usr/share/doc") && Path.GetFileName(file.Name) == "copyright")
{
// Treat the copyright file as though it came from the %%doc section in an RPM spec file.
fileFlags.Add(isGhost ? RpmFileDoc | RpmFileGhost : RpmFileDoc);
}
else
{
fileFlags.Add(isGhost ? RpmFileGhost : 0);
}
}
entries.Add(new(RpmHeaderTag.FileDigests, RpmHeaderEntryType.StringArray, fileDigests.ToArray()));
entries.Add(new(RpmHeaderTag.BaseNames, RpmHeaderEntryType.StringArray, baseNames.ToArray()));
entries.Add(new(RpmHeaderTag.DirectoryNameIndices, RpmHeaderEntryType.Int32, directoryNameIndices.ToArray()));
entries.Add(new(RpmHeaderTag.DirectoryNames, RpmHeaderEntryType.StringArray, directories.ToArray()));
entries.Add(new(RpmHeaderTag.FileClass, RpmHeaderEntryType.Int32, fileClassIndices.ToArray()));
entries.Add(new(RpmHeaderTag.FileClassDictionary, RpmHeaderEntryType.StringArray, fileClassDictionary.ToArray()));
entries.Add(new(RpmHeaderTag.FileInode, RpmHeaderEntryType.Int32, inodes.ToArray()));
entries.Add(new(RpmHeaderTag.FileSizes, RpmHeaderEntryType.Int32, fileSizes.ToArray()));
entries.Add(new(RpmHeaderTag.FileUserName, RpmHeaderEntryType.StringArray, fileUserAndGroupNames.ToArray()));
entries.Add(new(RpmHeaderTag.FileGroupName, RpmHeaderEntryType.StringArray, fileUserAndGroupNames.ToArray()));
entries.Add(new(RpmHeaderTag.FileModes, RpmHeaderEntryType.Int16, fileModes.ToArray()));
entries.Add(new(RpmHeaderTag.DeviceFileIds, RpmHeaderEntryType.Int16, deviceFileIds.ToArray()));
entries.Add(new(RpmHeaderTag.FileModificationTimestamp, RpmHeaderEntryType.Int32, fileTimestamps.ToArray()));
entries.Add(new(RpmHeaderTag.FileVerifyFlags, RpmHeaderEntryType.Int32, fileVerifyFlags.ToArray()));
entries.Add(new(RpmHeaderTag.FileDevices, RpmHeaderEntryType.Int32, fileDevices.ToArray()));
entries.Add(new(RpmHeaderTag.FileLang, RpmHeaderEntryType.StringArray, fileLangs.ToArray()));
entries.Add(new(RpmHeaderTag.FileColors, RpmHeaderEntryType.Int32, fileColors.ToArray()));
entries.Add(new(RpmHeaderTag.InstalledSize, RpmHeaderEntryType.Int32, new[] { installedSize }));
entries.Add(new(RpmHeaderTag.FileFlags, RpmHeaderEntryType.Int32, fileFlags.ToArray()));
entries.Add(new(RpmHeaderTag.FileLinkTos, RpmHeaderEntryType.StringArray, fileLinkTos.ToArray()));
}
cpioArchive.Seek(0, SeekOrigin.Begin);
// TODO: Add more package-level header entries.
MemoryStream compressedPayload = new();
using (GZipStream gzipStream = new(compressedPayload, CompressionLevel.Optimal, leaveOpen: true))
{
cpioArchive.CopyTo(gzipStream);
}
cpioArchive.Seek(0, SeekOrigin.Begin);
compressedPayload.Seek(0, SeekOrigin.Begin);
using (SHA256 sha256 = SHA256.Create())
{
entries.Add(new(RpmHeaderTag.PayloadDigestAlgorithm, RpmHeaderEntryType.Int32, Sha256DigestAlgorithmValue));
entries.Add(new(RpmHeaderTag.CompressedPayloadDigest, RpmHeaderEntryType.StringArray, new string[] { HexConverter.ToHexStringLower(sha256.ComputeHash(compressedPayload)) }));
entries.Add(new(RpmHeaderTag.UncompressedPayloadDigest, RpmHeaderEntryType.StringArray, new string[] { HexConverter.ToHexStringLower(sha256.ComputeHash(cpioArchive)) }));
cpioArchive.Seek(0, SeekOrigin.Begin);
CpioReader reader = new(cpioArchive, leaveOpen: true);
}
MemoryStream headerStream = new();
RpmHeader<RpmHeaderTag> header = new(entries);
header.WriteTo(headerStream, RpmHeaderTag.Immutable);
headerStream.Seek(0, SeekOrigin.Begin);
cpioArchive.Seek(0, SeekOrigin.Begin);
List<RpmHeader<RpmSignatureTag>.Entry> signatureEntries = [
new(RpmSignatureTag.UncompressedPayloadSize, RpmHeaderEntryType.Int32, new[] { (int)cpioArchive.Length }),
new(RpmSignatureTag.HeaderAndPayloadSize, RpmHeaderEntryType.Int32, new[] { (int)headerStream.Length + (int)compressedPayload.Length }),
];
// Only include the "header" signature tags.
// RPM has removed the header+payload legacy tags in favor of the header-only tags + payload digests in the header in newer versions.
using (SHA1 sha1 = SHA1.Create())
{
signatureEntries.Add(new(RpmSignatureTag.Sha1Header, RpmHeaderEntryType.String, HexConverter.ToHexStringLower(sha1.ComputeHash(headerStream))));
headerStream.Seek(0, SeekOrigin.Begin);
}
using (SHA256 sha256 = SHA256.Create())
{
signatureEntries.Add(new(RpmSignatureTag.Sha256Header, RpmHeaderEntryType.String, HexConverter.ToHexStringLower(sha256.ComputeHash(headerStream))));
}
signatureEntries.Add(new(RpmSignatureTag.ReservedSpace, RpmHeaderEntryType.Binary, new ArraySegment<byte>(new byte[4128])));
RpmHeader<RpmSignatureTag> signature = new(signatureEntries);
return new RpmPackage(Lead with { Name = packageName }, signature, header, cpioArchive);
}
}
}