Skip to content

Commit c01eeaa

Browse files
Harden YAML import path identity checks
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 29efafc commit c01eeaa

2 files changed

Lines changed: 170 additions & 21 deletions

File tree

src/functions/public/Import-Yaml.ps1

Lines changed: 149 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ function Import-Yaml {
7575
7676
.OUTPUTS
7777
System.Object
78+
79+
.NOTES
80+
Only FileSystem provider paths are supported.
81+
82+
.LINK
83+
ConvertFrom-Yaml
7884
#>
7985
[OutputType([object])]
8086
[CmdletBinding(DefaultParameterSetName = 'Path')]
@@ -226,19 +232,9 @@ function Import-Yaml {
226232
)
227233
$PSCmdlet.ThrowTerminatingError($record)
228234
}
229-
if (-not [System.IO.File]::Exists($providerPath)) {
230-
if ([System.IO.Directory]::Exists($providerPath)) {
231-
$exception = [System.IO.IOException]::new(
232-
"Cannot import YAML from '$requestedPath': the path is not a file."
233-
)
234-
$record = [System.Management.Automation.ErrorRecord]::new(
235-
$exception,
236-
'YamlImportNotFile',
237-
[System.Management.Automation.ErrorCategory]::InvalidType,
238-
$requestedPath
239-
)
240-
$PSCmdlet.ThrowTerminatingError($record)
241-
}
235+
try {
236+
$pathAttributes = [System.IO.File]::GetAttributes($providerPath)
237+
} catch [System.IO.FileNotFoundException] {
242238
$exception = [System.IO.FileNotFoundException]::new(
243239
"Cannot import YAML from '$requestedPath': the path was not found."
244240
)
@@ -249,6 +245,55 @@ function Import-Yaml {
249245
$requestedPath
250246
)
251247
$PSCmdlet.ThrowTerminatingError($record)
248+
} catch [System.IO.DirectoryNotFoundException] {
249+
$exception = [System.IO.FileNotFoundException]::new(
250+
"Cannot import YAML from '$requestedPath': the path was not found."
251+
)
252+
$record = [System.Management.Automation.ErrorRecord]::new(
253+
$exception,
254+
'YamlImportPathNotFound',
255+
[System.Management.Automation.ErrorCategory]::ObjectNotFound,
256+
$requestedPath
257+
)
258+
$PSCmdlet.ThrowTerminatingError($record)
259+
} catch [System.UnauthorizedAccessException] {
260+
$inspectionError = $_
261+
$exception = [System.IO.IOException]::new(
262+
"Cannot inspect YAML input path '$requestedPath': $($inspectionError.Exception.Message)",
263+
$inspectionError.Exception
264+
)
265+
$record = [System.Management.Automation.ErrorRecord]::new(
266+
$exception,
267+
'YamlImportPermissionDenied',
268+
[System.Management.Automation.ErrorCategory]::PermissionDenied,
269+
$requestedPath
270+
)
271+
$PSCmdlet.ThrowTerminatingError($record)
272+
} catch [System.IO.IOException] {
273+
$inspectionError = $_
274+
$exception = [System.IO.IOException]::new(
275+
"Cannot inspect YAML input path '$requestedPath': $($inspectionError.Exception.Message)",
276+
$inspectionError.Exception
277+
)
278+
$record = [System.Management.Automation.ErrorRecord]::new(
279+
$exception,
280+
'YamlImportPathInspectionFailed',
281+
[System.Management.Automation.ErrorCategory]::ReadError,
282+
$requestedPath
283+
)
284+
$PSCmdlet.ThrowTerminatingError($record)
285+
}
286+
if (($pathAttributes -band [System.IO.FileAttributes]::Directory) -ne 0) {
287+
$exception = [System.IO.IOException]::new(
288+
"Cannot import YAML from '$requestedPath': the path is not a file."
289+
)
290+
$record = [System.Management.Automation.ErrorRecord]::new(
291+
$exception,
292+
'YamlImportNotFile',
293+
[System.Management.Automation.ErrorCategory]::InvalidType,
294+
$requestedPath
295+
)
296+
$PSCmdlet.ThrowTerminatingError($record)
252297
}
253298
$resolvedPaths.Add([System.IO.Path]::GetFullPath($providerPath))
254299
continue
@@ -337,7 +382,36 @@ function Import-Yaml {
337382
)
338383
$PSCmdlet.ThrowTerminatingError($record)
339384
}
340-
if (-not [System.IO.File]::Exists($pathInfo.ProviderPath)) {
385+
try {
386+
$pathAttributes = [System.IO.File]::GetAttributes($pathInfo.ProviderPath)
387+
} catch [System.UnauthorizedAccessException] {
388+
$inspectionError = $_
389+
$exception = [System.IO.IOException]::new(
390+
"Cannot inspect YAML input path '$($pathInfo.Path)': $($inspectionError.Exception.Message)",
391+
$inspectionError.Exception
392+
)
393+
$record = [System.Management.Automation.ErrorRecord]::new(
394+
$exception,
395+
'YamlImportPermissionDenied',
396+
[System.Management.Automation.ErrorCategory]::PermissionDenied,
397+
$pathInfo.Path
398+
)
399+
$PSCmdlet.ThrowTerminatingError($record)
400+
} catch [System.IO.IOException] {
401+
$inspectionError = $_
402+
$exception = [System.IO.IOException]::new(
403+
"Cannot inspect YAML input path '$($pathInfo.Path)': $($inspectionError.Exception.Message)",
404+
$inspectionError.Exception
405+
)
406+
$record = [System.Management.Automation.ErrorRecord]::new(
407+
$exception,
408+
'YamlImportPathInspectionFailed',
409+
[System.Management.Automation.ErrorCategory]::ReadError,
410+
$pathInfo.Path
411+
)
412+
$PSCmdlet.ThrowTerminatingError($record)
413+
}
414+
if (($pathAttributes -band [System.IO.FileAttributes]::Directory) -ne 0) {
341415
$exception = [System.IO.IOException]::new(
342416
"Cannot import YAML from '$($pathInfo.Path)': the path is not a file."
343417
)
@@ -353,17 +427,71 @@ function Import-Yaml {
353427
}
354428
}
355429

356-
$pathComparer = if ($IsWindows) {
357-
[System.StringComparer]::OrdinalIgnoreCase
358-
} else {
430+
$caseSensitivePaths = [System.Collections.Generic.HashSet[string]]::new(
359431
[System.StringComparer]::Ordinal
360-
}
361-
$uniquePaths = [System.Collections.Generic.HashSet[string]]::new($pathComparer)
432+
)
433+
$caseInsensitivePaths = [System.Collections.Generic.HashSet[string]]::new(
434+
[System.StringComparer]::OrdinalIgnoreCase
435+
)
362436
foreach ($resolvedPath in $resolvedPaths) {
363-
$null = $uniquePaths.Add($resolvedPath)
437+
$useCaseSensitiveIdentity = -not $IsWindows
438+
if ($IsWindows) {
439+
$identityPath = $resolvedPath
440+
while (-not [string]::IsNullOrEmpty($identityPath)) {
441+
$directoryPath = [System.IO.Path]::GetDirectoryName($identityPath)
442+
if ([string]::IsNullOrEmpty($directoryPath)) {
443+
break
444+
}
445+
$leafName = [System.IO.Path]::GetFileName($identityPath)
446+
try {
447+
$caseVariantCount = 0
448+
foreach ($directoryEntry in (
449+
[System.IO.Directory]::EnumerateFileSystemEntries($directoryPath)
450+
)) {
451+
if ([string]::Equals(
452+
[System.IO.Path]::GetFileName($directoryEntry),
453+
$leafName,
454+
[System.StringComparison]::OrdinalIgnoreCase
455+
)) {
456+
$caseVariantCount++
457+
if ($caseVariantCount -gt 1) {
458+
$useCaseSensitiveIdentity = $true
459+
break
460+
}
461+
}
462+
}
463+
} catch [System.UnauthorizedAccessException] {
464+
$useCaseSensitiveIdentity = $true
465+
} catch [System.IO.IOException] {
466+
$useCaseSensitiveIdentity = $true
467+
}
468+
if ($useCaseSensitiveIdentity -or
469+
[string]::Equals(
470+
$identityPath,
471+
$directoryPath,
472+
[System.StringComparison]::Ordinal
473+
)) {
474+
break
475+
}
476+
$identityPath = $directoryPath
477+
}
478+
}
479+
480+
if ($useCaseSensitiveIdentity) {
481+
$null = $caseSensitivePaths.Add($resolvedPath)
482+
} else {
483+
$null = $caseInsensitivePaths.Add($resolvedPath)
484+
}
485+
}
486+
$uniquePaths = [System.Collections.Generic.List[string]]::new()
487+
foreach ($uniquePath in $caseSensitivePaths) {
488+
$uniquePaths.Add($uniquePath)
489+
}
490+
foreach ($uniquePath in $caseInsensitivePaths) {
491+
$uniquePaths.Add($uniquePath)
364492
}
365493
[string[]] $orderedPaths = $uniquePaths
366-
[System.Array]::Sort($orderedPaths, $pathComparer)
494+
[System.Array]::Sort($orderedPaths, [System.StringComparer]::Ordinal)
367495

368496
foreach ($resolvedPath in $orderedPaths) {
369497
try {

tests/Import-Yaml.Tests.ps1

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,27 @@ Describe 'Import-Yaml' {
8585
@($result.value) | Should -Be @('first', 'second')
8686
}
8787

88+
It 'keeps case-distinct files separate on case-sensitive filesystems' -Skip:$IsWindows {
89+
$upperPath = Join-Path $TestDrive 'Case.yaml'
90+
$lowerPath = Join-Path $TestDrive 'case.yaml'
91+
[System.IO.File]::WriteAllText($upperPath, 'value: upper')
92+
[System.IO.File]::WriteAllText($lowerPath, 'value: lower')
93+
94+
$result = @(Import-Yaml -Path @($upperPath, $lowerPath))
95+
96+
@($result.value) | Should -Be @('upper', 'lower')
97+
}
98+
99+
It 'suppresses path case variants on case-insensitive filesystems' -Skip:(-not $IsWindows) {
100+
$path = Join-Path $TestDrive 'CaseVariant.yaml'
101+
[System.IO.File]::WriteAllText($path, 'value: once')
102+
103+
$result = @(Import-Yaml -Path @($path, $path.ToLowerInvariant()))
104+
105+
$result.Count | Should -Be 1
106+
$result[0].value | Should -Be 'once'
107+
}
108+
88109
It 'preserves deterministic file and document order together' {
89110
$firstPath = Join-Path $TestDrive '01.yaml'
90111
$secondPath = Join-Path $TestDrive '02.yaml'

0 commit comments

Comments
 (0)