File: PseudoCustomAttributes.Decoding.cs
Web Access
Project: ILAssembler.csproj (ILAssembler)
// 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.Immutable;
using System.Reflection.Metadata;

namespace ILAssembler;

internal static partial class PseudoCustomAttributes
{
    private readonly record struct EncodedArgumentType(
        SerializationTypeCode Type,
        SerializationTypeCode ArrayType = SerializationTypeCode.Invalid,
        SerializationTypeCode EnumUnderlyingType = SerializationTypeCode.Invalid,
        string? EnumName = null);

    private static EncodedArgumentType ReadEncodedArgumentType(ref BlobReader reader)
    {
        SerializationTypeCode type = reader.ReadSerializationTypeCode();
        SerializationTypeCode arrayType = SerializationTypeCode.Invalid;
        if (type == SerializationTypeCode.SZArray)
        {
            arrayType = reader.ReadSerializationTypeCode();
        }

        SerializationTypeCode effectiveType =
            type == SerializationTypeCode.SZArray ? arrayType : type;
        if (effectiveType == SerializationTypeCode.Invalid)
        {
            throw new BadImageFormatException();
        }

        string? enumName = null;
        if (effectiveType == SerializationTypeCode.Enum)
        {
            enumName = reader.ReadSerializedString();
            if (enumName is null)
            {
                throw new BadImageFormatException();
            }
        }

        return new(type, arrayType, EnumName: enumName);
    }

    private static CustomAttributeTypedArgument<SerializationTypeCode> ReadArgument(
        ref BlobReader reader,
        SerializationTypeCode type,
        SerializationTypeCode enumUnderlyingType = SerializationTypeCode.Invalid)
    {
        SerializationTypeCode effectiveType =
            type == SerializationTypeCode.Enum ? enumUnderlyingType : type;

        object? value = effectiveType switch
        {
            SerializationTypeCode.Boolean => reader.ReadBoolean(),
            SerializationTypeCode.SByte => reader.ReadSByte(),
            SerializationTypeCode.Byte => reader.ReadByte(),
            SerializationTypeCode.Char => reader.ReadChar(),
            SerializationTypeCode.Int16 => reader.ReadInt16(),
            SerializationTypeCode.UInt16 => reader.ReadUInt16(),
            SerializationTypeCode.Int32 => reader.ReadInt32(),
            SerializationTypeCode.UInt32 => reader.ReadUInt32(),
            SerializationTypeCode.Int64 => reader.ReadInt64(),
            SerializationTypeCode.UInt64 => reader.ReadUInt64(),
            SerializationTypeCode.Single => reader.ReadSingle(),
            SerializationTypeCode.Double => reader.ReadDouble(),
            SerializationTypeCode.String or SerializationTypeCode.Type => reader.ReadSerializedString(),
            _ => throw new BadImageFormatException(),
        };

        return new(type, value);
    }

    private static unsafe bool TryParseArguments(
        LoweringContext context,
        KnownAttribute known,
        out CustomAttributeValue<SerializationTypeCode> arguments)
    {
        byte[] blob = context.Attribute.Value.ToArray();
        fixed (byte* blobPointer = blob)
        {
            var reader = new BlobReader(blobPointer, blob.Length);
            try
            {
                if (reader.ReadUInt16() != 0x0001)
                {
                    arguments = default;
                    return context.InvalidBlob();
                }

                var fixedArguments =
                    ImmutableArray.CreateBuilder<CustomAttributeTypedArgument<SerializationTypeCode>>(
                        known.FixedArguments.Length);
                foreach (SerializationTypeCode type in known.FixedArguments)
                {
                    fixedArguments.Add(ReadArgument(ref reader, type));
                }

                if (!TryParseNamedArguments(context, known, ref reader,
                    out ImmutableArray<CustomAttributeNamedArgument<SerializationTypeCode>> namedArguments))
                {
                    arguments = default;
                    return false;
                }

                if (reader.RemainingBytes != 0)
                {
                    arguments = default;
                    return context.InvalidBlob();
                }

                arguments = new(fixedArguments.MoveToImmutable(), namedArguments);
                return true;
            }
            catch (BadImageFormatException)
            {
                arguments = default;
                return context.InvalidBlob();
            }
        }
    }

