# lib/files.ps1 - JSON read/write for PowerShell 5.1, and restricting a file to its owner. # # Part of claude-mode.ps1, which dot-sources it into its own script scope after # the settings at its top. Not meant to run on its own. ASCII only: Windows # PowerShell 5.1 reads a .ps1 without a BOM as ANSI. $PSScriptRoot here would be # lib\, so paths beside the main script go through $script:Here. # --------------------------------------------------------------------------- # JSON helpers (PS 5.1 has no ConvertFrom-Json -AsHashtable) # --------------------------------------------------------------------------- function ConvertTo-DeepHashtable { param($InputObject) if ($null -eq $InputObject) { return $null } if ($InputObject -is [System.Collections.IDictionary]) { $h = [ordered]@{} foreach ($k in $InputObject.Keys) { $h[[string]$k] = ConvertTo-DeepHashtable $InputObject[$k] } return $h } if ($InputObject -is [System.Management.Automation.PSCustomObject]) { $h = [ordered]@{} foreach ($p in $InputObject.PSObject.Properties) { $h[$p.Name] = ConvertTo-DeepHashtable $p.Value } return $h } if ($InputObject -is [string]) { return $InputObject } if ($InputObject -is [System.Collections.IEnumerable]) { $list = New-Object System.Collections.ArrayList foreach ($item in $InputObject) { [void]$list.Add((ConvertTo-DeepHashtable $item)) } return , $list.ToArray() } return $InputObject } function Read-JsonFile { param([string] $Path) if (-not (Test-Path -LiteralPath $Path)) { return $null } $raw = Get-Content -LiteralPath $Path -Raw -Encoding UTF8 if ([string]::IsNullOrWhiteSpace($raw)) { return $null } return ConvertTo-DeepHashtable (ConvertFrom-Json $raw) } function Write-JsonFile { param([string] $Path, $Data) $dir = Split-Path -Parent $Path if (-not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } $json = Format-JsonPretty ($Data | ConvertTo-Json -Depth 100) # UTF-8 without BOM; some JSON readers choke on a BOM. [System.IO.File]::WriteAllText($Path, $json, (New-Object System.Text.UTF8Encoding($false))) } # PowerShell 5.1's ConvertTo-Json indents by aligning values into a column, # which is valid but painful to hand-edit. Re-indent with node when available. function Format-JsonPretty { param([string] $Json) if ($null -eq $script:NodeExe) { $c = Get-Command node -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 if ($c) { $script:NodeExe = $c.Source } else { $script:NodeExe = '' } } if (-not $script:NodeExe) { return $Json } $tmp = [System.IO.Path]::GetTempFileName() try { [System.IO.File]::WriteAllText($tmp, $Json, (New-Object System.Text.UTF8Encoding($false))) $out = & $script:NodeExe -e "const fs=require('fs');process.stdout.write(JSON.stringify(JSON.parse(fs.readFileSync(process.argv[1],'utf8')),null,2)+'\n')" $tmp if ($out) { return (($out -join "`n") + "`n") } return $Json } catch { return $Json } finally { Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue } } # --------------------------------------------------------------------------- # ACL hardening - restrict a file to the current user only # --------------------------------------------------------------------------- function Protect-FileAcl { param([string] $Path) try { $acl = Get-Acl -LiteralPath $Path if ($acl.AreAccessRulesProtected) { return } $acl.SetAccessRuleProtection($true, $false) foreach ($rule in @($acl.Access)) { [void]$acl.RemoveAccessRule($rule) } $me = New-Object System.Security.Principal.NTAccount($env:USERDOMAIN, $env:USERNAME) $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule( $me, 'FullControl', 'None', 'None', 'Allow'))) Set-Acl -LiteralPath $Path -AclObject $acl } catch { Write-Warn2 "could not harden ACL on $Path : $($_.Exception.Message)" } }