Private/TextFile.ps1
|
# Byte-preserving text edits for files SecondLine changes but doesn't own: profiles, # .editorconfig, VS Code settings and line-ending repairs. # # ASCII-compatible files (UTF-8, ANSI code pages) are handled as Latin-1, which maps # each byte to one char and back. An edit that only touches ASCII therefore leaves # every other byte, including multi-byte UTF-8 and the BOM, exactly as it was. # UTF-16 files (with a BOM) are decoded and re-encoded with the same encoding. $script:SLLatin1 = [Text.Encoding]::GetEncoding(28591) # Reads a file as Text (string) plus what's needed to write it back unchanged: # Kind (Bytes | Utf16LE | Utf16BE), Preamble (BOM bytes) and Newline (dominant ending). function Read-SLTextFile { param([Parameter(Mandatory)] [string] $Path) $bytes = [IO.File]::ReadAllBytes($Path) $kind = 'Bytes' $preamble = [byte[]] @() $body = $bytes if ($bytes.Length -ge 4 -and (($bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE -and $bytes[2] -eq 0 -and $bytes[3] -eq 0) -or ($bytes[0] -eq 0 -and $bytes[1] -eq 0 -and $bytes[2] -eq 0xFE -and $bytes[3] -eq 0xFF))) { throw "$Path is UTF-32, which SecondLine does not edit." } if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $preamble = [byte[]] $bytes[0..2] } elseif ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) { $kind = 'Utf16LE'; $preamble = [byte[]] $bytes[0..1] } elseif ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) { $kind = 'Utf16BE'; $preamble = [byte[]] $bytes[0..1] } if ($preamble.Length -gt 0) { $body = New-Object byte[] ($bytes.Length - $preamble.Length) [Array]::Copy($bytes, $preamble.Length, $body, 0, $body.Length) } $text = (Get-SLTextEncoding $kind).GetString($body) $crlf = ([regex]::Matches($text, "`r`n")).Count $lf = ([regex]::Matches($text, "(?<!`r)`n")).Count $newline = "`r`n" if ($lf -gt $crlf) { $newline = "`n" } return [pscustomobject]@{ Path = $Path; Text = $text; Kind = $kind; Preamble = $preamble; Newline = $newline; Crlf = $crlf; Lf = $lf } } function Get-SLTextEncoding { param([string] $Kind) switch ($Kind) { 'Utf16LE' { return (New-Object Text.UnicodeEncoding($false, $false)) } 'Utf16BE' { return (New-Object Text.UnicodeEncoding($true, $false)) } default { return $script:SLLatin1 } } } # Writes Text back with the original encoding and BOM, through a temporary file. function Write-SLTextFile { param([Parameter(Mandatory)] $File, [Parameter(Mandatory)] [AllowEmptyString()] [string] $Text, [string] $Path) if (-not $Path) { $Path = $File.Path } $body = (Get-SLTextEncoding $File.Kind).GetBytes($Text) $bytes = New-Object byte[] ($File.Preamble.Length + $body.Length) [Array]::Copy([byte[]] $File.Preamble, 0, $bytes, 0, $File.Preamble.Length) [Array]::Copy($body, 0, $bytes, $File.Preamble.Length, $body.Length) $temp = "$Path.$([guid]::NewGuid().ToString('N')).tmp" [IO.File]::WriteAllBytes($temp, $bytes) try { if (Test-Path -LiteralPath $Path) { [IO.File]::Replace($temp, $Path, [NullString]::Value) } else { [IO.File]::Move($temp, $Path) } } catch { Remove-Item -LiteralPath $temp -Force -ErrorAction SilentlyContinue; throw } } # A new, empty file description: UTF-8 without BOM, with the platform's line ending. function New-SLTextFile { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Describes a file to create; writes nothing.')] param([Parameter(Mandatory)] [string] $Path) $newline = "`r`n" if ([IO.Path]::DirectorySeparatorChar -eq '/') { $newline = "`n" } return [pscustomobject]@{ Path = $Path; Text = ''; Kind = 'Bytes'; Preamble = [byte[]] @(); Newline = $newline; Crlf = 0; Lf = 0 } } |