Private/Common.ps1
|
function New-DtDirectory { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Path, [switch]$Force ) if (Test-Path -LiteralPath $Path) { Write-Verbose "Directory already exists: $Path" return $false } New-Item -ItemType Directory -Path $Path -Force:$Force | Out-Null Write-Verbose "Created directory: $Path" return $true } function Resolve-DtContainedPath { <# .SYNOPSIS Resolves a child name against a base directory and rejects the result if it escapes that directory. .DESCRIPTION Mirrors the containment check used elsewhere in this portfolio (cockpit's MarlinAdapter._resolve_contained_path) - a user-supplied name must not be able to write outside the intended base directory via ".." segments, UNC paths, or similar. .PARAMETER BasePath The directory the result must stay inside. .PARAMETER ChildName The caller-supplied name to join onto BasePath. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$BasePath, [Parameter(Mandatory = $true)] [string]$ChildName ) if ([System.IO.Path]::IsPathRooted($ChildName) -or $ChildName -match '^[A-Za-z]:' -or $ChildName -match '^[\\/]') { throw "'$ChildName' is not a valid name: rooted paths are not allowed." } $childSegments = $ChildName -split '[\\/]' if ($childSegments -contains '..') { throw "'$ChildName' is not a valid name: parent traversal is not allowed." } $resolvedBase = [System.IO.Path]::GetFullPath($BasePath) $candidate = Join-Path -Path $resolvedBase -ChildPath $ChildName $resolvedCandidate = [System.IO.Path]::GetFullPath($candidate) $pathComparison = if ([System.IO.Path]::DirectorySeparatorChar -eq '\') { [System.StringComparison]::OrdinalIgnoreCase } else { [System.StringComparison]::Ordinal } $trimmedBase = $resolvedBase.TrimEnd('\', '/') $baseWithSep = $trimmedBase + [System.IO.Path]::DirectorySeparatorChar $isBasePath = $resolvedCandidate.Equals($trimmedBase, $pathComparison) $isContainedPath = $resolvedCandidate.StartsWith($baseWithSep, $pathComparison) if (-not $isBasePath -and -not $isContainedPath) { throw "'$ChildName' is not a valid name: it resolves outside the base directory '$resolvedBase'." } return $resolvedCandidate } function ConvertTo-DtSafeMarkdownText { <# .SYNOPSIS Strips characters from user-supplied text that could break generated Markdown structure. .DESCRIPTION Low-severity defense-in-depth for text interpolated into generated Markdown (README titles/descriptions, project names). Not full sanitization - this is a non-executable surface - just strips newlines and leading heading markers so a supplied value can't inject new Markdown sections or headings into generated files. .PARAMETER Text The value to sanitize. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [AllowEmptyString()] [string]$Text ) $singleLine = ($Text -replace '[\r\n]+', ' ').Trim() return ($singleLine -replace '^#+\s*', '') } function Set-DtFileContent { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [string]$Content, [switch]$Force ) if ((Test-Path -LiteralPath $Path) -and -not $Force) { Write-Warning "Path already exists. Use -Force to overwrite: $Path" return $false } $parent = Split-Path -Path $Path -Parent if ($parent -and -not (Test-Path -LiteralPath $parent)) { New-DtDirectory -Path $parent -Force:$Force | Out-Null } $utf8NoBom = New-Object System.Text.UTF8Encoding($false) $contentWithNewline = $Content + [System.Environment]::NewLine [System.IO.File]::WriteAllText($Path, $contentWithNewline, $utf8NoBom) Write-Verbose "Wrote file: $Path" return $true } |