File: src\RpmBuilder.cs
Web Access
Project: Microsoft.DotNet.Build.Tasks.Installers.csproj (Microsoft.DotNet.Build.Tasks.Installers)
// 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);
    }
}