// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#if !UNIX

using System.Security.Cryptography;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Management.Automation.Internal;
using System.Management.Automation.Security;
using System.Management.Automation.Win32Native;
using System.Runtime.InteropServices;

namespace System.Management.Automation
{
    /// <summary>
    /// Defines the possible status when validating integrity of catalog.
    /// </summary>
    public enum CatalogValidationStatus
    {
        /// <summary>
        /// Status when catalog is not tampered.
        /// </summary>
        Valid,

        /// <summary>
        /// Status when catalog is tampered.
        /// </summary>
        ValidationFailed
    }

    /// <summary>
    /// Object returned by Catalog Cmdlets.
    /// </summary>
    public class CatalogInformation
    {
        /// <summary>
        /// Status of catalog.
        /// </summary>
        public CatalogValidationStatus Status { get; set; }

        /// <summary>
        /// Hash Algorithm used to calculate the hashes of files in Catalog.
        /// </summary>
        public string HashAlgorithm { get; set; }

        /// <summary>
        /// Dictionary mapping files relative paths to their hash values found from Catalog.
        /// </summary>
        public Dictionary<string, string> CatalogItems { get; set; }

        /// <summary>
        /// Dictionary mapping files relative paths to their hash values.
        /// </summary>
        public Dictionary<string, string> PathItems { get; set; }

        /// <summary>
        /// Signature for the catalog.
        /// </summary>
        public Signature Signature { get; set; }
    }

    /// <summary>
    /// Helper functions for Windows Catalog functionality.
    /// </summary>
    internal static class CatalogHelper
    {
        // Catalog Version is (0X100 = 256) for Catalog Version 1
        private const int catalogVersion1 = 256;

        // Catalog Version is (0X200 = 512) for Catalog Version 2
        private const int catalogVersion2 = 512;

        // Hash Algorithms supported by Windows Catalog
        private const string HashAlgorithmSHA1 = "SHA1";
        private const string HashAlgorithmSHA256 = "SHA256";
        private static PSCmdlet _cmdlet = null;

        /// <summary>
        /// Find out the Version of Catalog by reading its Meta data. We can have either version 1 or version 2 catalog.
        /// </summary>
        /// <param name="catalogHandle">Handle to open catalog file.</param>
        /// <returns>Version of the catalog.</returns>
        private static int GetCatalogVersion(SafeCATHandle catalogHandle)
        {
            int catalogVersion = -1;

            WinTrustMethods.CRYPTCATSTORE catalogInfo = WinTrustMethods.CryptCATStoreFromHandle(catalogHandle);

            if (catalogInfo.dwPublicVersion == catalogVersion2)
            {
                catalogVersion = 2;
            }
            // One Windows 7 this API sent version information as decimal 1 not hex (0X100 = 256)
            // so we are checking for that value as well. Reason we are not checking for version 2 above in
            // this scenario because catalog version 2 is not supported on win7.
            else if ((catalogInfo.dwPublicVersion == catalogVersion1) || (catalogInfo.dwPublicVersion == 1))
            {
                catalogVersion = 1;
            }
            else
            {
                // catalog version we don't understand
                Exception exception = new InvalidOperationException(StringUtil.Format(CatalogStrings.UnKnownCatalogVersion,
                                      catalogVersion1.ToString("X"),
                                      catalogVersion2.ToString("X")));

                ErrorRecord errorRecord = new ErrorRecord(exception, "UnKnownCatalogVersion", ErrorCategory.InvalidOperation, null);
                _cmdlet.ThrowTerminatingError(errorRecord);
            }

            return catalogVersion;
        }

        /// <summary>
        /// HashAlgorithm used by the Catalog. It is based on the version of Catalog.
        /// </summary>
        /// <param name="catalogVersion">Path of the output catalog file.</param>
        /// <returns>Version of the catalog.</returns>
        private static string GetCatalogHashAlgorithm(int catalogVersion)
        {
            string hashAlgorithm = string.Empty;

            if (catalogVersion == 1)
            {
                hashAlgorithm = HashAlgorithmSHA1;
            }
            else if (catalogVersion == 2)
            {
                hashAlgorithm = HashAlgorithmSHA256;
            }
            else
            {
                // version we don't understand
                Exception exception = new InvalidOperationException(StringUtil.Format(CatalogStrings.UnKnownCatalogVersion,
                                      "1.0",
                                      "2.0"));

                ErrorRecord errorRecord = new ErrorRecord(exception, "UnKnownCatalogVersion", ErrorCategory.InvalidOperation, null);
                _cmdlet.ThrowTerminatingError(errorRecord);
            }

            return hashAlgorithm;
        }

