File: NativeCommandLine.cs
Web Access
Project: ilasm.csproj (ilasm)
// 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.CommandLine;

namespace ILAssembler;

internal static class NativeCommandLine
{
    private static readonly Dictionary<string, string> s_options = new(StringComparer.OrdinalIgnoreCase)
    {
        ["32B"] = "--32bitpreferred",
        ["ALI"] = "--alignment",
        ["ANA"] = "--aname",
        ["APP"] = "--appcontainer",
        ["BAS"] = "--base",
        ["CLO"] = "--clock",
        ["DEB"] = "--debug",
        ["DET"] = "--deterministic",
        ["DLL"] = "--dll",
        ["ERR"] = "--error",
        ["EXE"] = "--exe",
        ["FLA"] = "--flags",
        ["FOL"] = "--fold",
        ["HIG"] = "--highentropyva",
        ["INC"] = "--include",
        ["KEY"] = "--key",
        ["MDV"] = "--mdv",
        ["NOA"] = "--noautoinherit",
        ["NOC"] = "--nocorstub",
        ["NOL"] = "--nologo",
        ["OPT"] = "--optimize",
        ["OUT"] = "--output",
        ["PDB"] = "--pdb",
        ["PE6"] = "--pe64",
        ["QUI"] = "--quiet",
        ["STA"] = "--stack",
        ["STR"] = "--stripreloc",
        ["SSV"] = "--ssver",
        ["SUB"] = "--subsystem",
        ["X64"] = "--x64",
    };

    internal static string[] Normalize(string[] args) =>
        Normalize(args, new IlasmRootCommand(), allowSlashOptions: OperatingSystem.IsWindows());

    internal static string[] Normalize(string[] args, bool allowSlashOptions)
        => Normalize(args, new IlasmRootCommand(), allowSlashOptions);

    internal static string[] Normalize(string[] args, IlasmRootCommand command)
        => Normalize(args, command, allowSlashOptions: OperatingSystem.IsWindows());

