// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using Microsoft.Cci;
using Microsoft.CodeAnalysis.Interop;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis
{
    /// <summary>
    /// Provides strong name and signs source assemblies.
    /// </summary>
    public class DesktopStrongNameProvider : StrongNameProvider
    {
        // This exception is only used to detect when the acquisition of IClrStrongName fails
        // and the likely reason is that we're running on CoreCLR on a non-Windows platform.
        // The place where the acquisition fails does not have access to localization,
        // so we can't throw some generic exception with a localized message.
        // So this is sort of a token for the eventual message to be generated.

        // The path from where this is thrown to where it is caught is all internal,
        // so there's no chance of an API consumer seeing it.
        internal sealed class ClrStrongNameMissingException : Exception
        {
        }

        private readonly ImmutableArray<string> _keyFileSearchPaths;
        internal override StrongNameFileSystem FileSystem { get; }

        public DesktopStrongNameProvider(ImmutableArray<string> keyFileSearchPaths) : this(keyFileSearchPaths, StrongNameFileSystem.Instance)
        {
        }

        /// <summary>
        /// Creates an instance of <see cref="DesktopStrongNameProvider"/>.
        /// </summary>
        /// <param name="tempPath">Path to use for any temporary file generation.</param>
        /// <param name="keyFileSearchPaths">An ordered set of fully qualified paths which are searched when locating a cryptographic key file.</param>
        public DesktopStrongNameProvider(ImmutableArray<string> keyFileSearchPaths = default, string? tempPath = null)
           : this(keyFileSearchPaths, tempPath == null ? StrongNameFileSystem.Instance : new StrongNameFileSystem(tempPath))
        {

        }

        internal DesktopStrongNameProvider(ImmutableArray<string> keyFileSearchPaths, StrongNameFileSystem strongNameFileSystem)
        {
            if (!keyFileSearchPaths.IsDefault && keyFileSearchPaths.Any(path => !PathUtilities.IsAbsolute(path)))
            {
                throw new ArgumentException(CodeAnalysisResources.AbsolutePathExpected, nameof(keyFileSearchPaths));
            }

            FileSystem = strongNameFileSystem ?? StrongNameFileSystem.Instance;
            _keyFileSearchPaths = keyFileSearchPaths.NullToEmpty();
        }

        internal override StrongNameKeys CreateKeys(string? keyFilePath, string? keyContainerName, bool hasCounterSignature, CommonMessageProvider messageProvider)
        {
            var keyPair = default(ImmutableArray<byte>);
            var publicKey = default(ImmutableArray<byte>);
            string? container = null;

            if (!string.IsNullOrEmpty(keyFilePath))
            {
                try
                {
                    string? resolvedKeyFile = ResolveStrongNameKeyFile(keyFilePath, FileSystem, _keyFileSearchPaths);
                    if (resolvedKeyFile == null)
                    {
                        return new StrongNameKeys(StrongNameKeys.GetKeyFileError(messageProvider, keyFilePath, CodeAnalysisResources.FileNotFound));
                    }

                    Debug.Assert(PathUtilities.IsAbsolute(resolvedKeyFile));
                    var fileContent = ImmutableArray.Create(FileSystem.ReadAllBytes(resolvedKeyFile));
                    return StrongNameKeys.CreateHelper(fileContent, keyFilePath, hasCounterSignature);
                }
                catch (Exception ex)
                {
                    return new StrongNameKeys(StrongNameKeys.GetKeyFileError(messageProvider, keyFilePath, ex.Message));
                }
            }
            else if (!string.IsNullOrEmpty(keyContainerName))
            {
                try
                {
                    ReadKeysFromContainer(keyContainerName, out publicKey);
                    container = keyContainerName;
                }
                catch (ClrStrongNameMissingException)
                {
                    return new StrongNameKeys(StrongNameKeys.GetContainerError(messageProvider, keyContainerName,
                        new CodeAnalysisResourcesLocalizableErrorArgument(nameof(CodeAnalysisResources.AssemblySigningNotSupported))));
                }
                catch (Exception ex)
                {
                    return new StrongNameKeys(StrongNameKeys.GetContainerError(messageProvider, keyContainerName, ex.Message));
                }
            }

            return new StrongNameKeys(keyPair, publicKey, privateKey: null, container, keyFilePath, hasCounterSignature);
        }

        /// <summary>
        /// Resolves assembly strong name key file path.
        /// </summary>
        /// <returns>Normalized key file path or null if not found.</returns>
        internal static string? ResolveStrongNameKeyFile(string path, StrongNameFileSystem fileSystem, ImmutableArray<string> keyFileSearchPaths)
        {
            // Dev11: key path is simply appended to the search paths, even if it starts with the current (parent) directory ("." or "..").
            // This is different from PathUtilities.ResolveRelativePath.

            if (PathUtilities.IsAbsolute(path))
            {
                if (fileSystem.FileExists(path))
                {
                    return FileUtilities.TryNormalizeAbsolutePath(path);
                }

                return path;
            }

            foreach (var searchPath in keyFileSearchPaths)
            {
                string? combinedPath = PathUtilities.CombineAbsoluteAndRelativePaths(searchPath, path);

                Debug.Assert(combinedPath == null || PathUtilities.IsAbsolute(combinedPath));

                if (fileSystem.FileExists(combinedPath))
                {
                    return FileUtilities.TryNormalizeAbsolutePath(combinedPath!);
                }
            }

            return null;
        }

        internal virtual void ReadKeysFromContainer(string keyContainer, out ImmutableArray<byte> publicKey)
        {
            try
            {
                publicKey = GetPublicKey(keyContainer);
            }
            catch (ClrStrongNameMissingException)
            {
                // pipe it through so it's catchable directly by type
                throw;
            }
            catch (Exception ex)
            {
                throw new IOException(ex.Message);
            }
        }

        internal override void SignFile(StrongNameKeys keys, string filePath)
        {
            Debug.Assert(string.IsNullOrEmpty(keys.KeyFilePath) != string.IsNullOrEmpty(keys.KeyContainer));

            if (!string.IsNullOrEmpty(keys.KeyFilePath))
            {
                Sign(filePath, keys.KeyPair);
            }
            else
            {
                Sign(filePath, keys.KeyContainer!);
            }
        }

        internal override void SignBuilder(ExtendedPEBuilder peBuilder, BlobBuilder peBlob, RSAParameters privateKey)
        {
            peBuilder.Sign(peBlob, content => SigningUtilities.CalculateRsaSignature(content, privateKey));
        }

        // EDMAURER in the event that the key is supplied as a file,
        // this type could get an instance member that caches the file
        // contents to avoid reading the file twice - once to get the
        // public key to establish the assembly name and another to do
        // the actual signing

        internal virtual IClrStrongName GetStrongNameInterface()
        {
            try
            {
                return ClrStrongName.GetInstance();
            }
            catch (MarshalDirectiveException) when (PathUtilities.IsUnixLikePlatform)
            {
                // CoreCLR, when not on Windows, doesn't support IClrStrongName (or COM in general).
                // This is really hard to detect/predict without false positives/negatives.
                // It turns out that CoreCLR throws a MarshalDirectiveException when attempting
                // to get the interface (Message "Cannot marshal 'return value': Unknown error."),
                // so just catch that and state that it's not supported.

                // We're deep in a try block that reports the exception's Message as part of a diagnostic.
                // This exception will skip through the IOException wrapping by `Sign` (in this class),
                // then caught by Compilation.SerializeToPeStream or DesktopStringNameProvider.CreateKeys
                throw new ClrStrongNameMissingException();
            }
        }

        internal ImmutableArray<byte> GetPublicKey(string keyContainer)
        {
            IClrStrongName strongName = GetStrongNameInterface();

            IntPtr keyBlob;
            int keyBlobByteCount;

            strongName.StrongNameGetPublicKey(keyContainer, pbKeyBlob: default, 0, out keyBlob, out keyBlobByteCount);

            byte[] pubKey = new byte[keyBlobByteCount];
            Marshal.Copy(keyBlob, pubKey, 0, keyBlobByteCount);
            strongName.StrongNameFreeBuffer(keyBlob);

            return pubKey.AsImmutableOrNull();
        }

        /// <exception cref="IOException"/>
        private void Sign(string filePath, string keyName)
        {
            try
            {
                IClrStrongName strongName = GetStrongNameInterface();
                strongName.StrongNameSignatureGeneration(filePath, keyName, IntPtr.Zero, 0, null, pcbSignatureBlob: out _);
            }
            catch (ClrStrongNameMissingException)
            {
                // pipe it through so it's catchable directly by type
                throw;
            }
            catch (Exception ex)
            {
                throw new IOException(ex.Message, ex);
            }
        }

        private unsafe void Sign(string filePath, ImmutableArray<byte> keyPair)
        {
            try
            {
                IClrStrongName strongName = GetStrongNameInterface();

                fixed (byte* pinned = keyPair.ToArray())
                {
                    strongName.StrongNameSignatureGeneration(filePath, null, (IntPtr)pinned, keyPair.Length, null, pcbSignatureBlob: out _);
                }
            }
            catch (ClrStrongNameMissingException)
            {
                // pipe it through so it's catchable directly by type
                throw;
            }
            catch (Exception ex)
            {
                throw new IOException(ex.Message, ex);
            }
        }

        public override int GetHashCode()
        {
            return Hash.CombineValues(_keyFileSearchPaths, StringComparer.Ordinal);
        }

        public override bool Equals(object? obj)
        {
            if (obj is null || GetType() != obj.GetType())
            {
                return false;
            }

            var other = (DesktopStrongNameProvider)obj;
            if (FileSystem != other.FileSystem)
            {
                return false;
            }

            if (!_keyFileSearchPaths.SequenceEqual(other._keyFileSearchPaths, StringComparer.Ordinal))
            {
                return false;
            }

            return true;
        }
    }
}