        /// <summary>
        /// Generate the Catalog Definition File representing files and folders.
        /// </summary>
        /// <param name="Path">Path of expected output .cdf file.</param>
        /// <param name="catalogFilePath">Path of the output catalog file.</param>
        /// <param name="cdfFilePath">Path of the catalog definition file.</param>
        /// <param name="catalogVersion">Version of catalog.</param>
        /// <param name="hashAlgorithm">Hash method used to generate hashes for the Catalog.</param>
        /// <returns>HashSet for the relative Path for files in Catalog.</returns>
        internal static string GenerateCDFFile(Collection<string> Path, string catalogFilePath, string cdfFilePath, int catalogVersion, string hashAlgorithm)
        {
            HashSet<string> relativePaths = new HashSet<string>();

            string cdfHeaderContent = string.Empty;
            string cdfFilesContent = string.Empty;
            int catAttributeCount = 0;

            // First create header and files section for the catalog then write in file
            cdfHeaderContent += "[CatalogHeader]" + Environment.NewLine;
            cdfHeaderContent += @"Name=" + catalogFilePath + Environment.NewLine;
            cdfHeaderContent += "CatalogVersion=" + catalogVersion + Environment.NewLine;
            cdfHeaderContent += "HashAlgorithms=" + hashAlgorithm + Environment.NewLine;
            cdfFilesContent += "[CatalogFiles]" + Environment.NewLine;

            foreach (string catalogFile in Path)
            {
                if (System.IO.Directory.Exists(catalogFile))
                {
                    var directoryItems = Directory.EnumerateFiles(catalogFile, "*.*", SearchOption.AllDirectories);
                    foreach (string fileItem in directoryItems)
                    {
                        ProcessFileToBeAddedInCatalogDefinitionFile(new FileInfo(fileItem), new DirectoryInfo(catalogFile), ref relativePaths, ref cdfHeaderContent, ref cdfFilesContent, ref catAttributeCount);
                    }
                }
                else if (System.IO.File.Exists(catalogFile))
                {
                    ProcessFileToBeAddedInCatalogDefinitionFile(new FileInfo(catalogFile), null, ref relativePaths, ref cdfHeaderContent, ref cdfFilesContent, ref catAttributeCount);
                }
            }

            using (System.IO.StreamWriter fileWriter = new System.IO.StreamWriter(new FileStream(cdfFilePath, FileMode.Create)))
            {
                fileWriter.WriteLine(cdfHeaderContent);
                fileWriter.WriteLine();
                fileWriter.WriteLine(cdfFilesContent);
            }

            return cdfFilePath;
        }

