File: src\RpmHeader.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.Buffers.Binary;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace Microsoft.DotNet.Build.Tasks.Installers;

public sealed partial class RpmHeader<TEntryTag>(List<RpmHeader<TEntryTag>.Entry> entries)
    where TEntryTag : struct, Enum
{

    [StructLayout(LayoutKind.Sequential)]
    private unsafe struct IndexEntry
    {
        public int Tag;
        public RpmHeaderEntryType Type;
        public int Offset;
        public int Count;
    }

    public readonly struct Entry(TEntryTag tag, RpmHeaderEntryType type, object value)
    {
        public Entry(int entryTag, RpmHeaderEntryType type, object value)
            : this((TEntryTag)Enum.ToObject(typeof(TEntryTag), entryTag), type, value)
        {
        }

        public TEntryTag Tag { get; } = tag;
        public RpmHeaderEntryType Type { get; } = type;
        public object Value { get; } = value;
    }

    public List<Entry> Entries { get; } = entries;

    public override string ToString()
    {
        StringBuilder builder = new();
        foreach (Entry entry in Entries)
        {
            builder.AppendLine($"\tTag: {entry.Tag}, Type: {entry.Type}");
            if (entry.Value is string str)
            {
                builder.AppendLine($"\t\t'{str}'");
            }
            else if (entry.Type == RpmHeaderEntryType.Binary)
            {
                builder.AppendLine($"\t\t{((ArraySegment<byte>)entry.Value).Count} bytes");
                builder.AppendLine($"\t\t{BitConverter.ToString(((ArraySegment<byte>)entry.Value).Array!, ((ArraySegment<byte>)entry.Value).Offset, ((ArraySegment<byte>)entry.Value).Count)}");
            }
            else if (entry.Value is IEnumerable collection)
            {
                foreach (object item in collection)
                {
                    builder.AppendLine($"\t\t- '{item}'");
                }
            }
            else
            {
                builder.AppendLine($"\t\t{entry.Value}");
            }
        }
        return builder.ToString();
    }

    private static IndexEntry ReadIndexEntry(ReadOnlySpan<byte> bytes)
    {
        return new IndexEntry()
        {
            Tag = BinaryPrimitives.ReadInt32BigEndian(bytes),
            Type = (RpmHeaderEntryType)BinaryPrimitives.ReadInt32BigEndian(bytes.Slice(4)),
            Offset = BinaryPrimitives.ReadInt32BigEndian(bytes.Slice(8)),
            Count = BinaryPrimitives.ReadInt32BigEndian(bytes.Slice(12))
        };
    }

    private static void WriteIndexEntry(Span<byte> bytes, IndexEntry entry)
    {
        BinaryPrimitives.WriteInt32BigEndian(bytes, entry.Tag);
        BinaryPrimitives.WriteInt32BigEndian(bytes.Slice(4), (int)entry.Type);
        BinaryPrimitives.WriteInt32BigEndian(bytes.Slice(8), entry.Offset);
        BinaryPrimitives.WriteInt32BigEndian(bytes.Slice(12), entry.Count);
    }

    public static unsafe RpmHeader<TEntryTag> Read(Stream stream, TEntryTag immutableRegionTag)
    {
        if (!stream.ReadExactly(3).SequenceEqual((ReadOnlySpan<byte>)[0x8e, 0xad, 0xe8 ]))
        {
            throw new InvalidDataException("Invalid RPM header magic");
        }

        byte version = (byte)stream.ReadByte();

        if (version != 1)
        {
            throw new InvalidDataException("Unsupported RPM header version");
        }

        if (!stream.ReadExactly(4).SequenceEqual((ReadOnlySpan<byte>)[ 0x00, 0x00, 0x00, 0x00 ]))
        {
            throw new InvalidDataException("Invalid RPM header reserved bytes");
        }

        int numIndexEntries = BinaryPrimitives.ReadInt32BigEndian(stream.ReadExactly(4));
        int numHeaderBytes = BinaryPrimitives.ReadInt32BigEndian(stream.ReadExactly(4));

        byte[] indexBytes = new byte[sizeof(IndexEntry) * numIndexEntries];
        stream.ReadExactly(indexBytes, 0, indexBytes.Length);
        Span<byte> indexBytesSpan = indexBytes;
        IndexEntry[] indexEntries = new IndexEntry[numIndexEntries];
        for (int i = 0; i < numIndexEntries; i++)
        {
            indexEntries[i] = ReadIndexEntry(indexBytesSpan.Slice(i * sizeof(IndexEntry)));
        }

        byte[] store = new byte[numHeaderBytes];
        stream.ReadExactly(store, 0, store.Length);

        List<Entry> entries = [];

        foreach (var entry in indexEntries)
        {
            // Check the "immutable tag" and validate it if it exists.
            // We don't persist this tag as we need to generate it during emit.
            if (entry.Tag.Equals(Convert.ToInt32(immutableRegionTag)))
            {
                IndexEntry indexEntry = ReadIndexEntry(store.AsSpan().Slice(entry.Offset));
                if (entry.Tag != indexEntry.Tag
                    || !indexEntry.Type.Equals(RpmHeaderEntryType.Binary)
                    || indexEntry.Count != 16)
                {
                    // Don't validate the size of the immutable region as it may not match the size of the header
                    // (i.e. if the package has been signed)
                    throw new InvalidOperationException("Invalid immutable region tag");
                }
                continue;
            }

            if (entry.Type is RpmHeaderEntryType.Binary)
            {
                entries.Add(new Entry(entry.Tag, entry.Type, new ArraySegment<byte>(store, entry.Offset, entry.Count)));
            }
            else if (entry.Type is RpmHeaderEntryType.Null)
            {
                throw new InvalidOperationException("Null entry should not be present in RPM header");
            }
            else if (entry.Type is RpmHeaderEntryType.String or RpmHeaderEntryType.I18NString)
            {
                int offset = entry.Offset;
                int nullTerminatorIndex = Array.IndexOf(store, (byte)0, offset);
                if (nullTerminatorIndex == -1)
                {
                    throw new InvalidOperationException("Invalid RPM header entry string (no null terminator found)");
                }
                int length = nullTerminatorIndex - offset;
                entries.Add(new Entry(entry.Tag, entry.Type, Encoding.UTF8.GetString(store, entry.Offset, length)));
            }
            else if (entry.Type is RpmHeaderEntryType.StringArray)
            {
                int offset = entry.Offset;
                string[] strings = new string[entry.Count];
                for (int i = 0; i < entry.Count; i++)
                {
                    int nullTerminatorIndex = Array.IndexOf(store, (byte)0, offset);
                    if (nullTerminatorIndex == -1)
                    {
                        throw new InvalidOperationException("Invalid RPM header entry string (no null terminator found)");
                    }
                    int length = nullTerminatorIndex - offset;
                    strings[i] = Encoding.UTF8.GetString(store, offset, length);
                    offset += length + 1;
                }
                entries.Add(new Entry(entry.Tag, entry.Type, strings));
            }
            else
            {
                Array contents = Array.CreateInstance(entry.Type switch
                {
                    RpmHeaderEntryType.Char => typeof(char),
                    RpmHeaderEntryType.Int8 => typeof(byte),
                    RpmHeaderEntryType.Int16 => typeof(short),
                    RpmHeaderEntryType.Int32 => typeof(int),
                    RpmHeaderEntryType.Int64 => typeof(long),
                    _ => throw new InvalidOperationException("Invalid RPM header entry type")
                }, entry.Count);

                int offset = entry.Offset;

                for (int i = 0; i < entry.Count; i++)
                {
                    switch (entry.Type)
                    {
                        case RpmHeaderEntryType.Char:
                            contents.SetValue((char)store[offset], i);
                            offset += 1;
                            break;
                        case RpmHeaderEntryType.Int8:
                            contents.SetValue(store[offset], i);
                            offset += 1;
                            break;
                        case RpmHeaderEntryType.Int16:
                            contents.SetValue(BinaryPrimitives.ReadInt16BigEndian(store.AsSpan().Slice(offset.AlignUp(2), 2)), i);
                            offset += 2;
                            break;

                        case RpmHeaderEntryType.Int32:
                            contents.SetValue(BinaryPrimitives.ReadInt32BigEndian(store.AsSpan().Slice(offset.AlignUp(4), 4)), i);
                            offset += 4;
                            break;

                        case RpmHeaderEntryType.Int64:
                            contents.SetValue(BinaryPrimitives.ReadInt64BigEndian(store.AsSpan().Slice(offset.AlignUp(8), 8)), i);
                            offset += 8;
                            break;
                    }
                }

                entries.Add(new Entry(entry.Tag, entry.Type, contents));
            }
        }

        return new RpmHeader<TEntryTag>(entries);
    }

    public void WriteTo(Stream stream, TEntryTag immutableRegionTag)
    {
        stream.Write([0x8e, 0xad, 0xe8]); // magic
        stream.WriteByte(1); // version
        stream.Write([0, 0, 0, 0]); // reserved
        byte[] numIndexEntries = new byte[4];
        // Add 1 for the immutable region tag
        BinaryPrimitives.WriteInt32BigEndian(numIndexEntries, Entries.Count + 1);
        stream.Write(numIndexEntries);

        MemoryStream indexStream = new();

        using MemoryStream storeStream = new();

        byte[] indexInfoBytes = new byte[16];

        foreach (var entry in Entries)
        {
            BinaryPrimitives.WriteInt32BigEndian(indexInfoBytes, Convert.ToInt32(entry.Tag));
            BinaryPrimitives.WriteInt32BigEndian(indexInfoBytes.AsSpan(4), (int)entry.Type);
            if (entry.Type is RpmHeaderEntryType.Binary)
            {
                BinaryPrimitives.WriteInt32BigEndian(indexInfoBytes.AsSpan(8), (int)storeStream.Length);

                ArraySegment<byte> binary = (ArraySegment<byte>)entry.Value;
                storeStream.Write(binary.Array!, binary.Offset, binary.Count);
                
                BinaryPrimitives.WriteInt32BigEndian(indexInfoBytes.AsSpan(12), binary.Count);
            }
            else if (entry.Type is RpmHeaderEntryType.Null)
            {
                throw new InvalidOperationException("Null entry should not be present in RPM header");
            }
            else if (entry.Type is RpmHeaderEntryType.String or RpmHeaderEntryType.I18NString)
            {
                BinaryPrimitives.WriteInt32BigEndian(indexInfoBytes.AsSpan(8), (int)storeStream.Length);
                BinaryPrimitives.WriteInt32BigEndian(indexInfoBytes.AsSpan(12), 1);

                byte[] bytes = Encoding.UTF8.GetBytes((string)entry.Value);
                storeStream.Write(bytes);
                storeStream.WriteByte(0);

            }
            else if (entry.Type is RpmHeaderEntryType.StringArray)
            {
                BinaryPrimitives.WriteInt32BigEndian(indexInfoBytes.AsSpan(8), (int)storeStream.Length);

                string[] strings = (string[])entry.Value;

                BinaryPrimitives.WriteInt32BigEndian(indexInfoBytes.AsSpan(12), strings.Length);

                foreach (string str in strings)
                {
                    byte[] bytes = Encoding.UTF8.GetBytes(str);
                    storeStream.Write(bytes);
                    storeStream.WriteByte(0);
                }
            }
            else
            {
                int alignment = entry.Type switch
                {
                    RpmHeaderEntryType.Char => 1,
                    RpmHeaderEntryType.Int8 => 1,
                    RpmHeaderEntryType.Int16 => 2,
                    RpmHeaderEntryType.Int32 => 4,
                    RpmHeaderEntryType.Int64 => 8,
                    _ => throw new InvalidOperationException("Invalid RPM header entry type")
                };

                storeStream.AlignWriteTo(alignment);
                BinaryPrimitives.WriteInt32BigEndian(indexInfoBytes.AsSpan(8), (int)storeStream.Length);

                Array contents = (Array)entry.Value;

                BinaryPrimitives.WriteInt32BigEndian(indexInfoBytes.AsSpan(12), contents.Length);

                byte[] tempBeBytes = new byte[8];
                for (int i = 0; i < contents.Length; i++)
                {
                    switch (entry.Type)
                    {
                        case RpmHeaderEntryType.Char:
                            storeStream.WriteByte((byte)(char)contents.GetValue(i)!);
                            break;
                        case RpmHeaderEntryType.Int8:
                            storeStream.WriteByte((byte)contents.GetValue(i)!);
                            break;
                        case RpmHeaderEntryType.Int16:
                            BinaryPrimitives.WriteInt16BigEndian(tempBeBytes, (short)contents.GetValue(i)!);
                            storeStream.Write(tempBeBytes, 0, 2);
                            break;
                        case RpmHeaderEntryType.Int32:
                            BinaryPrimitives.WriteInt32BigEndian(tempBeBytes, (int)contents.GetValue(i)!);
                            storeStream.Write(tempBeBytes, 0, 4);
                            break;
                        case RpmHeaderEntryType.Int64:
                            BinaryPrimitives.WriteInt64BigEndian(tempBeBytes, (long)contents.GetValue(i)!);
                            storeStream.Write(tempBeBytes, 0, 8);
                            break;
                    }
                }
            }

            indexStream.Write(indexInfoBytes);
        }

        // Now that we've written all of the index entries, we can write the immutable region tags.
        byte[] immutableRegionIndexEntryBytes = new byte[16];
        IndexEntry immutableRegionIndexEntry = new()
        {
            Tag = Convert.ToInt32(immutableRegionTag),
            Type = RpmHeaderEntryType.Binary,
            Offset = (int)storeStream.Position,
            Count = 16
        };

        WriteIndexEntry(immutableRegionIndexEntryBytes, immutableRegionIndexEntry);

        IndexEntry immutableRegionTrailer = new()
        {
            Tag = Convert.ToInt32(immutableRegionTag),
            Type = RpmHeaderEntryType.Binary,
            Offset = -(Entries.Count + 1) * Unsafe.SizeOf<IndexEntry>(),
            Count = 16
        };

        WriteIndexEntry(indexInfoBytes, immutableRegionTrailer);
        storeStream.Write(indexInfoBytes);

        // Now that we've written the whole index and store, we can write the size of the store
        byte[] numHeaderBytes = new byte[4];
        BinaryPrimitives.WriteInt32BigEndian(numHeaderBytes, (int)storeStream.Length);
        stream.Write(numHeaderBytes);

        // Now write the immutable region index entry
        // at the start of the index.
        stream.Write(immutableRegionIndexEntryBytes);

        // Now write the rest of the index index and the entire store.
        indexStream.Position = 0;
        storeStream.Position = 0;
        indexStream.CopyTo(stream);
        storeStream.CopyTo(stream);
    }
}