Skip to content

Instantly share code, notes, and snippets.

@maskati
Last active January 2, 2025 08:37
Show Gist options
  • Save maskati/23f329e7db43c00184143f04498e537c to your computer and use it in GitHub Desktop.
Save maskati/23f329e7db43c00184143f04498e537c to your computer and use it in GitHub Desktop.
Calculating the Bicep `uniqueString` hash locally using PowerShell

Calculating the Bicep uniqueString hash locally using PowerShell

The Bicep uniqueString as well as the ARM uniqueString function:

Creates a deterministic hash string based on the values provided as parameters

The actual function implementation is not documented, but is (almost certainly) a variant of the Murmur hash algorithm that maps the provided string parameters to a 64 bit hash and returns a 13 character Base32-like encoding of this hash.

The function:

  1. Concatenates the string parameters with a dash -
  2. UTF8 encodes the resulting concatenated string
  3. Calculates a 64-bit hash of the encoded bytes using a variant of the Murmur hash algorithm
  4. Encodes the 64-bit hash as a 13 character Base32-like string using a custom Base32 alphabet a-z,2-7

Warning

Since the ARM/Bicep function is undocumented there are no guarantees the below implementation correct. However it most likely is correct, since it is difficult for Microsoft to change the hash output without impacting existing ARM and Bicep templates.

The implementation is derived from Azure.Deployments.Expression.Functions.BuiltIn.UniqueStringFunction in Azure.Deployments.Expression as well as Microsoft.WindowsAzure.ResourceStack.Common.Algorithms.ComputeHash in Microsoft.PowerPlatform.ResourceStack. Both are MIT licensed.

Note

As an aside, the same hash algorithm is also used to calulate the deployment template hash in Bicep using Azure.Deployments.Core.Helpers.TemplateHelpers.ComputeTemplateHash in Azure.Deployments.Core. The hash is based on an uppercase minified JSON representation of the template. The actual templateHash value is the string representation of the 64-bit unsigned integer hash, which is why template hashes are numeric strings.

The PowerShell script azure-powershell-uniquestring.ps1 adds a Bicep type with a UniqueString static method simulating the Bicep uniqueString function.

To verify, deploying the following Bicep template:

output empty string = uniqueString('')
output test string = uniqueString('test')
output twoparams string = uniqueString('test', 'test2')
output threeparams string = uniqueString('test', 'test2', 'test3')

Gives the result:

"empty": "aaaaaaaaaaaaa"
"test": "rbgf3xv4ufgzg"
"twoparams": "pivml2tg5alca"
"threeparams": "bqgd334z2uj64"

Which matches the output given by PowerShell:

PS C:\> [Bicep]::UniqueString("")
aaaaaaaaaaaaa
PS C:\> [Bicep]::UniqueString("test")
rbgf3xv4ufgzg
PS C:\> [Bicep]::UniqueString("test", "test2")
pivml2tg5alca
PS C:\> [Bicep]::UniqueString("test", "test2", "test3")
bqgd334z2uj64
Add-Type -TypeDefinition @"
// This code is licensed under https://licenses.nuget.org/MIT.
// Derived from works (c) Microsoft Corporation licensed under https://licenses.nuget.org/MIT.
// https://www.nuget.org/packages/Azure.Deployments.Expression/
// https://www.nuget.org/packages/Microsoft.PowerPlatform.ResourceStack/
public class Bicep
{
public static string UniqueString(params string[] values)
{
var hyphenConcatentedValue = string.Join('-', values);
var utf8bytes = System.Text.Encoding.UTF8.GetBytes(hyphenConcatentedValue);
var hash = MurmurHash64(utf8bytes);
var uniqueString = Base32Encode(hash);
return uniqueString;
}
private static ulong MurmurHash64(byte[] data, uint seed = 0u)
{
int length = data.Length;
uint h1 = seed;
uint h2 = seed;
int index;
for (index = 0; index + 7 < length; index += 8)
{
uint k2 = (uint)(data[index] | (data[index + 1] << 8) | (data[index + 2] << 16) | (data[index + 3] << 24));
uint k4 = (uint)(data[index + 4] | (data[index + 5] << 8) | (data[index + 6] << 16) | (data[index + 7] << 24));
k2 *= 597399067;
k2 = RotateLeft32(k2, 15);
k2 *= 2869860233u;
h1 ^= k2;
h1 = RotateLeft32(h1, 19);
h1 += h2;
h1 = h1 * 5 + 1444728091;
k4 *= 2869860233u;
k4 = RotateLeft32(k4, 17);
k4 *= 597399067;
h2 ^= k4;
h2 = RotateLeft32(h2, 13);
h2 += h1;
h2 = h2 * 5 + 197830471;
}
int tail = length - index;
if (tail > 0)
{
uint k1 = ((tail >= 4) ? ((uint)(data[index] | (data[index + 1] << 8) | (data[index + 2] << 16) | (data[index + 3] << 24))) : (tail switch
{
2 => (uint)(data[index] | (data[index + 1] << 8)),
3 => (uint)(data[index] | (data[index + 1] << 8) | (data[index + 2] << 16)),
_ => data[index],
}));
k1 *= 597399067;
k1 = RotateLeft32(k1, 15);
k1 *= 2869860233u;
h1 ^= k1;
if (tail > 4)
{
uint k3 = (uint)(tail switch
{
6 => data[index + 4] | (data[index + 5] << 8),
7 => data[index + 4] | (data[index + 5] << 8) | (data[index + 6] << 16),
_ => data[index + 4],
} * -1425107063);
k3 = RotateLeft32(k3, 17);
k3 *= 597399067;
h2 ^= k3;
}
}
h1 ^= (uint)length;
h2 ^= (uint)length;
h1 += h2;
h2 += h1;
h1 ^= h1 >> 16;
h1 *= 2246822507u;
h1 ^= h1 >> 13;
h1 *= 3266489909u;
h1 ^= h1 >> 16;
h2 ^= h2 >> 16;
h2 *= 2246822507u;
h2 ^= h2 >> 13;
h2 *= 3266489909u;
h2 ^= h2 >> 16;
h1 += h2;
h2 += h1;
return ((ulong)h2 << 32) | h1;
}
private static uint RotateLeft32(uint value, int count)
{
return (value << count) | (value >> 32 - count);
}
private static string Base32Encode(ulong input)
{
const string charset = "abcdefghijklmnopqrstuvwxyz234567";
var sb = new System.Text.StringBuilder();
for (int index = 0; index < 13; index++)
{
sb.Append(charset[(int)(input >> 59)]);
input <<= 5;
}
return sb.ToString();
}
}
"@
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment