Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion ModuleFast.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -1023,7 +1023,25 @@ function Install-ModuleFastHelper {

#We are going to extract these straight out of memory, so we don't need to write the nupkg to disk
$zip = [IO.Compression.ZipArchive]::new($stream, 'Read')
[IO.Compression.ZipFileExtensions]::ExtractToDirectory($zip, $installPath)
# NuGet packages may URL-encode file names (e.g. spaces as %20), so we must decode each
# entry's FullName before using it as a file path. See:
# https://github.com/NuGet/NuGet.Client/blob/d2887cd591059fd675d397b48c79cdc30ee2b6ba/src/NuGet.Core/NuGet.Packaging/PackageArchiveReader.cs#L317
foreach ($entry in $zip.Entries) {
$decodedEntryName = [Uri]::UnescapeDataString($entry.FullName)
$destPath = Join-Path $installPath $decodedEntryName
# ZIP spec uses '/' but some tools emit '\'; treat both as directory entries
if ($decodedEntryName.EndsWith('/') -or $decodedEntryName.EndsWith('\')) {
# Directory entry — ensure the directory exists
New-Item -ItemType Directory -Path $destPath -Force | Out-Null
} else {
# File entry — ensure parent directory exists, then extract
$destDir = [Path]::GetDirectoryName($destPath)
if (-not (Test-Path $destDir)) {
New-Item -ItemType Directory -Path $destDir -Force | Out-Null
}
[IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $destPath, $true)
}
}

$manifestPath = Join-Path $installPath "$($context.Module.Name).psd1"

Expand Down
45 changes: 45 additions & 0 deletions ModuleFast.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,51 @@ InModuleScope 'ModuleFast' {
$manifest.RootModule | Should -Be 'coreclr\PrtgAPI.PowerShell.dll'
}
}

Describe 'URL-decoding zip entry names during extraction' {
It 'Decodes URL-encoded file names (e.g. spaces as %20) when extracting a zip archive' {
# Build an in-memory ZIP that has a percent-encoded entry name and an explicit directory entry
$memStream = [System.IO.MemoryStream]::new()
$zipWrite = [System.IO.Compression.ZipArchive]::new($memStream, [System.IO.Compression.ZipArchiveMode]::Create, $true)
# Explicit directory entry with percent-encoded name
$zipWrite.CreateEntry('sub%20folder/') | Out-Null
# File entry inside the percent-encoded directory
$entry = $zipWrite.CreateEntry('sub%20folder/file%20with%20spaces.txt')
$writer = [System.IO.StreamWriter]::new($entry.Open())
$writer.Write('hello')
$writer.Dispose()
$zipWrite.Dispose()
$memStream.Position = 0

# Replicate the extraction logic from Install-ModuleFast (URL-decode each entry name)
$extractPath = Join-Path $TestDrive ([System.Guid]::NewGuid())
New-Item -ItemType Directory -Path $extractPath -Force | Out-Null
$zipRead = [System.IO.Compression.ZipArchive]::new($memStream, [System.IO.Compression.ZipArchiveMode]::Read)
foreach ($zipEntry in $zipRead.Entries) {
$decodedEntryName = [Uri]::UnescapeDataString($zipEntry.FullName)
$destPath = Join-Path $extractPath $decodedEntryName
if ($decodedEntryName.EndsWith('/') -or $decodedEntryName.EndsWith('\')) {
New-Item -ItemType Directory -Path $destPath -Force | Out-Null
} else {
$destDir = [System.IO.Path]::GetDirectoryName($destPath)
if (-not (Test-Path $destDir)) {
New-Item -ItemType Directory -Path $destDir -Force | Out-Null
}
[System.IO.Compression.ZipFileExtensions]::ExtractToFile($zipEntry, $destPath, $true)
}
}
$zipRead.Dispose()
$memStream.Dispose()

# The decoded directory should exist
Test-Path (Join-Path $extractPath 'sub folder') | Should -BeTrue
Test-Path (Join-Path $extractPath 'sub%20folder') | Should -BeFalse

# The extracted file should use the decoded name, not the percent-encoded one
Test-Path (Join-Path $extractPath 'sub folder' 'file with spaces.txt') | Should -BeTrue
Test-Path (Join-Path $extractPath 'sub folder' 'file%20with%20spaces.txt') | Should -BeFalse
}
}
}

Describe 'Get-ModuleFastPlan' -Tag 'E2E' {
Expand Down