        /// <summary>
        /// Get file attribute (Relative path in our case) from catalog.
        /// </summary>
        /// <param name="fileToHash">File to hash.</param>
        /// <param name="dirInfo">Directory information about file needed to calculate relative file path.</param>
        /// <param name="relativePaths">Working set of relative paths of all files.</param>
        /// <param name="cdfHeaderContent">Content to be added in CatalogHeader section of cdf File.</param>
        /// <param name="cdfFilesContent">Content to be added in CatalogFiles section of cdf File.</param>
        /// <param name="catAttributeCount">Indicating the current no of catalog header level attributes.</param>
        /// <returns>Void.</returns>
        internal static void ProcessFileToBeAddedInCatalogDefinitionFile(FileInfo fileToHash, DirectoryInfo dirInfo, ref HashSet<string> relativePaths, ref string cdfHeaderContent, ref string cdfFilesContent, ref int catAttributeCount)
        {
            string relativePath = string.Empty;

            if (dirInfo != null)
            {
                // Relative path of the file is the path inside the containing folder excluding folder Name
                relativePath = fileToHash.FullName.AsSpan(dirInfo.FullName.Length).TrimStart('\\').ToString();
            }
            else
            {
                relativePath = fileToHash.Name;
            }

            if (!relativePaths.Contains(relativePath))
            {
                relativePaths.Add(relativePath);
                if (fileToHash.Length != 0)
                {
                    cdfFilesContent += "<HASH>" + fileToHash.FullName + "=" + fileToHash.FullName + Environment.NewLine;
                    cdfFilesContent += "<HASH>" + fileToHash.FullName + "ATTR1=0x10010001:FilePath:" + relativePath + Environment.NewLine;
                }
                else
                {
                    // zero length files are added as catalog level attributes because they can not be hashed
                    cdfHeaderContent += "CATATTR" + (++catAttributeCount) + "=0x10010001:FilePath:" + relativePath + Environment.NewLine;
                }
            }
            else
            {
                // If Files have same relative paths we can not distinguish them for
                // Validation. So failing.
                ErrorRecord errorRecord = new ErrorRecord(new InvalidOperationException(StringUtil.Format(CatalogStrings.FoundDuplicateFilesRelativePath, relativePath)), "FoundDuplicateFilesRelativePath", ErrorCategory.InvalidOperation, null);
                _cmdlet.ThrowTerminatingError(errorRecord);
            }
        }
        /// <summary>
        /// Generate the Catalog file for Input Catalog Definition File.
        /// </summary>
        /// <param name="cdfFilePath">Path to the Input .cdf file.</param>
        internal static void GenerateCatalogFile(string cdfFilePath)
        {
            // Open CDF File
            SafeCATCDFHandle resultCDF;
            try
            {
                resultCDF = WinTrustMethods.CryptCATCDFOpen(cdfFilePath, ParseErrorCallback);
            }
            catch (Win32Exception e)
            {
                // If we are not able to open CDF file we can not continue generating catalog
                ErrorRecord errorRecord = new ErrorRecord(
                    new InvalidOperationException(CatalogStrings.UnableToOpenCatalogDefinitionFile, e),
                    "UnableToOpenCatalogDefinitionFile",
                    ErrorCategory.InvalidOperation,
                    null);
                _cmdlet.ThrowTerminatingError(errorRecord);
                return;
            }

            // navigate CDF header and files sections
            using (resultCDF)
            {
                // First navigate all catalog level attributes entries first, they represent zero size files
                IntPtr catalogAttr = IntPtr.Zero;
                do
                {
                    catalogAttr = WinTrustMethods.CryptCATCDFEnumCatAttributes(resultCDF, catalogAttr, ParseErrorCallback);

                    if (catalogAttr != IntPtr.Zero)
                    {
                        string filePath = ProcessFilePathAttributeInCatalog(catalogAttr);
                        _cmdlet.WriteVerbose(StringUtil.Format(CatalogStrings.AddFileToCatalog, filePath, filePath));
                    }
                } while (catalogAttr != IntPtr.Zero);

                // navigate all the files hash entries in the .cdf file
                IntPtr memberInfo = IntPtr.Zero;
                IntPtr memberFile = IntPtr.Zero;
                string fileName = string.Empty;
                do
                {
                    memberFile = WinTrustMethods.CryptCATCDFEnumMembersByCDFTagEx(resultCDF, memberFile, ParseErrorCallback, ref memberInfo,
                        fContinueOnError: true, pvReserved: IntPtr.Zero);
                    fileName = Marshal.PtrToStringUni(memberFile);

                    if (!string.IsNullOrEmpty(fileName))
                    {
                        IntPtr memberAttr = IntPtr.Zero;
                        string fileRelativePath = string.Empty;
                        do
                        {
                            memberAttr = WinTrustMethods.CryptCATCDFEnumAttributesWithCDFTag(resultCDF, memberFile, memberInfo, memberAttr, ParseErrorCallback);

                            if (memberAttr != IntPtr.Zero)
                            {
                                fileRelativePath = ProcessFilePathAttributeInCatalog(memberAttr);
                                if (!string.IsNullOrEmpty(fileRelativePath))
                                {
                                    // Found the attribute we are looking for
                                    // Filename we read from the above API has <Hash> appended to its name as per CDF file tags convention
                                    // Truncating that Information from the string.
                                    string itemName = fileName.Substring(6);
                                    _cmdlet.WriteVerbose(StringUtil.Format(CatalogStrings.AddFileToCatalog, itemName, fileRelativePath));
                                    break;
                                }
                            }
                        } while (memberAttr != IntPtr.Zero);
                    }
                } while (fileName != null);
            }
        }

