Private/Read-EFBoundedTextTail.ps1
|
function Read-EFBoundedTextTail { [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Path, [Parameter(Mandatory)] [ValidateRange(1, 10000)] [int]$TailLines, [Parameter(Mandatory)] [ValidateSet('Utf8', 'Unicode', 'BigEndianUnicode', 'Ascii')] [string]$Encoding, [ValidateRange(1, 8388608)] [int]$MaximumDecodedCharacters = 8388608 ) $textEncoding = switch ($Encoding) { 'Utf8' { [Text.UTF8Encoding]::new($false, $true) } 'Unicode' { [Text.UnicodeEncoding]::new($false, $true, $true) } 'BigEndianUnicode' { [Text.UnicodeEncoding]::new($true, $true, $true) } 'Ascii' { [Text.Encoding]::GetEncoding( 'us-ascii', [Text.EncoderFallback]::ExceptionFallback, [Text.DecoderFallback]::ExceptionFallback ) } } $bytesPerCharacter = switch ($Encoding) { 'Utf8' { 4 } 'Ascii' { 1 } default { 2 } } $newlineAllowance = [int64]$TailLines * $(if ($Encoding -in @('Unicode', 'BigEndianUnicode')) { 4 } else { 2 }) $maximumBytes = ([int64]$MaximumDecodedCharacters + 1) * $bytesPerCharacter + $newlineAllowance + 4 $fileStream = [IO.FileStream]::new( $Path, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite ) try { $fileLength = [int64]$fileStream.Length if ($fileLength -eq 0) { return [pscustomobject]@{ Lines = @() BytesRead = 0 WasLimited = $false } } $bytesToRead = [int][math]::Min($fileLength, $maximumBytes) $startOffset = $fileLength - $bytesToRead if ($Encoding -in @('Unicode', 'BigEndianUnicode') -and $startOffset % 2 -ne 0) { $startOffset-- $bytesToRead++ } $buffer = [byte[]]::new($bytesToRead) $null = $fileStream.Seek($startOffset, [IO.SeekOrigin]::Begin) $totalRead = 0 while ($totalRead -lt $bytesToRead) { $read = $fileStream.Read($buffer, $totalRead, $bytesToRead - $totalRead) if ($read -eq 0) { break } $totalRead += $read } if ($totalRead -ne $bytesToRead) { throw [System.IO.IOException]::new('The text file changed or ended before the bounded tail read completed.') } $wasLimited = $startOffset -gt 0 if ($wasLimited) { $delimiterEnd = -1 if ($Encoding -in @('Utf8', 'Ascii')) { for ($index = 0; $index -lt $buffer.Length; $index++) { if ($buffer[$index] -eq 10) { $delimiterEnd = $index + 1 break } if ($buffer[$index] -eq 13) { $delimiterEnd = if ($index + 1 -lt $buffer.Length -and $buffer[$index + 1] -eq 10) { $index + 2 } else { $index + 1 } break } } } else { for ($index = 0; $index + 1 -lt $buffer.Length; $index += 2) { $codePoint = if ($Encoding -eq 'Unicode') { [int]$buffer[$index] -bor ([int]$buffer[$index + 1] -shl 8) } else { ([int]$buffer[$index] -shl 8) -bor [int]$buffer[$index + 1] } if ($codePoint -eq 10) { $delimiterEnd = $index + 2 break } if ($codePoint -eq 13) { $nextCodePoint = -1 if ($index + 3 -lt $buffer.Length) { $nextCodePoint = if ($Encoding -eq 'Unicode') { [int]$buffer[$index + 2] -bor ([int]$buffer[$index + 3] -shl 8) } else { ([int]$buffer[$index + 2] -shl 8) -bor [int]$buffer[$index + 3] } } $delimiterEnd = if ($nextCodePoint -eq 10) { $index + 4 } else { $index + 2 } break } } } if ($delimiterEnd -lt 0 -or $delimiterEnd -ge $buffer.Length) { throw [System.IO.InvalidDataException]::new( 'The selected log tail exceeds the safe read limit. Reduce TailLines and run the check again.' ) } $trimmedBuffer = [byte[]]::new($buffer.Length - $delimiterEnd) [Array]::Copy($buffer, $delimiterEnd, $trimmedBuffer, 0, $trimmedBuffer.Length) $buffer = $trimmedBuffer } try { $decodedText = $textEncoding.GetString($buffer) } catch [Text.DecoderFallbackException] { throw [System.IO.InvalidDataException]::new( "The text file is not valid $Encoding text. Choose the correct Encoding and run the check again.", $_.Exception ) } $selectedLines = @() if ($decodedText.Length -gt 0) { # Ignore one terminal line delimiter, matching Get-Content -Tail semantics. # Locate the requested tail by scanning backward so a file containing # millions of tiny lines never materializes millions of strings. $effectiveEnd = $decodedText.Length if ($decodedText[$effectiveEnd - 1] -eq "`n") { $effectiveEnd-- if ($effectiveEnd -gt 0 -and $decodedText[$effectiveEnd - 1] -eq "`r") { $effectiveEnd-- } } elseif ($decodedText[$effectiveEnd - 1] -eq "`r") { $effectiveEnd-- } $selectedStart = 0 $logicalLineCount = 1 for ($index = $effectiveEnd - 1; $index -ge 0; $index--) { if ($decodedText[$index] -eq "`n") { $delimiterEnd = $index + 1 if ($index -gt 0 -and $decodedText[$index - 1] -eq "`r") { $index-- } $logicalLineCount++ if ($logicalLineCount -gt $TailLines) { $selectedStart = $delimiterEnd break } } elseif ($decodedText[$index] -eq "`r") { $logicalLineCount++ if ($logicalLineCount -gt $TailLines) { $selectedStart = $index + 1 break } } } if ($wasLimited -and $logicalLineCount -lt $TailLines) { throw [System.IO.InvalidDataException]::new( 'The selected log tail exceeds the safe read limit. Reduce TailLines and run the check again.' ) } $selectedText = $decodedText.Substring($selectedStart, $effectiveEnd - $selectedStart) $lineList = [Collections.Generic.List[string]]::new() $lineStart = 0 $decodedCharacterCount = 0 for ($index = 0; $index -lt $selectedText.Length; $index++) { if ($selectedText[$index] -ne "`r" -and $selectedText[$index] -ne "`n") { continue } $lineLength = $index - $lineStart $decodedCharacterCount += $lineLength if ($decodedCharacterCount -gt $MaximumDecodedCharacters) { throw [System.IO.InvalidDataException]::new( 'The selected log tail exceeds the decoded-character limit. Reduce TailLines and run the check again.' ) } $lineList.Add($selectedText.Substring($lineStart, $lineLength)) if ($selectedText[$index] -eq "`r" -and $index + 1 -lt $selectedText.Length -and $selectedText[$index + 1] -eq "`n") { $index++ } $lineStart = $index + 1 } $lastLineLength = $selectedText.Length - $lineStart $decodedCharacterCount += $lastLineLength if ($decodedCharacterCount -gt $MaximumDecodedCharacters) { throw [System.IO.InvalidDataException]::new( 'The selected log tail exceeds the decoded-character limit. Reduce TailLines and run the check again.' ) } $lineList.Add($selectedText.Substring($lineStart, $lastLineLength)) $selectedLines = @($lineList) } [pscustomobject]@{ Lines = @($selectedLines) BytesRead = $totalRead WasLimited = $wasLimited } } finally { $fileStream.Dispose() } } |