PowerShell スクリプトで UUID v5 を生成する

## はじめに

先日、とある業務中に「 UUID v5 を生成する PowerShell スクリプト」を書く必要が生じました。

普通のランダムな UUID (UUID v4) であれば

```powershell
[Guid]::NewGuid().ToString()
```

のように簡単に書くことができるのですが、ハッシュ値を使用する UUID v5 はググっても簡単な書き方はおろか、先例となるコードも見つけることができませんでした。
なので、自分で書くことにしました。

使用した環境は Windows 10 + PowerShell 7.3.3 です。

## UUID v5 とは

詳しくは [Wikipedia の記事](https://ja.wikipedia.org/wiki/UUID#%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B33/5)などを読んでいただければと思いますが、簡単に概要を述べると、

- UUID (Universally Unique Identifier): 一意（ユニーク）であることが期待される識別子。32 桁の 16 進数と 4 つのハイフンで構成され、`xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` のような形式で表記される。
- 一般的によく見かける UUID は v4 。
    - ランダム値をもとに生成される。
    - Microsoft は GUID と呼んでいる。
- v3/5 はハッシュ値をもとにした UUID 。
    - 一意であることが保証されているバイト列をハッシュ化して生成される。
    - 同じバイト列からは同じ UUID が生成される。
    - このバイト列は名前空間 (namespace) + 名前 (name) の組み合わせから構成されるため、この2つを組み合わせたものが一意である必要がある。
    - v3 はハッシュ化アルゴリズムに [MD5](https://ja.wikipedia.org/wiki/MD5) を、 v5 は [SHA-1](https://ja.wikipedia.org/wiki/SHA-1) を使用する。

といったものです。

以下が今回書いた PowerShell スクリプトです。

## スクリプト

```generateUUIDv5.ps1
param(
    [Parameter(Mandatory=$true)]
    [ValidateNotNullOrEmpty()]
    [String]$namespace,

    [Parameter(Mandatory=$true)]
    [ValidateNotNullOrEmpty()]
    [String]$name
)

function swapUUIDByteArray([Array]$byteArray){
    $resultArray = $byteArray

    $temp = $resultArray[0]
    $resultArray[0] = $resultArray[3]
    $resultArray[3] = $temp

    $temp = $resultArray[1]
    $resultArray[1] = $resultArray[2]
    $resultArray[2] = $temp

    $temp = $resultArray[4]
    $resultArray[4] = $resultArray[5]
    $resultArray[5] = $temp

    $temp = $resultArray[6]
    $resultArray[6] = $resultArray[7]
    $resultArray[7] = $temp

    return $resultArray
}

function convertBytesToStr([Array]$bytes){
    return [String](-join ($bytes | ForEach-Object { '{0:x2}' -f $_ }))
}

function convertUUIDBytesToStr([Array]$bytes){
    return [String](convertBytesToStr $bytes[0..3]) +
    "-" + (convertBytesToStr $bytes[4..5]) +
    "-" + (convertBytesToStr $bytes[6..7]) +
    "-" +  (convertBytesToStr $bytes[8..9]) +
    "-" + (convertBytesToStr $bytes[10..15])
}

function generateUUIDv5([Guid]$namespace, [String]$name){
    $nsBytes = swapUUIDByteArray $namespace.ToByteArray() # Convert namespace uuid to network byte order.

    $hasher = New-Object System.Security.Cryptography.SHA1CryptoServiceProvider
    $bytes = $nsBytes + [Text.Encoding]::UTF8.GetBytes($name)
    $hash = $hasher.ComputeHash($bytes)
    $hash[6] = ($hash[6] -band 0x0F) -bor 0x50 # Embed version bits.
    $hash[8] = ($hash[8] -band 0x3F) -bor 0x80 # Embed variant bits.
    
    $uuidStr = convertUUIDBytesToStr $hash

    return [Guid]::new($uuidStr)
}

function generateUUIDv5Str([String]$namespace, [String]$name){
    $namespaceUuid = [Guid]::new($namespace)
    $uuid = generateUUIDv5 $namespaceUuid $name

    return $uuid.ToString()
}

generateUUIDv5Str $namespace $name

```

## 使用法

上記のスクリプトを任意のディレクトリに `generateUUIDv5.ps1` のような名前で保存してやり、

```powershell
generateUUIDv5.ps1 -namespace 'cfbff0d1-9375-5685-968c-48ce8b15ae17' -name 'example'
```

のように呼んでやると、

```
0782cc7f-ce8d-528a-a980-9295f3abe079
```

のように UUID v5 が返ってきます[^encoding]。

[^encoding]: 引数`-name`に与える文字列のエンコードがUTF-8でない場合、期待した結果と異なる結果になることがあります。

## 終わりに

今まで PowerShell スクリプトを書いたことがなかったために意外と苦戦しましたが、 [UUID の RFC](https://datatracker.ietf.org/doc/html/rfc4122) を読んだりと楽しんで書くことができました。

とてもニッチな小ネタですが、どなたかのお役に立てば幸いです。