    private static bool TryParseNamedArguments(
        LoweringContext context,
        KnownAttribute known,
        ref BlobReader reader,
        out ImmutableArray<CustomAttributeNamedArgument<SerializationTypeCode>> namedArguments)
    {
        ushort actualCount = reader.ReadUInt16();
        var arguments =
            ImmutableArray.CreateBuilder<CustomAttributeNamedArgument<SerializationTypeCode>>(
                Math.Min(actualCount, (ushort)known.NamedArguments.Length));
        var seenArguments = new bool[known.NamedArguments.Length];

        for (int i = 0; i < actualCount; i++)
        {
            var kind = (CustomAttributeNamedArgumentKind)reader.ReadSerializationTypeCode();
            if (kind is not (CustomAttributeNamedArgumentKind.Field or CustomAttributeNamedArgumentKind.Property))
            {
                namedArguments = default;
                return context.InvalidBlob();
            }

            EncodedArgumentType actual = ReadEncodedArgumentType(ref reader);
            string? argumentName = reader.ReadSerializedString();
            if (string.IsNullOrEmpty(argumentName))
            {
                namedArguments = default;
                return context.InvalidBlob();
            }

            int match = -1;
            for (int candidate = 0; candidate < known.NamedArguments.Length; candidate++)
            {
                NamedArgument descriptor = known.NamedArguments[candidate];

                if (descriptor.Type != SerializationTypeCode.TaggedObject)
                {
                    if (actual.Type != descriptor.Type)
                    {
                        continue;
                    }

                    if (actual.Type == SerializationTypeCode.SZArray
                        && descriptor.ArrayType != SerializationTypeCode.TaggedObject
                        && actual.ArrayType != descriptor.ArrayType)
                    {
                        continue;
                    }
                }

                if (descriptor.Name != argumentName)
                {
                    continue;
                }

                if (descriptor.Type == SerializationTypeCode.Enum
                    || (descriptor.Type == SerializationTypeCode.SZArray && descriptor.ArrayType == SerializationTypeCode.Enum))
                {
                    if (!EnumNameMatches(descriptor.EnumName, actual.EnumName))
                    {
                        continue;
                    }

                    actual = actual with { EnumUnderlyingType = descriptor.EnumType };
                }

                match = candidate;
                break;
            }

            if (match < 0)
            {
                namedArguments = default;
                return context.UnknownArgument(argumentName);
            }

            if (seenArguments[match])
            {
                namedArguments = default;
                return context.RepeatedArgument(argumentName);
            }

            seenArguments[match] = true;
            CustomAttributeTypedArgument<SerializationTypeCode> value =
                ReadArgument(ref reader, actual.Type, actual.EnumUnderlyingType);
            arguments.Add(new(argumentName, kind, value.Type, value.Value));
        }

        namedArguments = arguments.ToImmutable();
        return true;
    }

    private static CustomAttributeNamedArgument<SerializationTypeCode>? FindNamedArgument(
        CustomAttributeValue<SerializationTypeCode> arguments,
        string name)
    {
        foreach (CustomAttributeNamedArgument<SerializationTypeCode> argument in arguments.NamedArguments)
        {
            if (argument.Name == name)
            {
                return argument;
            }
        }

        return null;
    }

    private static short GetInt16(object? value) => value switch
    {
        short signed => signed,
        ushort unsigned => unchecked((short)unsigned),
        _ => throw new BadImageFormatException(),
    };

    private static int GetInt32(object? value) => value switch
    {
        short signed => unchecked((ushort)signed),
        ushort unsigned => unsigned,
        int signed => signed,
        uint unsigned => unchecked((int)unsigned),
        _ => throw new BadImageFormatException(),
    };

    private static bool GetBoolean(object? value) =>
        value is bool boolean ? boolean : throw new BadImageFormatException();

    private static string GetString(object? value) => value as string ?? "";

    /// <summary>
    /// Matches an enum type name against a descriptor name, allowing the blob to carry an
    /// assembly-qualified name whose namespace-qualified prefix matches.
    /// </summary>
    private static bool EnumNameMatches(string descriptorName, string? actualName)
    {
        if (actualName is null || descriptorName.Length > actualName.Length)
        {
            return false;
        }

        if (!actualName.AsSpan(0, descriptorName.Length).SequenceEqual(descriptorName))
        {
            return false;
        }

        return descriptorName.Length == actualName.Length || actualName[descriptorName.Length] == ',';
    }
}