        /// <summary>
        /// To generate Catalog for the folder.
        /// </summary>
        /// <param name="Path">Path to folder or File.</param>
        /// <param name="catalogFilePath">Catalog File Path.</param>
        /// <param name="catalogVersion">Catalog File Path.</param>
        /// <param name="cmdlet">Instance of cmdlet calling this method.</param>
        /// <returns>True if able to generate .cat file or false.</returns>
        internal static FileInfo GenerateCatalog(PSCmdlet cmdlet, Collection<string> Path, string catalogFilePath, int catalogVersion)
        {
            _cmdlet = cmdlet;
            string hashAlgorithm = GetCatalogHashAlgorithm(catalogVersion);

            if (!string.IsNullOrEmpty(hashAlgorithm))
            {
                // Generate Path for Catalog Definition File
                string cdfFilePath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), System.IO.Path.GetRandomFileName());
                cdfFilePath += ".cdf";
                try
                {
                    cdfFilePath = GenerateCDFFile(Path, catalogFilePath, cdfFilePath, catalogVersion, hashAlgorithm);

                    if (!File.Exists(cdfFilePath))
                    {
                        // If we are not able to generate catalog definition file we can not continue generating catalog
                        // throw PSTraceSource.NewInvalidOperationException("catalog", CatalogStrings.CatalogDefinitionFileNotGenerated);
                        ErrorRecord errorRecord = new ErrorRecord(new InvalidOperationException(CatalogStrings.CatalogDefinitionFileNotGenerated), "CatalogDefinitionFileNotGenerated", ErrorCategory.InvalidOperation, null);
                        _cmdlet.ThrowTerminatingError(errorRecord);
                    }

                    GenerateCatalogFile(cdfFilePath);
                    if (File.Exists(catalogFilePath))
                    {
                        return new FileInfo(catalogFilePath);
                    }
                }
                finally
                {
                    File.Delete(cdfFilePath);
                }
            }

            return null;
        }

        /// <summary>
        /// Get file attribute (Relative path in our case) from catalog.
        /// </summary>
        /// <param name="memberAttrInfo">Pointer to current attribute of catalog member.</param>
        /// <returns>Value of the attribute.</returns>
        internal static string ProcessFilePathAttributeInCatalog(IntPtr memberAttrInfo)
        {
            string relativePath = string.Empty;

            WinTrustMethods.CRYPTCATATTRIBUTE currentMemberAttr = Marshal.PtrToStructure<WinTrustMethods.CRYPTCATATTRIBUTE>(memberAttrInfo);

            // check if this is the attribute we are looking for
            // catalog generated other way not using New-FileCatalog can have attributes we don't understand
            if (currentMemberAttr.pwszReferenceTag.Equals("FilePath", StringComparison.OrdinalIgnoreCase))
            {
                // find the size for the current attribute value and then allocate buffer and copy from byte array
                int attrValueSize = (int)currentMemberAttr.cbValue;
                byte[] attrValue = new byte[attrValueSize];
                Marshal.Copy(currentMemberAttr.pbValue, attrValue, 0, attrValueSize);
                relativePath = System.Text.Encoding.Unicode.GetString(attrValue);
                relativePath = relativePath.TrimEnd('\0');
            }

            return relativePath;
        }

        /// <summary>
        /// Make a hash for the file.
        /// </summary>
        /// <param name="filePath">Path of the file.</param>
        /// <param name="hashAlgorithm">Used to calculate Hash.</param>
        /// <returns>HashValue for the file.</returns>
        internal static string CalculateFileHash(string filePath, string hashAlgorithm)
        {
            string hashValue = string.Empty;

            // To get handle to the hash algorithm to be used to calculate hashes
            SafeCATAdminHandle catAdmin;
            try
            {
                catAdmin = WinTrustMethods.CryptCATAdminAcquireContext2(hashAlgorithm);
            }
            catch (Win32Exception e)
            {
                ErrorRecord errorRecord = new ErrorRecord(
                    new InvalidOperationException(StringUtil.Format(CatalogStrings.UnableToAcquireHashAlgorithmContext, hashAlgorithm), e),
                    "UnableToAcquireHashAlgorithmContext",
                    ErrorCategory.InvalidOperation,
                    null);
                _cmdlet.ThrowTerminatingError(errorRecord);

                // The method returns an empty string on a failure.
                return hashValue;
            }

            // Open the file that is to be hashed for reading and get its handle
            FileStream fileStream;
            try
            {
                fileStream = File.Open(filePath, FileMode.Open, FileAccess.Read);
            }
            catch (Exception e)
            {
                // If we are not able to open file that is to be hashed we can not continue with catalog validation
                ErrorRecord errorRecord = new ErrorRecord(
                    new InvalidOperationException(StringUtil.Format(CatalogStrings.UnableToReadFileToHash, filePath), e),
                    "UnableToReadFileToHash",
                    ErrorCategory.InvalidOperation,
                    null);
                _cmdlet.ThrowTerminatingError(errorRecord);

                // The method returns an empty string on a failure.
                return hashValue;
            }

            using (catAdmin)
            using (fileStream)
            {
                byte[] hashBytes = Array.Empty<byte>();
                try
                {
                    hashBytes = WinTrustMethods.CryptCATAdminCalcHashFromFileHandle2(catAdmin, fileStream.SafeFileHandle);
                }
                catch (Win32Exception e)
                {
                    ErrorRecord errorRecord = new ErrorRecord(
                        new InvalidOperationException(StringUtil.Format(CatalogStrings.UnableToCreateFileHash, filePath), e),
                        "UnableToCreateFileHash",
                        ErrorCategory.InvalidOperation,
                        null);
                    _cmdlet.ThrowTerminatingError(errorRecord);
                }

                hashValue = Convert.ToHexString(hashBytes);
            }

            return hashValue;
        }

        /// <summary>
        /// Make list of hashes for given Catalog File.
        /// </summary>
        /// <param name="catalogFilePath">Path to the folder having catalog file.</param>
        /// <param name="excludedPatterns"></param>
        /// <param name="catalogVersion">The version of input catalog we read from catalog meta data after opening it.</param>
        /// <returns>Dictionary mapping files relative paths to HashValues.</returns>
        internal static Dictionary<string, string> GetHashesFromCatalog(string catalogFilePath, WildcardPattern[] excludedPatterns, out int catalogVersion)
        {
            Dictionary<string, string> catalogHashes = new Dictionary<string, string>(StringComparer.CurrentCultureIgnoreCase);
            catalogVersion = 0;

            SafeCATHandle resultCatalog;
            try
            {
                resultCatalog = WinTrustMethods.CryptCATOpen(catalogFilePath, 0, IntPtr.Zero, 1, 0);
            }
            catch (Win32Exception e)
            {
                ErrorRecord errorRecord = new ErrorRecord(
                    new InvalidOperationException(StringUtil.Format(CatalogStrings.UnableToOpenCatalogFile, catalogFilePath), e),
                    "UnableToOpenCatalogFile",
                    ErrorCategory.InvalidOperation,
                    null);
                _cmdlet.ThrowTerminatingError(errorRecord);
                return catalogHashes;
            }

            using (resultCatalog)
            {
                IntPtr catAttrInfo = IntPtr.Zero;

                // First traverse all catalog level attributes to get information about zero size file.
                do
                {
                    catAttrInfo = WinTrustMethods.CryptCATEnumerateCatAttr(resultCatalog, catAttrInfo);

                    // If we found attribute it is a file information retrieve its relative path
                    // and add it to catalog hash collection if its not in excluded files criteria
                    if (catAttrInfo != IntPtr.Zero)
                    {
                        string relativePath = ProcessFilePathAttributeInCatalog(catAttrInfo);
                        if (!string.IsNullOrEmpty(relativePath))
                        {
                            ProcessCatalogFile(relativePath, string.Empty, excludedPatterns, ref catalogHashes);
                        }
                    }
                } while (catAttrInfo != IntPtr.Zero);

                catalogVersion = GetCatalogVersion(resultCatalog);

                IntPtr memberInfo = IntPtr.Zero;
                // Next Navigate all members in Catalog files and get their relative paths and hashes
                do
                {
                    memberInfo = WinTrustMethods.CryptCATEnumerateMember(resultCatalog, memberInfo);
                    if (memberInfo != IntPtr.Zero)
                    {
                        WinTrustMethods.CRYPTCATMEMBER currentMember = Marshal.PtrToStructure<WinTrustMethods.CRYPTCATMEMBER>(memberInfo);
                        WinTrustMethods.SIP_INDIRECT_DATA pIndirectData = Marshal.PtrToStructure<WinTrustMethods.SIP_INDIRECT_DATA>(currentMember.pIndirectData);

                        // For Catalog version 2 CryptoAPI puts hashes of file attributes(relative path in our case) in Catalog as well
                        // We validate those along with file hashes so we are skipping duplicate entries
                        if (!((catalogVersion == 2) && (pIndirectData.DigestAlgorithm.pszObjId.Equals(new Oid("SHA1").Value, StringComparison.OrdinalIgnoreCase))))
                        {
                            string relativePath = string.Empty;
                            IntPtr memberAttrInfo = IntPtr.Zero;
                            do
                            {
                                memberAttrInfo = WinTrustMethods.CryptCATEnumerateAttr(resultCatalog, memberInfo, memberAttrInfo);

                                if (memberAttrInfo != IntPtr.Zero)
                                {
                                    relativePath = ProcessFilePathAttributeInCatalog(memberAttrInfo);
                                    if (!string.IsNullOrEmpty(relativePath))
                                    {
                                        break;
                                    }
                                }
                            }
                            while (memberAttrInfo != IntPtr.Zero);

                            // If we did not find any Relative Path for the item in catalog we should quit
                            // This catalog must not be valid for our use as catalogs generated using New-FileCatalog
                            // always contains relative file Paths
                            if (string.IsNullOrEmpty(relativePath))
                            {
                                ErrorRecord errorRecord = new ErrorRecord(new InvalidOperationException(StringUtil.Format(CatalogStrings.UnableToOpenCatalogFile, catalogFilePath)), "UnableToOpenCatalogFile", ErrorCategory.InvalidOperation, null);
                                _cmdlet.ThrowTerminatingError(errorRecord);
                            }

                            ProcessCatalogFile(relativePath, currentMember.pwszReferenceTag, excludedPatterns, ref catalogHashes);
                        }
                    }
                } while (memberInfo != IntPtr.Zero);
            }

            return catalogHashes;
        }

        /// <summary>
        /// Process file in path for its relative paths.
        /// </summary>
        /// <param name="relativePath">Relative path of file found in catalog.</param>
        /// <param name="fileHash">Hash of file found in catalog.</param>
        /// <param name="excludedPatterns">Skip file from validation if it matches these patterns.</param>
        /// <param name="catalogHashes">Collection of hashes of catalog.</param>
        /// <returns>Void.</returns>
        internal static void ProcessCatalogFile(string relativePath, string fileHash, WildcardPattern[] excludedPatterns, ref Dictionary<string, string> catalogHashes)
        {
            // Found the attribute we are looking for
            _cmdlet.WriteVerbose(StringUtil.Format(CatalogStrings.FoundFileHashInCatalogItem, relativePath, fileHash));

            // Only add the file for validation if it does not meet exclusion criteria
            if (!CheckExcludedCriteria((new FileInfo(relativePath)).Name, excludedPatterns))
            {
                // Add relativePath mapping to hashvalue for each file
                catalogHashes.Add(relativePath, fileHash);
            }
            else
            {
                // Verbose about skipping file from catalog
                _cmdlet.WriteVerbose(StringUtil.Format(CatalogStrings.SkipValidationOfCatalogFile, relativePath));
            }
        }
        /// <summary>
        /// Process file in path for its relative paths.
        /// </summary>
        /// <param name="fileToHash">File to hash.</param>
        /// <param name="dirInfo">Directory information about file needed to calculate relative file path.</param>
        /// <param name="hashAlgorithm">Used to calculate Hash.</param>
        /// <param name="excludedPatterns">Skip file if it matches these patterns.</param>
        /// <param name="fileHashes">Collection of hashes of files.</param>
        /// <returns>Void.</returns>
        internal static void ProcessPathFile(FileInfo fileToHash, DirectoryInfo dirInfo, string hashAlgorithm, WildcardPattern[] excludedPatterns, ref Dictionary<string, string> fileHashes)
        {
            string relativePath = string.Empty;
            string exclude = string.Empty;

            if (dirInfo != null)
            {
                // Relative path of the file is the path inside the containing folder excluding folder Name
                relativePath = fileToHash.FullName.AsSpan(dirInfo.FullName.Length).TrimStart('\\').ToString();
                exclude = fileToHash.Name;
            }
            else
            {
                relativePath = fileToHash.Name;
                exclude = relativePath;
            }

            if (!CheckExcludedCriteria(exclude, excludedPatterns))
            {
                string fileHash = string.Empty;

                if (fileToHash.Length != 0)
                {
                    fileHash = CalculateFileHash(fileToHash.FullName, hashAlgorithm);
                }

                if (fileHashes.TryAdd(relativePath, fileHash))
                {
                    _cmdlet.WriteVerbose(StringUtil.Format(CatalogStrings.FoundFileInPath, relativePath, fileHash));
                }
                else
                {
                    ErrorRecord errorRecord = new ErrorRecord(new InvalidOperationException(StringUtil.Format(CatalogStrings.FoundDuplicateFilesRelativePath, relativePath)), "FoundDuplicateFilesRelativePath", ErrorCategory.InvalidOperation, null);
                    _cmdlet.ThrowTerminatingError(errorRecord);
                }
            }
            else
            {
                // Verbose about skipping file from path
                _cmdlet.WriteVerbose(StringUtil.Format(CatalogStrings.SkipValidationOfPathFile, relativePath));
            }
        }

        /// <summary>
        /// Generate the hashes of all the files in given folder.
        /// </summary>
        /// <param name="folderPaths">Path to folder or File.</param>
        /// <param name="catalogFilePath">Catalog file path it should be skipped when calculating the hashes.</param>
        /// <param name="hashAlgorithm">Used to calculate Hash.</param>
        /// <param name="excludedPatterns"></param>
        /// <returns>Dictionary mapping file relative paths to hashes..</returns>
        internal static Dictionary<string, string> CalculateHashesFromPath(Collection<string> folderPaths, string catalogFilePath, string hashAlgorithm, WildcardPattern[] excludedPatterns)
        {
            // Create a HashTable of file Hashes
            Dictionary<string, string> fileHashes = new Dictionary<string, string>(StringComparer.CurrentCultureIgnoreCase);

            foreach (string folderPath in folderPaths)
            {
                if (System.IO.Directory.Exists(folderPath))
                {
                    var directoryItems = Directory.EnumerateFiles(folderPath, "*.*", SearchOption.AllDirectories);
                    foreach (string fileItem in directoryItems)
                    {
                        // if its the catalog file we are validating we will skip it
                        if (string.Equals(fileItem, catalogFilePath, StringComparison.OrdinalIgnoreCase))
                            continue;

                        ProcessPathFile(new FileInfo(fileItem), new DirectoryInfo(folderPath), hashAlgorithm, excludedPatterns, ref fileHashes);
                    }
                }
                else if (System.IO.File.Exists(folderPath))
                {
                    ProcessPathFile(new FileInfo(folderPath), null, hashAlgorithm, excludedPatterns, ref fileHashes);
                }
            }

            return fileHashes;
        }

        /// <summary>
        /// Compare Dictionary objects.
        /// </summary>
        /// <param name="catalogItems">Hashes extracted from Catalog.</param>
        /// <param name="pathItems">Hashes created from folders path.</param>
        /// <returns>True if both collections are same.</returns>
        internal static bool CompareDictionaries(Dictionary<string, string> catalogItems, Dictionary<string, string> pathItems)
        {
            bool Status = true;

            List<string> relativePathsFromFolder = pathItems.Keys.ToList();
            List<string> relativePathsFromCatalog = catalogItems.Keys.ToList();

            // Find entries that are not in both lists. These should be empty lists for success
            // Hashes in Catalog should be exactly similar to the ones from folder
            List<string> relativePathsNotInFolder = relativePathsFromFolder.Except(relativePathsFromCatalog, StringComparer.CurrentCultureIgnoreCase).ToList();
            List<string> relativePathsNotInCatalog = relativePathsFromCatalog.Except(relativePathsFromFolder, StringComparer.CurrentCultureIgnoreCase).ToList();

            // Found extra hashes in Folder
            if ((relativePathsNotInFolder.Count != 0) || (relativePathsNotInCatalog.Count != 0))
            {
                Status = false;
            }

            foreach (KeyValuePair<string, string> item in catalogItems)
            {
                string catalogHashValue = (string)catalogItems[item.Key];
                if (pathItems.ContainsKey(item.Key))
                {
                    string folderHashValue = (string)pathItems[item.Key];
                    if (folderHashValue.Equals(catalogHashValue))
                    {
                        continue;
                    }
                    else
                    {
                        Status = false;
                    }
                }
            }

            return Status;
        }
        /// <summary>
        /// To Validate the Integrity of Catalog.
        /// </summary>
        /// <param name="catalogFolders">Folder for which catalog is created.</param>
        /// <param name="catalogFilePath">File Name of the Catalog.</param>
        /// <param name="excludedPatterns"></param>
        /// <param name="cmdlet">Instance of cmdlet calling this method.</param>
        /// <returns>Information about Catalog.</returns>
        internal static CatalogInformation ValidateCatalog(PSCmdlet cmdlet, Collection<string> catalogFolders, string catalogFilePath, WildcardPattern[] excludedPatterns)
        {
            _cmdlet = cmdlet;
            int catalogVersion = 0;
            Dictionary<string, string> catalogHashes = GetHashesFromCatalog(catalogFilePath, excludedPatterns, out catalogVersion);
            string hashAlgorithm = GetCatalogHashAlgorithm(catalogVersion);

            if (!string.IsNullOrEmpty(hashAlgorithm))
            {
                Dictionary<string, string> fileHashes = CalculateHashesFromPath(catalogFolders, catalogFilePath, hashAlgorithm, excludedPatterns);
                CatalogInformation catalog = new CatalogInformation();
                catalog.CatalogItems = catalogHashes;
                catalog.PathItems = fileHashes;
                bool status = CompareDictionaries(catalogHashes, fileHashes);
                if (status)
                {
                    catalog.Status = CatalogValidationStatus.Valid;
                }
                else
                {
                    catalog.Status = CatalogValidationStatus.ValidationFailed;
                }

                catalog.HashAlgorithm = hashAlgorithm;
                catalog.Signature = SignatureHelper.GetSignature(catalogFilePath, null);
                return catalog;
            }

            return null;
        }

        /// <summary>
        /// Check if file meets the skip validation criteria.
        /// </summary>
        /// <param name="filename"></param>
        /// <param name="excludedPatterns"></param>
        /// <returns>True if match is found else false.</returns>
        internal static bool CheckExcludedCriteria(string filename, WildcardPattern[] excludedPatterns)
        {
            if (excludedPatterns != null)
            {
                foreach (WildcardPattern patternItem in excludedPatterns)
                {
                    if (patternItem.IsMatch(filename))
                    {
                        return true;
                    }
                }
            }

            return false;
        }
        /// <summary>
        /// Call back when error is thrown by catalog API's.
        /// </summary>
        private static void ParseErrorCallback(uint dwErrorArea, uint dwLocalError, string pwszLine)
        {
            switch (dwErrorArea)
            {
                case NativeConstants.CRYPTCAT_E_AREA_HEADER:
                    break;
                case NativeConstants.CRYPTCAT_E_AREA_MEMBER:
                    break;
                case NativeConstants.CRYPTCAT_E_AREA_ATTRIBUTE:
                    break;
                default:
                    break;
            }

            switch (dwLocalError)
            {
                case NativeConstants.CRYPTCAT_E_CDF_MEMBER_FILE_PATH:
                    {
                        ErrorRecord errorRecord = new ErrorRecord(new InvalidOperationException(StringUtil.Format(CatalogStrings.UnableToFindFileNameOrPathForCatalogMember, pwszLine)), "UnableToFindFileNameOrPathForCatalogMember", ErrorCategory.InvalidOperation, null);
                        _cmdlet.ThrowTerminatingError(errorRecord);
                        break;
                    }
                case NativeConstants.CRYPTCAT_E_CDF_MEMBER_INDIRECTDATA:
                    {
                        ErrorRecord errorRecord = new ErrorRecord(new InvalidOperationException(StringUtil.Format(CatalogStrings.UnableToCreateFileHash, pwszLine)), "UnableToCreateFileHash", ErrorCategory.InvalidOperation, null);
                        _cmdlet.ThrowTerminatingError(errorRecord);
                        break;
                    }
                case NativeConstants.CRYPTCAT_E_CDF_MEMBER_FILENOTFOUND:
                    {
                        ErrorRecord errorRecord = new ErrorRecord(new InvalidOperationException(StringUtil.Format(CatalogStrings.UnableToFindFileToHash, pwszLine)), "UnableToFindFileToHash", ErrorCategory.InvalidOperation, null);
                        _cmdlet.ThrowTerminatingError(errorRecord);
                        break;
                    }
                case NativeConstants.CRYPTCAT_E_CDF_BAD_GUID_CONV:
                    break;
                case NativeConstants.CRYPTCAT_E_CDF_ATTR_TYPECOMBO:
                    break;
                case NativeConstants.CRYPTCAT_E_CDF_ATTR_TOOFEWVALUES:
                    break;
                case NativeConstants.CRYPTCAT_E_CDF_UNSUPPORTED:
                    break;
                case NativeConstants.CRYPTCAT_E_CDF_DUPLICATE:
                    {
                        ErrorRecord errorRecord = new ErrorRecord(new InvalidOperationException(StringUtil.Format(CatalogStrings.FoundDuplicateFileMemberInCatalog, pwszLine)), "FoundDuplicateFileMemberInCatalog", ErrorCategory.InvalidOperation, null);
                        _cmdlet.ThrowTerminatingError(errorRecord);
                        break;
                    }
                case NativeConstants.CRYPTCAT_E_CDF_TAGNOTFOUND:
                    break;
                default:
                    break;
            }
        }
    }
}

#endif
