Set-StrictMode -Version Latest
|
$ErrorActionPreference = 'Stop'
|
|
function Throw-AnaSemiJcsStopV001 {
|
param([Parameter(Mandatory = $true)][string]$StopCode)
|
$failure = New-Object InvalidOperationException($StopCode)
|
$failure.Data['StopCode'] = $StopCode
|
throw $failure
|
}
|
|
function Get-AnaSemiSha256BytesV001 {
|
param([Parameter(Mandatory = $true)][byte[]]$Bytes)
|
$sha = [Security.Cryptography.SHA256]::Create()
|
try { return ([BitConverter]::ToString($sha.ComputeHash($Bytes)).Replace('-', '').ToLowerInvariant()) }
|
finally { $sha.Dispose() }
|
}
|
|
function Get-AnaSemiSha256FileV001 {
|
param([Parameter(Mandatory = $true)][string]$Path)
|
$stream = [IO.File]::Open($Path, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::None)
|
try {
|
$sha = [Security.Cryptography.SHA256]::Create()
|
try { return ([BitConverter]::ToString($sha.ComputeHash($stream)).Replace('-', '').ToLowerInvariant()) }
|
finally { $sha.Dispose() }
|
}
|
finally { $stream.Dispose() }
|
}
|
|
function ConvertTo-AnaSemiJcsStringV001 {
|
param([Parameter(Mandatory = $true)][AllowEmptyString()][string]$Value)
|
$builder = New-Object Text.StringBuilder
|
[void]$builder.Append([char]34)
|
$index = 0
|
while ($index -lt $Value.Length) {
|
$code = [int]$Value[$index]
|
if ($code -ge 0xD800 -and $code -le 0xDBFF) {
|
if ($index + 1 -ge $Value.Length) { Throw-AnaSemiJcsStopV001 'STOP_JCS_INVALID_SURROGATE' }
|
$low = [int]$Value[$index + 1]
|
if ($low -lt 0xDC00 -or $low -gt 0xDFFF) { Throw-AnaSemiJcsStopV001 'STOP_JCS_INVALID_SURROGATE' }
|
[void]$builder.Append($Value[$index])
|
[void]$builder.Append($Value[$index + 1])
|
$index += 2
|
continue
|
}
|
if ($code -ge 0xDC00 -and $code -le 0xDFFF) { Throw-AnaSemiJcsStopV001 'STOP_JCS_INVALID_SURROGATE' }
|
if ($code -eq 8) { [void]$builder.Append('\b') }
|
elseif ($code -eq 9) { [void]$builder.Append('\t') }
|
elseif ($code -eq 10) { [void]$builder.Append('\n') }
|
elseif ($code -eq 12) { [void]$builder.Append('\f') }
|
elseif ($code -eq 13) { [void]$builder.Append('\r') }
|
elseif ($code -eq 34) { [void]$builder.Append('\"') }
|
elseif ($code -eq 92) { [void]$builder.Append('\\') }
|
elseif ($code -lt 32) { [void]$builder.Append(('\u{0:x4}' -f $code)) }
|
else { [void]$builder.Append($Value[$index]) }
|
$index++
|
}
|
[void]$builder.Append([char]34)
|
return $builder.ToString()
|
}
|
|
function ConvertTo-AnaSemiJcsTextV001 {
|
param([AllowNull()]$Value)
|
if ($null -eq $Value) { return 'null' }
|
if ($Value -is [string]) { return ConvertTo-AnaSemiJcsStringV001 $Value }
|
if ($Value -is [bool]) { if ($Value) { return 'true' }; return 'false' }
|
if ($Value -is [byte] -or $Value -is [sbyte] -or $Value -is [int16] -or $Value -is [uint16] -or
|
$Value -is [int32] -or $Value -is [uint32] -or $Value -is [int64] -or $Value -is [uint64]) {
|
return ([Convert]::ToString($Value, [Globalization.CultureInfo]::InvariantCulture))
|
}
|
if ($Value -is [single] -or $Value -is [double] -or $Value -is [decimal]) {
|
Throw-AnaSemiJcsStopV001 'STOP_JCS_UNSUPPORTED_NUMBER'
|
}
|
if ($Value -is [Collections.IDictionary]) {
|
$keys = New-Object Collections.Generic.List[string]
|
$ordinal = New-Object 'Collections.Generic.HashSet[string]' ([StringComparer]::Ordinal)
|
$ignoreCase = New-Object 'Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase)
|
foreach ($rawKey in $Value.Keys) {
|
if ($rawKey -isnot [string]) { Throw-AnaSemiJcsStopV001 'STOP_JCS_DUPLICATE_KEY' }
|
$key = [string]$rawKey
|
if (-not $ordinal.Add($key) -or -not $ignoreCase.Add($key)) { Throw-AnaSemiJcsStopV001 'STOP_JCS_DUPLICATE_KEY' }
|
[void]$keys.Add($key)
|
}
|
$array = $keys.ToArray()
|
[Array]::Sort($array, [StringComparer]::Ordinal)
|
$parts = New-Object Collections.Generic.List[string]
|
foreach ($key in $array) {
|
[void]$parts.Add((ConvertTo-AnaSemiJcsStringV001 $key) + ':' + (ConvertTo-AnaSemiJcsTextV001 $Value[$key]))
|
}
|
return '{' + [string]::Join(',', $parts.ToArray()) + '}'
|
}
|
if ($Value -is [Collections.IEnumerable]) {
|
$items = New-Object Collections.Generic.List[string]
|
foreach ($item in $Value) { [void]$items.Add((ConvertTo-AnaSemiJcsTextV001 $item)) }
|
return '[' + [string]::Join(',', $items.ToArray()) + ']'
|
}
|
$dictionary = [ordered]@{}
|
$seen = New-Object 'Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase)
|
foreach ($property in $Value.PSObject.Properties) {
|
if (-not $seen.Add([string]$property.Name)) { Throw-AnaSemiJcsStopV001 'STOP_JCS_DUPLICATE_KEY' }
|
$dictionary[[string]$property.Name] = $property.Value
|
}
|
if ($dictionary.Count -eq 0) { Throw-AnaSemiJcsStopV001 'STOP_JCS_PARSE_OR_CANONICAL_DRIFT' }
|
return ConvertTo-AnaSemiJcsTextV001 $dictionary
|
}
|
|
function Test-AnaSemiJsonObjectKeysV001 {
|
param([Parameter(Mandatory = $true)][string]$Text)
|
$stack = New-Object Collections.Stack
|
$index = 0
|
while ($index -lt $Text.Length) {
|
$ch = $Text[$index]
|
if ([char]::IsWhiteSpace($ch)) { $index++; continue }
|
if ($ch -eq [char]34) {
|
$start = $index
|
$index++
|
$escaped = $false
|
while ($index -lt $Text.Length) {
|
$current = $Text[$index]
|
if ($escaped) { $escaped = $false; $index++; continue }
|
if ($current -eq [char]92) { $escaped = $true; $index++; continue }
|
if ($current -eq [char]34) { break }
|
$index++
|
}
|
if ($index -ge $Text.Length) { Throw-AnaSemiJcsStopV001 'STOP_JCS_PARSE_OR_CANONICAL_DRIFT' }
|
$token = $Text.Substring($start, ($index - $start + 1))
|
$index++
|
if ($stack.Count -gt 0) {
|
$context = $stack.Peek()
|
if ($context.type -ceq 'object' -and $context.expect_key) {
|
$probe = $index
|
while ($probe -lt $Text.Length -and [char]::IsWhiteSpace($Text[$probe])) { $probe++ }
|
if ($probe -ge $Text.Length -or $Text[$probe] -ne ':') { Throw-AnaSemiJcsStopV001 'STOP_JCS_PARSE_OR_CANONICAL_DRIFT' }
|
try { $decoded = ConvertFrom-Json -InputObject $token -ErrorAction Stop }
|
catch { Throw-AnaSemiJcsStopV001 'STOP_JCS_PARSE_OR_CANONICAL_DRIFT' }
|
$key = [string]$decoded
|
if (-not $context.ordinal.Add($key) -or -not $context.ignore_case.Add($key)) { Throw-AnaSemiJcsStopV001 'STOP_JCS_DUPLICATE_KEY' }
|
$context.expect_key = $false
|
}
|
}
|
continue
|
}
|
if ($ch -eq '{') {
|
$stack.Push([pscustomobject]@{
|
type = 'object'
|
expect_key = $true
|
ordinal = New-Object 'Collections.Generic.HashSet[string]' ([StringComparer]::Ordinal)
|
ignore_case = New-Object 'Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase)
|
})
|
$index++
|
continue
|
}
|
if ($ch -eq '[') {
|
$stack.Push([pscustomobject]@{ type = 'array'; expect_key = $false })
|
$index++
|
continue
|
}
|
if ($ch -eq '}' -or $ch -eq ']') {
|
if ($stack.Count -eq 0) { Throw-AnaSemiJcsStopV001 'STOP_JCS_PARSE_OR_CANONICAL_DRIFT' }
|
[void]$stack.Pop()
|
$index++
|
continue
|
}
|
if ($ch -eq ',' -and $stack.Count -gt 0) {
|
$context = $stack.Peek()
|
if ($context.type -ceq 'object') { $context.expect_key = $true }
|
$index++
|
continue
|
}
|
$index++
|
}
|
if ($stack.Count -ne 0) { Throw-AnaSemiJcsStopV001 'STOP_JCS_PARSE_OR_CANONICAL_DRIFT' }
|
}
|
|
function Test-AnaSemiJcsBytesInternalV001 {
|
param([Parameter(Mandatory = $true)][byte[]]$Bytes)
|
if ($Bytes.Length -ge 3 -and $Bytes[0] -eq 239 -and $Bytes[1] -eq 187 -and $Bytes[2] -eq 191) {
|
Throw-AnaSemiJcsStopV001 'STOP_JCS_PARSE_OR_CANONICAL_DRIFT'
|
}
|
try { $text = (New-Object Text.UTF8Encoding($false, $true)).GetString($Bytes) }
|
catch { Throw-AnaSemiJcsStopV001 'STOP_JCS_PARSE_OR_CANONICAL_DRIFT' }
|
if ($text.Contains('\\\\')) { Throw-AnaSemiJcsStopV001 'STOP_JCS_STRING_ESCAPE_DRIFT' }
|
if ($text -match '\\\\u[dD][89aAbB][0-9a-fA-F]{2}(?!\\\\u[dD][c-fC-F][0-9a-fA-F]{2})') {
|
Throw-AnaSemiJcsStopV001 'STOP_JCS_INVALID_SURROGATE'
|
}
|
Test-AnaSemiJsonObjectKeysV001 $text
|
try { $value = ConvertFrom-Json -InputObject $text -ErrorAction Stop }
|
catch {
|
if ($text -match '\\u[dD][89aAbB][0-9a-fA-F]{2}(?!\\u[dD][c-fC-F][0-9a-fA-F]{2})') {
|
Throw-AnaSemiJcsStopV001 'STOP_JCS_INVALID_SURROGATE'
|
}
|
Throw-AnaSemiJcsStopV001 'STOP_JCS_PARSE_OR_CANONICAL_DRIFT'
|
}
|
$canonical = ConvertTo-AnaSemiJcsUtf8BytesV001 $value
|
if ($canonical.Length -ne $Bytes.Length) { Throw-AnaSemiJcsStopV001 'STOP_JCS_PARSE_OR_CANONICAL_DRIFT' }
|
for ($index = 0; $index -lt $Bytes.Length; $index++) {
|
if ($canonical[$index] -ne $Bytes[$index]) { Throw-AnaSemiJcsStopV001 'STOP_JCS_PARSE_OR_CANONICAL_DRIFT' }
|
}
|
return [pscustomobject]@{
|
bytes = [uint64]$Bytes.Length
|
sha256 = Get-AnaSemiSha256BytesV001 $Bytes
|
status = 'PASS'
|
stop_code = $null
|
}
|
}
|
|
function ConvertTo-AnaSemiJcsUtf8BytesV001 {
|
[CmdletBinding()]
|
param([Parameter(Mandatory = $true)][AllowNull()]$Value)
|
$text = ConvertTo-AnaSemiJcsTextV001 $Value
|
return ,(New-Object Text.UTF8Encoding($false, $true)).GetBytes($text)
|
}
|
|
function Write-AnaSemiJcsCreateNewV001 {
|
[CmdletBinding()]
|
param(
|
[Parameter(Mandatory = $true)][string]$Path,
|
[Parameter(Mandatory = $true)][AllowNull()]$Value
|
)
|
$absolute = [IO.Path]::GetFullPath($Path)
|
$bytes = ConvertTo-AnaSemiJcsUtf8BytesV001 $Value
|
$stream = $null
|
try {
|
$stream = New-Object IO.FileStream($absolute, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
|
$stream.Write($bytes, 0, $bytes.Length)
|
$stream.Flush($true)
|
}
|
catch [IO.IOException] {
|
if (Test-Path -LiteralPath $absolute) { Throw-AnaSemiJcsStopV001 'STOP_JCS_TARGET_ALREADY_EXISTS' }
|
throw
|
}
|
finally {
|
if ($null -ne $stream) { $stream.Dispose() }
|
}
|
$verification = Test-AnaSemiJcsFileV001 -Path $absolute -SchemaId ''
|
return [pscustomobject]@{
|
bytes = [uint64]$verification.bytes
|
path = $absolute
|
sha256 = [string]$verification.sha256
|
status = 'PASS'
|
stop_code = $null
|
}
|
}
|
|
function Test-AnaSemiJcsFileV001 {
|
[CmdletBinding()]
|
param(
|
[Parameter(Mandatory = $true)][string]$Path,
|
[Parameter(Mandatory = $true)][AllowEmptyString()][string]$SchemaId
|
)
|
$absolute = [IO.Path]::GetFullPath($Path)
|
$bytes = [IO.File]::ReadAllBytes($absolute)
|
$result = Test-AnaSemiJcsBytesInternalV001 $bytes
|
if (-not [string]::IsNullOrEmpty($SchemaId)) {
|
try { $value = ConvertFrom-Json -InputObject ((New-Object Text.UTF8Encoding($false, $true)).GetString($bytes)) -ErrorAction Stop }
|
catch { Throw-AnaSemiJcsStopV001 'STOP_JCS_PARSE_OR_CANONICAL_DRIFT' }
|
if ($null -eq $value.PSObject.Properties['schema_id'] -or [string]$value.schema_id -cne $SchemaId) {
|
Throw-AnaSemiJcsStopV001 'STOP_JCS_PARSE_OR_CANONICAL_DRIFT'
|
}
|
}
|
return [pscustomobject]@{
|
bytes = [uint64]$result.bytes
|
path = $absolute
|
sha256 = [string]$result.sha256
|
status = 'PASS'
|
stop_code = $null
|
}
|
}
|
|
Export-ModuleMember -Function ConvertTo-AnaSemiJcsUtf8BytesV001,Write-AnaSemiJcsCreateNewV001,Test-AnaSemiJcsFileV001
|