Public/Get-UUIDFromNamespace.ps1

using namespace System.Text
using namespace System.Security.Cryptography

function Get-UUIDFromNamespace {
    <#
    .SYNOPSIS
    Gets a UUID version 3 (UUIDv3) or version 5 (UUIDv5) derived from a namespace and a name.
 
    .DESCRIPTION
    The Get-UUIDFromNamespace function gets a UUID version 3 or version 5, which are universally unique identifiers that are generated using a namespace identifier and a name. UUIDv3 uses MD5 hashing, while UUIDv5 uses SHA-1 hashing to create the identifier.
 
    .PARAMETER Namespace
    The namespace identifier, which is a UUID that defines the scope of the name.
 
    .PARAMETER Name
    The name from which the UUID will be generated. This is typically a string.
 
    .PARAMETER Version
    The version of the UUID to generate. Accepts 3 or 5. Default is 5.
 
    .PARAMETER Encoding
    The encoding method to use for the name. UTF8 for standard RFC compliance, UTF16LE for Windows Terminal compatibility.
 
    .OUTPUTS
    [Guid] The UUID version 3 or 5 derived from the namespace and name.
 
    .EXAMPLE
    PS C:\> Get-UUIDFromNamespace -Namespace "6ba7b810-9dad-11d1-80b4-00c04fd430c8" -Name "example" -Version 5
    Gets a UUIDv5 based on the provided namespace and name.
 
    .EXAMPLE
    PS C:\> Get-UUIDFromNamespace -Namespace "6ba7b810-9dad-11d1-80b4-00c04fd430c8" -Name "example" -Version 3
    Gets a UUIDv3 based on the provided namespace and name.
 
    .EXAMPLE
    PS C:\> Get-UUIDFromNamespace -Namespace "{2bde4a90-d05f-401c-9492-e40884ead1d8}" -Name "Ubuntu" -Encoding UTF16LE
 
    Generates a UUID for a Windows Terminal profile using the profile namespace and UTF16LE encoding.
    Output: 2c4de342-38b7-51cf-b940-2309a097f518
 
    .EXAMPLE
    PS C:\> $AppNamespace = Get-UUIDFromNamespace -Namespace '{f65ddb7e-706b-4499-8a50-40313caf510a}' -Name "Git" -Encoding UTF16LE
    PS C:\> Get-UUIDFromNamespace -Namespace $AppNamespace -Name "Git Bash" -Encoding UTF16LE
 
    Generates a UUID for a Windows Terminal profile using the fragment namespace and UTF16LE encoding.
    Output: 2ece5bfe-50ed-5f3a-ab87-5cd4baafed2b
 
    .NOTES
    UUID version 3 is defined in RFC 4122.
    UUID version 5 is defined in RFC 4122.
    #>

    [Alias('Get-GUIDFromNamespace','gufn')]
    [OutputType([guid])]
    [CmdletBinding()]
    param (
        [Parameter(
            Mandatory,
            Position = 0,
            ValueFromPipeline
        )]
        [Alias('NS')]
        [Guid]
        $Namespace,

        [Parameter(
            Mandatory,
            Position = 1,
            ValueFromPipeline
        )]
        [string]
        $Name,

        [ValidateSet(3, 5)]
        [int]
        $Version = 5,

        [Parameter(
            HelpMessage = 'Use UTF16LE for Windows Terminal compatibility.'
        )]
        [ValidateSet('UTF8', 'UTF16LE')]
        [string]
        $Encoding = 'UTF8'
    )

    begin {
        Set-Variable -Name 'LITTLE_ENDIAN' -Value ([BitConverter]::IsLittleEndian) -Option Constant -Scope Global -ea SilentlyContinue
        function convertUTF16Bytes {
            [CmdletBinding()]
            param (
                [Parameter(ValueFromPipeline)]
                [string]$String
            )
            process {
                $UTF16Enc = [UnicodeEncoding]::new($false, $false)
                $UTF16Bytes = $UTF16Enc.GetBytes($String)
                $AsciiStr = [Encoding]::ASCII.GetString($UTF16Bytes)
                $UTF8Bytes = [Encoding]::UTF8.GetBytes($AsciiStr)
                return $UTF8Bytes
            }
        }
    }

    process {
        # Convert the namespace GUID to a byte array, keeping the endianness from swapping
        if($PSEdition -eq 'Core') {
            $NamespaceBytes = $Namespace.ToByteArray($LITTLE_ENDIAN)
        }
        elseif($PSEdition -eq 'Desktop') {
            # $NamespaceBytes = Switch-ByteNibble $Namespace.ToByteArray()
            # Convert to big-endian (network byte order) for RFC 4122
            $NamespaceBytes = $Namespace.ToByteArray()
            [Array]::Reverse($NamespaceBytes, 0, 4)
            [Array]::Reverse($NamespaceBytes, 4, 2)
            [Array]::Reverse($NamespaceBytes, 6, 2)
        }

        # Convert the name to a byte array
        switch ($Encoding) {
            'UTF16LE' {
                $NameBytes = $Name | convertUTF16Bytes
            }
            'UTF8' {
                $NameBytes = [Encoding]::UTF8.GetBytes($Name)
            }
        }

        # Combine namespace and name
        $CombinedBytes = [byte[]]($NamespaceBytes + $NameBytes)

        # Create a hash based on the version
        if ($Version -eq 5) {
            $HashAlgorithm = [SHA1]::Create()
        }
        elseif ($Version -eq 3) {
            $HashAlgorithm = [MD5]::Create()
        }
        $VersionByte = $Version -shl 4

        # Compute the hash
        $HashBytes = $HashAlgorithm.ComputeHash($CombinedBytes)[0..15]

        # Set the version and variant bits
        $HashBytes[6] = ($HashBytes[6] -band 0x0F) -bor $VersionByte
        $HashBytes[8] = ($HashBytes[8] -band 0x3F) -bor 0x80

        # verify it's a byte array and not a generic object[]
        $HashBytes = $HashBytes -as [byte[]]

        # Create a new GUID from the hash
        if($PSEdition -eq 'Core') {
            $uuid = [Guid]::New($HashBytes, $LITTLE_ENDIAN)
        }
        elseif($PSEdition -eq 'Desktop') {
            [Array]::Reverse($HashBytes, 0, 4)
            [Array]::Reverse($HashBytes, 4, 2)
            [Array]::Reverse($HashBytes, 6, 2)
            $uuid = [Guid]::New($HashBytes)
        }
        return $uuid
    }

    end {
        $HashAlgorithm.Dispose()
    }
}