diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ad68d3..4f98af6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## String utilities restoration (unreleased) + +- Restore character conversion, string checksums and SHA-256 hashing. +- Preserve the character map while fixing index leakage and default space handling. +- Copy custom maps; normalize text and process Unicode text elements. +- Retain historical MD5 checksum formatting and allow stronger digest selection. +- Default SHA-256 output to hexadecimal; expose LegacyFormat for old comparisons. +- Add behavioral and known-vector tests. + ## API request restoration (unreleased) - Restore New-ApiRequest with independent optional-parameter handling. diff --git a/IT-ToolBox.psd1 b/IT-ToolBox.psd1 index 2d59abd..7580717 100644 --- a/IT-ToolBox.psd1 +++ b/IT-ToolBox.psd1 @@ -24,6 +24,9 @@ 'New-RandomPassword' 'New-PhoneticPassword' 'New-ApiRequest' + 'New-StringConversion' + 'Get-StringCheckSum' + 'Get-StringHashCode' ) CmdletsToExport = @() VariablesToExport = @() diff --git a/IT-ToolBox.psm1 b/IT-ToolBox.psm1 index 4540fe4..99f6894 100644 --- a/IT-ToolBox.psm1 +++ b/IT-ToolBox.psm1 @@ -24,4 +24,7 @@ Export-ModuleMember -Function @( 'New-RandomPassword' 'New-PhoneticPassword' 'New-ApiRequest' + 'New-StringConversion' + 'Get-StringCheckSum' + 'Get-StringHashCode' ) diff --git a/Private/Get-ITToolBoxCharacterMap.ps1 b/Private/Get-ITToolBoxCharacterMap.ps1 new file mode 100644 index 0000000..d5a2f91 --- /dev/null +++ b/Private/Get-ITToolBoxCharacterMap.ps1 @@ -0,0 +1,148 @@ +function Get-ITToolBoxCharacterMap { + # Retain the original domain-specific mapping; this is not universal transliteration. + return @{ + + # a + 'æ' = 'a' + 'à' = 'a' + 'â' = 'a' + 'ã' = 'a' + 'å' = 'a' + 'ā' = 'a' + 'ă' = 'a' + 'ą' = 'a' + 'ä' = 'a' + 'á' = 'a' + + # b + 'ƀ' = 'b' + 'ƃ' = 'b' + + # Tone six + 'ƅ' = 'b' + + # c + 'ç' = 'c' + 'ć' = 'c' + 'ĉ' = 'c' + 'ċ' = 'c' + 'č' = 'c' + 'ƈ' = 'c' + + # d + 'ď' = 'd' + 'đ' = 'd' + 'ƌ' = 'd' + + # e + 'è' = 'e' + 'é' = 'e' + 'ê' = 'e' + 'ë' = 'e' + 'ē' = 'e' + 'ĕ' = 'e' + 'ė' = 'e' + 'ę' = 'e' + 'ě' = 'e' + '&' = 'e' + + # g + 'ĝ' = 'e' + 'ğ' = 'e' + 'ġ' = 'e' + 'ģ' = 'e' + + # h + 'ĥ' = 'h' + 'ħ' = 'h' + + # i + 'ì' = 'i' + 'í' = 'i' + 'î' = 'i' + 'ï' = 'i' + 'ĩ' = 'i' + 'ī' = 'i' + 'ĭ' = 'i' + 'į' = 'i' + 'ı' = 'i' + + # j + 'ij' = 'j' + 'ĵ' = 'j' + + # k + 'ķ' = 'k' + 'ĸ' = 'k' + + # l + 'ĺ' = 'l' + 'ļ' = 'l' + 'ľ' = 'l' + 'ŀ' = 'l' + 'ł' = 'l' + + # n + 'ñ' = 'n' + 'ń' = 'n' + 'ņ' = 'n' + 'ň' = 'n' + 'ʼn' = 'n' + 'ŋ' = 'n' + + # o + 'ð' = 'o' + 'ó' = 'o' + 'õ' = 'o' + 'ô' = 'o' + 'ö' = 'o' + 'ø' = 'o' + 'ō' = 'o' + 'ŏ' = 'o' + 'ő' = 'o' + 'œ' = 'o' + + # r + 'ŕ' = 'r' + 'ŗ' = 'r' + 'ř' = 'r' + + # s + 'ś' = 's' + 'ŝ' = 's' + 'ş' = 's' + 'š' = 's' + 'ß' = 'ss' + 'ſ' = 's' + + # t + 'ţ' = 't' + 'ť' = 't' + 'ŧ' = 't' + + # u + 'ù' = 'u' + 'ú' = 'u' + 'û' = 'u' + 'ü' = 'u' + 'ũ' = 'u' + 'ū' = 'u' + 'ŭ' = 'u' + 'ů' = 'u' + 'ű' = 'u' + 'ų' = 'u' + + # w + 'ŵ' = 'w' + + # y + 'ý' = 'y' + 'ÿ' = 'y' + 'ŷ' = 'y' + + # z + 'ź' = 'z' + 'ż' = 'z' + 'ž' = 'z' + } +} diff --git a/Private/Get-ITToolBoxStringDigest.ps1 b/Private/Get-ITToolBoxStringDigest.ps1 new file mode 100644 index 0000000..a34afc0 --- /dev/null +++ b/Private/Get-ITToolBoxStringDigest.ps1 @@ -0,0 +1,12 @@ +function Get-ITToolBoxStringDigest { + param([string]$Text, [string]$Algorithm) + [byte[]]$bytes = [System.Text.Encoding]::UTF8.GetBytes($Text) + [byte[]]$digest = switch ($Algorithm.ToUpperInvariant()) { + 'MD5' { [System.Security.Cryptography.MD5]::HashData($bytes) } + 'SHA256' { [System.Security.Cryptography.SHA256]::HashData($bytes) } + 'SHA384' { [System.Security.Cryptography.SHA384]::HashData($bytes) } + 'SHA512' { [System.Security.Cryptography.SHA512]::HashData($bytes) } + default { throw [ArgumentException]::new('Unsupported digest algorithm.') } + } + return ,$digest +} diff --git a/Public/Get-StringCheckSum.ps1 b/Public/Get-StringCheckSum.ps1 new file mode 100644 index 0000000..af0f256 --- /dev/null +++ b/Public/Get-StringCheckSum.ps1 @@ -0,0 +1,19 @@ +function Get-StringCheckSum { + <# + .SYNOPSIS + Returns a UTF-8 text checksum as uppercase hyphen-separated hexadecimal bytes. + .DESCRIPTION + MD5 remains the default for compatibility with existing checksums. It is for + non-security comparison only. SHA256, SHA384 and SHA512 can be selected explicitly. + An unkeyed digest does not authenticate data or provide password storage. + Text is encoded as supplied without Unicode normalization or a BOM. + #> + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$StringToCheck, + [ValidateSet('MD5','SHA256','SHA384','SHA512')][string]$Algorithm = 'MD5' + ) + [byte[]]$digest = Get-ITToolBoxStringDigest -Text $StringToCheck -Algorithm $Algorithm + return [BitConverter]::ToString($digest) +} diff --git a/Public/Get-StringHashCode.ps1 b/Public/Get-StringHashCode.ps1 new file mode 100644 index 0000000..44f3e89 --- /dev/null +++ b/Public/Get-StringHashCode.ps1 @@ -0,0 +1,26 @@ +function Get-StringHashCode { + <# + .SYNOPSIS + Returns the SHA-256 digest of UTF-8 text as 64 uppercase hexadecimal characters. + .DESCRIPTION + This is SHA-256, not .NET GetHashCode. LegacyFormat returns the historical + delimiter-free decimal byte concatenation for migration/comparison only. + Legacy output is an ambiguous encoding and should not be used for new identifiers. + No Unicode normalization is performed. This is not password hashing or authentication. + #> + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$StringToHash, + [switch]$LegacyFormat + ) + [byte[]]$digest = Get-ITToolBoxStringDigest -Text $StringToHash -Algorithm SHA256 + if ($LegacyFormat) { + $result = [System.Text.StringBuilder]::new() + foreach ($value in $digest) { + [void]$result.Append($value.ToString([System.Globalization.CultureInfo]::InvariantCulture)) + } + return $result.ToString() + } + return [Convert]::ToHexString($digest) +} diff --git a/Public/New-StringConversion.ps1 b/Public/New-StringConversion.ps1 new file mode 100644 index 0000000..cef934d --- /dev/null +++ b/Public/New-StringConversion.ps1 @@ -0,0 +1,52 @@ +function New-StringConversion { + <# + .SYNOPSIS + Converts mapped characters and replaces unsupported text elements. + .DESCRIPTION + Retains the historical character map and permitted ASCII punctuation. + Default spaces become hyphens. Use IgnoreSpaces, RemoveSpaces or ReplaceSpaces + to select another policy. Custom maps replace the default map and are not mutated. + Input is normalized to Unicode Form C; each remaining text element is processed + once. This is a domain-specific conversion, not general Unicode transliteration. + Returns exactly one string, without internal collection indices. + #> + [CmdletBinding(DefaultParameterSetName = 'ReplaceSpaces')] + [OutputType([string])] + param( + [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$StringToConvert, + [ValidateNotNull()][hashtable]$UnicodeHashTable, + [Parameter(ParameterSetName = 'IgnoreSpaces')][switch]$IgnoreSpaces, + [Parameter(ParameterSetName = 'RemoveSpaces')][switch]$RemoveSpaces, + [Parameter(ParameterSetName = 'ReplaceSpaces')][AllowEmptyString()][ValidateNotNull()][string]$ReplaceSpaces = '-', + [ValidateNotNullOrEmpty()][string]$UnknownCharacter = '?' + ) + if ($PSBoundParameters.ContainsKey('UnicodeHashTable')) { + $map = @{} + foreach ($key in $UnicodeHashTable.Keys) { + $normalizedKey = ([string]$key).Normalize([System.Text.NormalizationForm]::FormC).ToLowerInvariant() + if ([string]::IsNullOrEmpty($normalizedKey)) { + throw [ArgumentException]::new('Character map keys cannot be empty.') + } + $map[$normalizedKey] = [string]$UnicodeHashTable[$key] + } + } + else { $map = Get-ITToolBoxCharacterMap } + $map[' '] = if ($IgnoreSpaces) { ' ' } elseif ($RemoveSpaces) { '' } else { $ReplaceSpaces } + $normalized = $StringToConvert.Normalize([System.Text.NormalizationForm]::FormC) + $elements = [System.Globalization.StringInfo]::GetTextElementEnumerator($normalized) + $result = [System.Text.StringBuilder]::new() + while ($elements.MoveNext()) { + [string]$element = $elements.GetTextElement() + [string]$lower = $element.ToLowerInvariant() + if ($map.ContainsKey($lower)) { + [string]$replacement = $map[$lower] + if ($element -cne $lower) { $replacement = $replacement.ToUpperInvariant() } + [void]$result.Append($replacement) + } + elseif ($element -cmatch '^[0-9a-zA-Z!#$@.''^_~-]$') { + [void]$result.Append($element) + } + else { [void]$result.Append($UnknownCharacter) } + } + return $result.ToString() +} diff --git a/README.md b/README.md index be9e3a6..e6b5489 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,9 @@ The foundation imports without WinSCP, GnuPG, Active Directory or Exchange depen | New-RandomPassword | Secure random passwords from the historical mixed alphabet | | New-PhoneticPassword | Passwords with phonetic spelling and exact category counts | | New-ApiRequest | OAuth-style form/JSON client requests | +| New-StringConversion | Domain-specific character mapping and space handling | +| Get-StringCheckSum | UTF-8 checksums with selectable digest algorithms | +| Get-StringHashCode | Standard SHA-256 hex with explicit legacy output mode | ```powershell Import-Module ./IT-ToolBox.psd1 @@ -46,10 +49,10 @@ module variables. Redaction is opt-in and does not guarantee detection of every - SCP and GnuPG wrappers and bundled WinSCP binaries are removed. Separate modules will own file transfer and OpenPGP; no replacement is bundled here. - `Legacy/` retains string encryption, Exchange and script-context helpers for reference. -- `Staging/v3/` retains 12 candidate commands pending tests and compatibility fixes. +- `Staging/v3/` retains 9 candidate commands pending tests and compatibility fixes. These include existing validators, strings, password generation, API requests, registry, uptime and AD utilities. They are not currently exported. -- Only the fifteen listed commands are exported. Private helpers, variables and aliases +- Only the eighteen listed commands are exported. Private helpers, variables and aliases are not exported. Existing calls to other v2 commands require the v2 release until those commands return to the supported API. - The module GUID and Git history are preserved. @@ -179,3 +182,27 @@ Connection timeout defaults to 30 seconds and can be configured with wall-clock deadline. Failures terminate with the original Invoke-RestMethod error; no retries or custom logging are added. Tests mock all HTTP calls: real endpoint behavior and TLS are not integration-tested. + +## String conversion and hashing + +`New-StringConversion` retains the original character map, custom-map parameter, +unknown-character replacement and space options. Its default now matches the +documented behavior: spaces become hyphens. It returns exactly one string rather +than leaking collection indices. The map remains domain-specific (for example, +`&` maps to `e`); it is not a general-purpose transliteration or safe-path generator. + +Custom maps replace the default map and are copied internally. Space policy +overrides an existing space mapping without mutating the caller's table. Conversion +normalizes text to Form C and handles unsupported Unicode text elements once, +including supplementary characters. This changes output for decomposed accents +and surrogate pairs compared with the old UTF-16 character loop. + +`Get-StringCheckSum` retains uppercase hyphen-separated MD5 output by default for +existing non-security comparisons. Select `-Algorithm SHA256`, `SHA384` or `SHA512` +when needed. MD5 is unsuitable for adversarial integrity checks. + +`Get-StringHashCode` returns 64 uppercase SHA-256 hexadecimal characters by default. +Use `-LegacyFormat` only when comparing with old delimiter-free decimal output. +The hash algorithm remains SHA-256; the default representation intentionally changes. +Both hash commands use UTF-8 without a BOM and do not normalize text. Neither is +a password-storage function or an authentication mechanism. diff --git a/Staging/v3/Get-StringCheckSum.ps1 b/Staging/v3/Get-StringCheckSum.ps1 deleted file mode 100644 index 0c9c94f..0000000 --- a/Staging/v3/Get-StringCheckSum.ps1 +++ /dev/null @@ -1,30 +0,0 @@ -function Get-StringCheckSum -{ -<# - .SYNOPSIS - Cmdlet calculate MD5 checksum of a string. - - .DESCRIPTION - Cmdlet calculate MD5 checksum of a string. - - .PARAMETER StringToCheck - A string representing the text for which the checksum should be calculated. - - .EXAMPLE - PS C:\> Get-StringCheckSum -StringToCheck 'Value1' -#> - - param - ( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string] - $StringToCheck - ) - - # Instantiate required objects - $md5Object = [System.Security.Cryptography.MD5CryptoServiceProvider]::Create() - $encodingObject = [System.Text.UTF8Encoding]::UTF8 - - return [System.BitConverter]::ToString($md5Object.ComputeHash($encodingObject.GetBytes($StringToCheck))) -} \ No newline at end of file diff --git a/Staging/v3/Get-StringHashCode.ps1 b/Staging/v3/Get-StringHashCode.ps1 deleted file mode 100644 index 8861b47..0000000 --- a/Staging/v3/Get-StringHashCode.ps1 +++ /dev/null @@ -1,43 +0,0 @@ -function Get-StringHashCode -{ -<# - .SYNOPSIS - Cmdlet will return hash of the supplied string. - - .DESCRIPTION - Cmdlet will return hash of the supplied string. - - .PARAMETER StringToHash - A string representing text to hash. - - .EXAMPLE - PS C:\> Get-StringHashCode -TextToHash 'value1' -#> - - param - ( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string] - $StringToHash - ) - - # Crete new string builder - [System.Text.StringBuilder]$hashString = [System.Text.StringBuilder]::new() - - # Get bytes - [byte[]]$hashText = [System.Text.Encoding]::UTF8.GetBytes($StringToHash) - - # Instantiate new object instance - [System.Security.Cryptography.SHA256Managed]$textHasher = New-Object -TypeName System.Security.Cryptography.SHA256Managed - - [array]$hashByteArray = $textHasher.ComputeHash($hashText) - - foreach ($byte in $hashByteArray) - { - # Append value - [void]($hashString.Append($byte.ToString())) - } - - return $hashString.ToString() -} \ No newline at end of file diff --git a/Staging/v3/New-StringConversion.ps1 b/Staging/v3/New-StringConversion.ps1 deleted file mode 100644 index 64a6c79..0000000 --- a/Staging/v3/New-StringConversion.ps1 +++ /dev/null @@ -1,338 +0,0 @@ -function New-StringConversion -{ - <# - .SYNOPSIS - Function to remove any non-unicode character from a string. - - .DESCRIPTION - Function is used to sanitize non-unicode characters from a string. - - Function supports custom characters map via the -UnicodeHashTable parameter accepting an hashtable of characters to replace. - - If characters not specified in the default character map are found they are replace with a question mark '?' unless a custom - unknown character is specified via -UnknownCharacter parameter. - - .PARAMETER StringToConvert - A string containing characters that need to be sanitized/converted. - - .PARAMETER UnicodeHashTable - An hashtable containing characters that should be replaced if parameter is not specified default values will be used. - - .PARAMETER IgnoreSpaces - By default spaces will be replaced with a dash '-' sign if paramter is specified function will not convert/take into consideraiotn - spaces in the string. - - .PARAMETER RemoveSpaces - If parameter is specified spaces will be removed from input string. - - .PARAMETER ReplaceSpaces - By default spaces will be replaced with a dash '-' sign if parameter is specified it is possible to specify character to use when - a space is encountered in the string. - - .PARAMETER UnknownCharacter - By default any special character not found in the UnicodeHashTable will be replaced with a question mark when parameter is used - it is possible to specify which character will be used for unknown entries. - - .EXAMPLE - PS C:\> New-StringConversion - - .NOTES - Additional information about the function. - #> - [CmdletBinding(DefaultParameterSetName = 'ReplaceSpaces', - ConfirmImpact = 'High', - HelpUri = 'https://PsCustomObject.github.io')] - param - ( - [Parameter(ParameterSetName = 'IgnoreSpaces', - Mandatory = $true)] - [Parameter(ParameterSetName = 'RemoveSpaces')] - [Parameter(ParameterSetName = 'ReplaceSpaces')] - [ValidateNotNullOrEmpty()] - [string] - $StringToConvert, - [Parameter(ParameterSetName = 'IgnoreSpaces')] - [Parameter(ParameterSetName = 'RemoveSpaces')] - [Parameter(ParameterSetName = 'ReplaceSpaces')] - [hashtable] - $UnicodeHashTable, - [Parameter(ParameterSetName = 'IgnoreSpaces')] - [switch] - $IgnoreSpaces, - [Parameter(ParameterSetName = 'RemoveSpaces')] - [switch] - $RemoveSpaces, - [Parameter(ParameterSetName = 'ReplaceSpaces')] - [ValidateNotNullOrEmpty()] - [string] - $ReplaceSpaces, - [Parameter(ParameterSetName = 'IgnoreSpaces')] - [Parameter(ParameterSetName = 'RemoveSpaces')] - [Parameter(ParameterSetName = 'ReplaceSpaces')] - [ValidateNotNullOrEmpty()] - [string] - $UnknownCharacter = '?' - ) - - begin - { - # Declare control variable - [bool]$isUpperCase = $false - - # Check if we should use custom array hash - if (-not ($PSBoundParameters.ContainsKey('UnicodeHashTable'))) - { - # Hashtable contaning special characters to replace - [hashtable]$unicodeHashTable = @{ - - # a - 'æ' = 'a' - 'à' = 'a' - 'â' = 'a' - 'ã' = 'a' - 'å' = 'a' - 'ā' = 'a' - 'ă' = 'a' - 'ą' = 'a' - 'ä' = 'a' - 'á' = 'a' - - # b - 'ƀ' = 'b' - 'ƃ' = 'b' - - # Tone six - 'ƅ' = 'b' - - # c - 'ç' = 'c' - 'ć' = 'c' - 'ĉ' = 'c' - 'ċ' = 'c' - 'č' = 'c' - 'ƈ' = 'c' - - # d - 'ď' = 'd' - 'đ' = 'd' - 'ƌ' = 'd' - - # e - 'è' = 'e' - 'é' = 'e' - 'ê' = 'e' - 'ë' = 'e' - 'ē' = 'e' - 'ĕ' = 'e' - 'ė' = 'e' - 'ę' = 'e' - 'ě' = 'e' - '&' = 'e' - - # g - 'ĝ' = 'e' - 'ğ' = 'e' - 'ġ' = 'e' - 'ģ' = 'e' - - # h - 'ĥ' = 'h' - 'ħ' = 'h' - - # i - 'ì' = 'i' - 'í' = 'i' - 'î' = 'i' - 'ï' = 'i' - 'ĩ' = 'i' - 'ī' = 'i' - 'ĭ' = 'i' - 'į' = 'i' - 'ı' = 'i' - - # j - 'ij' = 'j' - 'ĵ' = 'j' - - # k - 'ķ' = 'k' - 'ĸ' = 'k' - - # l - 'ĺ' = 'l' - 'ļ' = 'l' - 'ľ' = 'l' - 'ŀ' = 'l' - 'ł' = 'l' - - # n - 'ñ' = 'n' - 'ń' = 'n' - 'ņ' = 'n' - 'ň' = 'n' - 'ʼn' = 'n' - 'ŋ' = 'n' - - # o - 'ð' = 'o' - 'ó' = 'o' - 'õ' = 'o' - 'ô' = 'o' - 'ö' = 'o' - 'ø' = 'o' - 'ō' = 'o' - 'ŏ' = 'o' - 'ő' = 'o' - 'œ' = 'o' - - # r - 'ŕ' = 'r' - 'ŗ' = 'r' - 'ř' = 'r' - - # s - 'ś' = 's' - 'ŝ' = 's' - 'ş' = 's' - 'š' = 's' - 'ß' = 'ss' - 'ſ' = 's' - - # t - 'ţ' = 't' - 'ť' = 't' - 'ŧ' = 't' - - # u - 'ù' = 'u' - 'ú' = 'u' - 'û' = 'u' - 'ü' = 'u' - 'ũ' = 'u' - 'ū' = 'u' - 'ŭ' = 'u' - 'ů' = 'u' - 'ű' = 'u' - 'ų' = 'u' - - # w - 'ŵ' = 'w' - - # y - 'ý' = 'y' - 'ÿ' = 'y' - 'ŷ' = 'y' - - # z - 'ź' = 'z' - 'ż' = 'z' - 'ž' = 'z' - } - } - - switch ($PSBoundParameters.Keys) - { - 'IgnoreSpaces' - { - $UnicodeHashTable.Add(' ', ' ') - - break - } - 'ReplaceSpaces' - { - # Replace spaces with specified character - $UnicodeHashTable.Add(' ', $ReplaceSpaces) - - break - } - 'RemoveSpaces' - { - # Replace spaces with specified character - $UnicodeHashTable.Add(' ', '') - - break - } - } - - # Create new chararray - [System.Collections.ArrayList]$resultStringArray = @() - - # Set a regex for additional special characters - [string]$unicodeRegExString = "^([0-9a-zA-Z!#$@.'^_`~-])*$" - } - process - { - # Convert string to array - [array]$stringCharArray = $StringToConvert.ToCharArray() - - foreach ($character in $stringCharArray) - { - # Reset control variables - $isUpperCase = $false - - # Set Char ref with current value - [string]$currentChar = $character.ToString() - [string]$currentCharLower = $character.ToString().ToLower() - - # Get character case - if ($currentChar.CompareTo($currentCharLower) -eq 1) - { - $isUpperCase = $true - } - - # Check if character should be translated - if ($UnicodeHashTable.ContainsKey($currentCharLower) -eq $true) - { - # Get unicode equivalent - [string]$tmpChar = $UnicodeHashTable[$currentChar] - - # Set character case - switch ($isUpperCase) - { - $true - { - $resultStringArray.Add($tmpChar.ToUpper()) - - break - } - default - { - $resultStringArray.Add($tmpChar) - } - } - } - else - { - # Check if character should be translated - if (($currentCharLower -match $unicodeRegExString) -eq $false) - { - # Handle characters not in hash - $currentChar = $UnknownCharacter - - # Append to result array - $resultStringArray.Add($currentChar).ToString() - } - else - { - # Set character case - switch ($isUpperCase) - { - $true - { - $resultStringArray.Add($currentChar).ToString().ToUpper() - } - default - { - $resultStringArray.Add($currentChar).ToString() - } - } - } - } - } - } - end - { - # Return string - return (-join $resultStringArray) - } -} \ No newline at end of file diff --git a/Tests/Module.Tests.ps1 b/Tests/Module.Tests.ps1 index 96fb69e..e258425 100644 --- a/Tests/Module.Tests.ps1 +++ b/Tests/Module.Tests.ps1 @@ -10,7 +10,7 @@ Describe 'IT-ToolBox module boundary' { } It 'exports exactly the supported commands' { - $expected = @('New-LogEntry', 'New-Timer', 'Get-TimerStatus', 'Stop-Timer', 'Get-ElapsedTime', 'Test-FileName', 'Test-IsValidPath', 'Test-IsIP', 'Test-IsDate', 'New-StringEncryption', 'New-StringDecryption', 'New-RandomString', 'New-RandomPassword', 'New-PhoneticPassword', 'New-ApiRequest') | Sort-Object + $expected = @('New-LogEntry', 'New-Timer', 'Get-TimerStatus', 'Stop-Timer', 'Get-ElapsedTime', 'Test-FileName', 'Test-IsValidPath', 'Test-IsIP', 'Test-IsDate', 'New-StringEncryption', 'New-StringDecryption', 'New-RandomString', 'New-RandomPassword', 'New-PhoneticPassword', 'New-ApiRequest', 'New-StringConversion', 'Get-StringCheckSum', 'Get-StringHashCode') | Sort-Object $actual = @($module.ExportedFunctions.Keys | Sort-Object) ($actual -join ',') | Should -Be ($expected -join ',') $module.ExportedVariables.Count | Should -Be 0 diff --git a/Tests/StringUtilities.Tests.ps1 b/Tests/StringUtilities.Tests.ps1 new file mode 100644 index 0000000..66ab868 --- /dev/null +++ b/Tests/StringUtilities.Tests.ps1 @@ -0,0 +1,75 @@ +BeforeAll { + Import-Module (Join-Path $PSScriptRoot '../IT-ToolBox.psd1') -Force -ErrorAction Stop +} +Describe 'String conversion' { + It 'returns one string without internal array indices' { + $result = @(New-StringConversion -StringToConvert 'café') + $result.Count | Should -Be 1 + $result[0] | Should -BeExactly 'cafe' + } + It 'preserves mapped uppercase and multi-character replacements' { + New-StringConversion -StringToConvert 'CAFÉ Straße' | Should -BeExactly 'CAFE-Strasse' + New-StringConversion -StringToConvert 'ÀÉÖ' | Should -BeExactly 'AEO' + } + It 'supports default, preserved, removed and custom spaces' { + New-StringConversion -StringToConvert 'a b' | Should -BeExactly 'a-b' + New-StringConversion -StringToConvert 'a b' -IgnoreSpaces | Should -BeExactly 'a b' + New-StringConversion -StringToConvert 'a b' -RemoveSpaces | Should -BeExactly 'ab' + New-StringConversion -StringToConvert 'a b' -ReplaceSpaces '_' | Should -BeExactly 'a_b' + New-StringConversion -StringToConvert 'a b' -ReplaceSpaces '' | Should -BeExactly 'ab' + } + It 'does not mutate custom maps even if a space key exists' { + $map = @{ 'é' = 'ee'; ' ' = 'original' } + New-StringConversion -StringToConvert 'É é' -UnicodeHashTable $map -IgnoreSpaces | Should -BeExactly 'EE ee' + $map.Count | Should -Be 2 + $map[' '] | Should -BeExactly 'original' + } + It 'uses the custom map instead of silently merging defaults' { + New-StringConversion -StringToConvert 'éö' -UnicodeHashTable @{ 'é' = 'e' } | Should -BeExactly 'e?' + } + It 'normalizes decomposed accents and handles supplementary text elements once' { + New-StringConversion -StringToConvert "cafe$([char]0x301)" | Should -BeExactly 'cafe' + New-StringConversion -StringToConvert 'a🔐b' -UnknownCharacter '' | Should -BeExactly 'ab' + } + It 'preserves the historical permitted punctuation and special mappings' { + New-StringConversion -StringToConvert 'A1!#$@.''^_~-' | Should -BeExactly 'A1!#$@.''^_~-' + New-StringConversion -StringToConvert '&' | Should -BeExactly 'e' + New-StringConversion -StringToConvert ':' | Should -BeExactly '?' + } + It 'rejects conflicting space policies and empty map keys' { + { New-StringConversion -StringToConvert 'a b' -IgnoreSpaces -RemoveSpaces } | Should -Throw + { New-StringConversion -StringToConvert 'a' -UnicodeHashTable @{ '' = 'x' } } | Should -Throw '*keys cannot be empty*' + } +} +Describe 'Checksums and SHA-256 hashes' { + It 'preserves the known MD5 checksum format' { + Get-StringCheckSum -StringToCheck 'abc' | Should -BeExactly '90-01-50-98-3C-D2-4F-B0-D6-96-3F-7D-28-E1-7F-72' + } + It 'returns the standard SHA-256 test vector' { + Get-StringHashCode -StringToHash 'abc' | Should -BeExactly 'BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD' + Get-StringCheckSum -StringToCheck 'abc' -Algorithm SHA256 | Should -BeExactly 'BA-78-16-BF-8F-01-CF-EA-41-41-40-DE-5D-AE-22-23-B0-03-61-A3-96-17-7A-9C-B4-10-FF-61-F2-00-15-AD' + } + It 'matches the test vector' -TestCases @( + @{ Algorithm = 'SHA384'; Expected = 'CB-00-75-3F-45-A3-5E-8B-B5-A0-3D-69-9A-C6-50-07-27-2C-32-AB-0E-DE-D1-63-1A-8B-60-5A-43-FF-5B-ED-80-86-07-2B-A1-E7-CC-23-58-BA-EC-A1-34-C8-25-A7' } + @{ Algorithm = 'SHA512'; Expected = 'DD-AF-35-A1-93-61-7A-BA-CC-41-73-49-AE-20-41-31-12-E6-FA-4E-89-A9-7E-A2-0A-9E-EE-E6-4B-55-D3-9A-21-92-99-2A-27-4F-C1-A8-36-BA-3C-23-A3-FE-EB-BD-45-4D-44-23-64-3C-E8-0E-2A-9A-C9-4F-A5-4C-A4-9F' } + ) { + param($Algorithm, $Expected) + Get-StringCheckSum -StringToCheck 'abc' -Algorithm $Algorithm | Should -BeExactly $Expected + } + It 'preserves historical decimal output explicitly' { + Get-StringHashCode -StringToHash 'abc' -LegacyFormat | Should -BeExactly '186120221911431207234656564222931743435176397163150231221561801625597242021173' + } + It 'hashes UTF-8 Unicode without a BOM' { + Get-StringHashCode -StringToHash 'café 🔐' | Should -BeExactly '288B0200DB21EE3277629FA955AC8B340CEB775FCC303F89D90A4D1245309093' + } + It 'does not silently normalize text before hashing' { + $composed = Get-StringHashCode -StringToHash 'é' + $decomposed = Get-StringHashCode -StringToHash "e$([char]0x301)" + $composed | Should -Not -Be $decomposed + } + It 'validates inputs and algorithm selection' { + { Get-StringCheckSum -StringToCheck '' } | Should -Throw + { Get-StringHashCode -StringToHash '' } | Should -Throw + { Get-StringCheckSum -StringToCheck 'abc' -Algorithm 'SHA1' } | Should -Throw + } +}