File: Utils.cs
Web Access
Project: src\src\SignCheck\Microsoft.SignCheck\Microsoft.DotNet.SignCheckLibrary.csproj (Microsoft.DotNet.SignCheckLibrary)
// 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.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
 
namespace Microsoft.SignCheck
{
    public static class Utils
    {
#if NET
        private static readonly HttpClient s_client = new(new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(10) });
#endif
        /// <summary>
        /// Generate a hash for a string value using a given hash algorithm.
        /// </summary>
        /// <param name="value">The value to hash.</param>
        /// <param name="hashName">The name of the <see cref="HashAlgorithm"/> to use.</param>
        /// <returns>A string containing the hash result.</returns>
        public static string GetHash(string value, string hashName)
        {
            byte[] bytes = Encoding.UTF8.GetBytes(value);
            HashAlgorithm ha = CreateHashAlgorithm(hashName);
            byte[] hash = ha.ComputeHash(bytes);
 
            var sb = new StringBuilder();
            foreach (byte b in hash)
            {
                sb.Append(b.ToString("x2"));
            }
 
            return sb.ToString();
        }
 
        public static HashAlgorithm CreateHashAlgorithm(string hashName)
        {
            switch (hashName.ToUpperInvariant())
            {
                case "SHA256":
                    return SHA256.Create();
                case "SHA1":
                    return SHA1.Create();
                case "MD5":
                    return MD5.Create();
                case "SHA384":
                    return SHA384.Create();
                case "SHA512":
                    return SHA512.Create();
                default:
                    throw new ArgumentException("Unsupported hash algorithm name", nameof(hashName));
            }
        }
 
        /// <summary>
        /// Converts a string containing wildcards (*, ?) into a regular expression pattern string.
        /// </summary>
        /// <param name="wildcardPattern">The string pattern.</param>
        /// <returns>A string containing regular expression pattern.</returns>
        public static string ConvertToRegexPattern(string wildcardPattern)
        {
            string escapedPattern = Regex.Escape(wildcardPattern).Replace(@"\*", ".*").Replace(@"\?", ".");
 
            if ((wildcardPattern.EndsWith("*")) || (wildcardPattern.EndsWith("?")))
            {
                return escapedPattern;
            }
            else
            {
                return String.Concat(escapedPattern, "$");
            }            
        }
 
        /// <summary>
        /// Gets the DateTime value from a string
        /// Returns the specified default value if the match is unsuccessful or the timestamp value is 0.
        /// </summary>
        /// <param name="timestamp">The timestamp string to parse.</param>
        /// <param name="defaultValue">The default DateTime value to return if parsing fails.</param>
        /// <returns>The parsed DateTime value or the default value.</returns>
        public static DateTime DateTimeOrDefault(this string timestamp, DateTime defaultValue)
        {
            // Try to parse the timestamp as a Unix timestamp (seconds since epoch)
            if (long.TryParse(timestamp, out long unixTime) && unixTime > 0)
            {
                return DateTimeOffset.FromUnixTimeSeconds(unixTime).UtcDateTime;
            }
 
            // Try to parse the timestamp as a DateTime string
            if (DateTime.TryParse(timestamp, out DateTime dateTime))
            {
                return dateTime;
            }
 
            return defaultValue;
        }
 
        /// <summary>
        /// Gets the value of a named group from a regex match.
        /// Returns null if the match is unsuccessful.
        /// </summary>
        /// <param name="match">The regex match.</param>
        /// <param name="groupName">The name of the group.</param>
        /// <returns>The value of the named group or null if the match is unsuccessful.</returns>
        public static string GroupValueOrDefault(this Match match, string groupName) =>
            match.Success ? match.Groups[groupName].Value : null;
 
        /// <summary>
        /// Captures the console output of an action.
        /// </summary>
        /// <param name="action">The action to execute.</param>
        /// <returns>A tuple containing the result of the action, the standard output, and the error output.</returns>
        public static (bool, string, string) CaptureConsoleOutput(Func<bool> action)
        {
            var consoleOutput = Console.Out;
            StringWriter outputWriter = new StringWriter();
            Console.SetOut(outputWriter);
 
            var errorOutput = Console.Error;
            StringWriter errorOutputWriter = new StringWriter();
            Console.SetError(errorOutputWriter);
 
            try
            {
                bool result = action();
                return (result, outputWriter.ToString(), errorOutputWriter.ToString());
            }
            finally
            {
                Console.SetOut(consoleOutput);
                Console.SetError(errorOutput);
            }
        }
 
        /// <summary>
        /// Runs a bash command and returns the output, error, and exit code.
        /// </summary>
        /// <param name="command">The command to run.</param>
        /// <returns>A tuple containing the exit code, output, and error.</returns>
        public static (int exitCode, string output, string error) RunBashCommand(string command)
        {
            var psi = new ProcessStartInfo
            {
                FileName = "bash",
                Arguments = $"-c \"{command}\"",
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                UseShellExecute = false,
                CreateNoWindow = true
            };
 
            using (var process = Process.Start(psi))
            {
                string output = process.StandardOutput.ReadToEnd();
                string error = process.StandardError.ReadToEnd();
 
                process.WaitForExit(10000); // 10 seconds
                
                return (process.ExitCode, output, error);
            }
        }
 
#if NET
        /// <summary>
        /// Download the Microsoft public key and import it into the keyring.
        /// </summary>
        public static void DownloadAndConfigureMicrosoftPublicKey(string tempDir)
        {
            using (Stream stream = s_client.GetStreamAsync("https://packages.microsoft.com/keys/microsoft.asc").Result)
            {
                using (FileStream fileStream = File.Create($"{tempDir}/microsoft.asc"))
                {
                    stream.CopyTo(fileStream);
                }
            }
 
            (int exitCode, _, string error) = RunBashCommand($"gpg --import {tempDir}/microsoft.asc");
 
            if (exitCode != 0)
            {
                throw new Exception($"Failed to import Microsoft public key: {(string.IsNullOrEmpty(error) ? "unknown error" : error)}");
            }
        }
#endif
    }
}