    private static string[] Normalize(string[] args, IlasmRootCommand command, bool allowSlashOptions)
    {
        HashSet<string> optionsWithValues = new(StringComparer.Ordinal);
        Dictionary<string, Option> modernShortOptions = new(StringComparer.Ordinal);
        foreach (Option option in command.Options)
        {
            AddModernOption(option.Name, option);
            foreach (string alias in option.Aliases)
            {
                AddModernOption(alias, option);
            }
        }

        List<string> result = new(args.Length);
        bool preserveRemainingArguments = false;
        bool preserveNextArgument = false;

        foreach (string arg in args)
        {
            if (preserveRemainingArguments || preserveNextArgument)
            {
                result.Add(arg);
                preserveNextArgument = false;
                continue;
            }

            if (arg == "--")
            {
                result.Add(arg);
                preserveRemainingArguments = true;
                continue;
            }

            if (optionsWithValues.Contains(arg))
            {
                result.Add(arg);
                preserveNextArgument = true;
                continue;
            }

            if (arg is "-?" || (allowSlashOptions && arg == "/?"))
            {
                result.Add("--help");
                continue;
            }

            if (!TrySplitNativeOption(arg, allowSlashOptions, out string optionName, out string? value))
            {
                result.Add(arg);
                continue;
            }

            if (optionName.Length < 3)
            {
                if (!IsValidModernShortOption(arg[0], optionName, value, modernShortOptions))
                {
                    throw new ArgumentException($"Invalid native option '{arg}'.");
                }

                result.Add(arg);
                continue;
            }

            string? normalizedOption;
            if (optionName.StartsWith("ARM64", StringComparison.OrdinalIgnoreCase))
            {
                normalizedOption = "--arm64";
            }
            else if (optionName.Equals("ARM", StringComparison.OrdinalIgnoreCase))
            {
                throw new ArgumentException(
                    $"Unsupported native option '{arg}'. ARM32 target images are not supported.");
            }
            else if (optionName.StartsWith("ARM", StringComparison.OrdinalIgnoreCase))
            {
                throw new ArgumentException($"Invalid native option '{arg}'.");
            }
            else
            {
                string prefix = optionName[..3];
                if (prefix.Equals("RES", StringComparison.OrdinalIgnoreCase))
                {
                    throw new ArgumentException(
                        $"Unsupported native option '{arg}'. The managed ilasm implementation does not support -RESOURCES.");
                }
                if (prefix.Equals("MSV", StringComparison.OrdinalIgnoreCase))
                {
                    throw new ArgumentException(
                        $"Unsupported native option '{arg}'. The managed ilasm implementation does not support -MSV.");
                }
                if (prefix.Equals("LIS", StringComparison.OrdinalIgnoreCase))
                {
                    continue;
                }

                s_options.TryGetValue(prefix, out normalizedOption);
            }

            if (normalizedOption is null)
            {
                if (IsValidModernShortOption(arg[0], optionName, value, modernShortOptions))
                {
                    result.Add(arg);
                    continue;
                }

                throw new ArgumentException($"Invalid native option '{arg}'.");
            }

            if (normalizedOption == "--debug" && value is not null)
            {
                if (string.IsNullOrWhiteSpace(value))
                {
                    throw new ArgumentException($"Invalid native option '{arg}'.");
                }

                result.Add("--debug");
                result.Add("--debug-mode");
                result.Add(NormalizeDebugMode(value));
                continue;
            }

            result.Add(normalizedOption);
            if (optionsWithValues.Contains(normalizedOption))
            {
                if (value is null)
                {
                    throw new ArgumentException($"Invalid native option '{arg}'.");
                }

                string normalizedValue = normalizedOption == "--aname" ? value : value.TrimStart();
                if (normalizedValue.Length == 0 && normalizedOption != "--aname")
                {
                    throw new ArgumentException($"Invalid native option '{arg}'.");
                }

                result.Add(normalizedValue);
            }
        }

        return result.ToArray();

        void AddModernOption(string name, Option option)
        {
            if (!name.StartsWith('-', StringComparison.Ordinal))
            {
                return;
            }

            if (option.Arity.MinimumNumberOfValues > 0)
            {
                optionsWithValues.Add(name);
            }

            AddModernShortOption(name, option);
        }

        void AddModernShortOption(string name, Option option)
        {
            if (name is ['-', _])
            {
                modernShortOptions.Add(name, option);
            }
        }
    }

    private static bool TrySplitNativeOption(
        string arg,
        bool allowSlash,
        out string optionName,
        out string? value)
    {
        optionName = string.Empty;
        value = null;

        if (arg.Length < 2 ||
            arg.StartsWith("--", StringComparison.Ordinal) ||
            (arg[0] != '-' && (!allowSlash || arg[0] != '/')))
        {
            return false;
        }

        ReadOnlySpan<char> option = arg.AsSpan(1);
        int separatorIndex = option.IndexOfAny('=', ':');
        if (separatorIndex >= 0)
        {
            optionName = option[..separatorIndex].ToString();
            value = option[(separatorIndex + 1)..].ToString();
        }
        else
        {
            optionName = option.ToString();
        }

        return true;
    }

    private static bool IsValidModernShortOption(
        char prefix,
        string optionName,
        string? value,
        Dictionary<string, Option> modernShortOptions)
    {
        if (prefix != '-' || optionName.Length != 1)
        {
            return false;
        }

        if (!modernShortOptions.TryGetValue($"-{optionName}", out Option? option))
        {
            return false;
        }

        if (value is null)
        {
            return true;
        }

        return option.ValueType == typeof(bool)
            ? bool.TryParse(value, out _)
            : option.Arity.MaximumNumberOfValues > 0;
    }

    private static string NormalizeDebugMode(string value)
    {
        string trimmedValue = value.TrimStart();
        if (trimmedValue.StartsWith("IMP", StringComparison.OrdinalIgnoreCase))
        {
            return nameof(DebugMode.Impl);
        }

        if (trimmedValue.StartsWith("OPT", StringComparison.OrdinalIgnoreCase))
        {
            return nameof(DebugMode.Opt);
        }

        return trimmedValue;
    }
}