Private/Read-EFBaselineFile.ps1
|
function Read-EFBaselineFile { [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Path, [AllowNull()] [string]$ExpectedSha256, [switch]$RequireDirectPath, [ValidateRange(1, 104857600)] [int]$MaximumSizeBytes = 1048576 ) $fileItem = Get-Item -LiteralPath $Path -Force -ErrorAction Stop if ($fileItem.PSIsContainer) { throw [System.IO.FileNotFoundException]::new("Baseline file '$Path' was not found.") } if ($RequireDirectPath) { $currentItem = $fileItem while ($null -ne $currentItem) { if (($currentItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw [System.Security.SecurityException]::new( "Baseline '$Path' passes through a link or reparse point. Use a direct local path for elevated or hash-verified use." ) } if ($currentItem -is [IO.FileInfo]) { $currentItem = $currentItem.Directory } else { $currentItem = $currentItem.Parent } } } $stream = $null try { # FileShare.Read prevents the opened file from being replaced or modified while # the one buffer used for both hashing and JSON parsing is captured. $stream = [IO.FileStream]::new( $fileItem.FullName, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read ) if ($stream.Length -gt $MaximumSizeBytes) { throw [System.IO.InvalidDataException]::new( "Baseline '$Path' is $($stream.Length) bytes. The maximum supported baseline size is $MaximumSizeBytes bytes." ) } $length = [int]$stream.Length $bytes = [byte[]]::new($length) $offset = 0 while ($offset -lt $length) { $readCount = $stream.Read($bytes, $offset, $length - $offset) if ($readCount -eq 0) { throw [System.IO.EndOfStreamException]::new( "Baseline '$Path' ended before its declared length could be read." ) } $offset += $readCount } } finally { if ($null -ne $stream) { $stream.Dispose() } } $sha256 = [Security.Cryptography.SHA256]::Create() try { $actualSha256 = ([BitConverter]::ToString($sha256.ComputeHash($bytes))).Replace('-', '') } finally { $sha256.Dispose() } if (-not [string]::IsNullOrWhiteSpace($ExpectedSha256)) { $normalizedExpectedSha256 = $ExpectedSha256.Trim() if ($normalizedExpectedSha256 -notmatch '^[A-Fa-f0-9]{64}$') { throw [System.ArgumentException]::new('ExpectedSha256 must contain exactly 64 hexadecimal characters.') } if (-not [string]::Equals( $actualSha256, $normalizedExpectedSha256, [StringComparison]::OrdinalIgnoreCase )) { throw [System.IO.InvalidDataException]::new( "Baseline '$Path' failed SHA-256 verification. Expected $($normalizedExpectedSha256.ToUpperInvariant()), but read $actualSha256." ) } } $textOffset = 0 if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $textOffset = 3 } try { $strictUtf8 = [Text.UTF8Encoding]::new($false, $true) $text = $strictUtf8.GetString($bytes, $textOffset, $bytes.Length - $textOffset) } catch [Text.DecoderFallbackException] { throw [System.IO.InvalidDataException]::new( "Baseline '$Path' is not valid UTF-8 text.", $_.Exception ) } return [pscustomobject]@{ Text = $text Sha256 = $actualSha256 LengthBytes = $bytes.Length } } |