Skip to content

ModuleSpecification/FullyQualifiedName usage does not handle paths well #8261

@rjmholt

Description

@rjmholt

Opened out of #8218.

We do very little santisation/normalisation on paths passed in as module names in ModuleSpecification types to various cmdlets.

Not many users rely directly on this functionality, but things like RequiredModules in module manifests do.

We currently don't have any layering or type API (that I know of) to reflect that a path passed in has been converted by PowerShell to one that's safe for .NET to operate on, and the ideal scenario would be for us to change that. Otherwise we are doomed to repeat the same path checks/resolutions at every layer in the code, either hurting perf or causing serious bugs.

Steps to reproduce

Some examples of this problem:

  • Remove module using a path in a qualified name doesn't work:
       Describe "Remove-Module works with FullyQualifiedName using a path for the name" {
        BeforeAll {
            $moduleName = 'rmomod'
            $moduleVersion = '1.2'
            $modulePath = Join-Path $TestDrive $moduleName
            $manifestPath = Join-Path $modulePath "$moduleName.psd1"
    
            New-Item -ItemType Directory -Path $modulePath
    
            New-ModuleManifest -Path $manifestPath -ModuleVersion $moduleVersion
    
            if ($IsWindows)
            {
                $sep = '\'
                $altSep = '/'
            }
            else
            { 
                $sep = '/'
                $altSep = '\'
            }
    
            $absoluteTestCases = @(
                @{ ModuleName = $moduleName; Case = 'module name' }
                @{ ModuleName = $modulePath; Case = 'absolute module dir path' }
                @{ ModuleName = "$TestDrive${altSep}$moduleName"; Case = 'absolute module dir path with alt dir separators' }
                @{ ModuleName = $manifestPath; Case = 'absolute manifest path' }
                @{ ModuleName = "$TestDrive${altSep}$moduleName${sep}$moduleName.psd1"; Case = 'absolute manifest path with alt dir separators' }
            )
    
            $relativeTestCases = @(
                @{ Case = 'relative path to module dir'; Location = $TestDrive; ModuleName = "./$moduleName" }
                @{ Case = 'relative path to module dir with alt sep'; Location = "$TestDrive/duck"; ModuleName = "..${altSep}moduleName" }
                @{ Case = 'relative path to manifest with alt sep'; Location = "$TestDrive/$moduleName"; ModuleName = "./$moduleName.psd1" }
                @{ Case = 'relative path to manifest with alt sep'; Location = $TestDrive; ModuleName = ".${sep}$moduleName{$altSep}$moduleName.psd1" }
                @{ Case = 'current dir being module dir'; Location = "$TestDrive/$moduleName"; ModuleName = "." }
            )
        }
    
        BeforeEach {
            Import-Module $modulePath
        }
    
        AfterEach {
            Remove-Module $moduleName -ErrorAction SilentlyContinue
        }
    
        It "Removes the module by <Case>" -TestCases $absoluteTestCases {
            param([string]$ModuleName, [string]$Case)
    
            $fqn = @{
                ModuleName = $ModuleName
                ModuleVersion = $moduleVersion
            }
    
            Remove-Module -FullyQualifiedName $fqn -ErrorAction Stop
            Get-Module $moduleName | Should -HaveCount 0
        }
    
        It "Removes the module by <Case>" -TestCases $relativeTestCases {
            param([string]$Location, [string]$ModuleName, [string]$Case)
    
            $fqn = @{
                ModuleName = $ModuleName
                ModuleVersion = $moduleVersion
            }
    
            if (-not (Test-Path $Location))
            {
                New-Item -ItemType Directory -Path $Location
            }
    
            Push-Location $Location
            try
            {
                Remove-Module -FullyQualifiedName $fqn -ErrorAction Stop
                Get-Module $moduleName | Should -HaveCount 0
            }
            finally
            {
                Pop-Location
            }
        }
    }
  • Required modules in a script:
    Describe "Requiring modules by absolute path" {
        BeforeAll {
            $scriptPath = Join-Path $TestDrive "script.ps1"
            $success = 'SUCCESS'
    
            $moduleName = 'reqmod'
            $modulePath = Join-Path $TestDrive $moduleName
            $manifestPath = Join-Path $modulePath "$moduleName.psd1"
    
            New-Item -ItemType Directory $modulePath -Force
    
            New-ModuleManifest -Path $manifestPath -ModuleVersion '3.2.4'
    
            if ($IsWindows)
            {
                $sep = '\'
                $altSep = '/'
            }
            else
            {
                $sep = '/'
                $altSep = '\'
            }
    
            $oldModulePath = $env:PSModulePath
            $env:PSModulePath += [System.IO.Path]::PathSeparator + $TestDrive
    
            $testCases = @(
                @{ Case = 'module name, with module on path'; ModuleName = $moduleName }
                @{ Case = 'absolute path to module dir'; ModuleName = $modulePath }
                @{ Case = 'absolute path to module dir with alt sep'; ModuleName = "$TestDrive${altSep}$moduleName" }
                @{ Case = 'absolute path to manifest'; ModuleName = $manifestPath }
                @{ Case = 'absolute path to manifest with alt sep'; ModuleName = "$TestDrive${sep}$moduleName${altSep}$moduleName.psd1" }
            )
        }
    
        AfterAll {
            $env:PSModulePath = $oldModulePath
        }
    
        Context "Autoload unloaded required module" {
            AfterEach {
                Remove-Module $moduleName -ErrorAction SilentlyContinue
            }
    
            It "Autoloads the required module by <Case>" -TestCases $testCases {
                param([string]$ModuleName, [string]$Case)
    
                $script = @"
    #requires -Modules @{ ModuleName = '$ModuleName'; ModuleVersion = '$moduleVersion' }
    '$success'
    "@
    
                New-Item -Path $scriptPath -Value $script -Force
                & $scriptPath | Should -BeExactly $success
            }
        }
    
        Context "Verify required module is already loaded" {
            BeforeAll {
                Import-Module $modulePath
            }
    
            AfterAll {
                Remove-Module $moduleName -ErrorAction SilentlyContinue
            }
    
            It "Verifies that the required module is loaded by <Case>" -TestCases $testCases {
                param([string]$ModuleName, [string]$Case)
    
                $script = @"
    #requires -Modules @{ ModuleName = '$ModuleName'; ModuleVersion = '$moduleVersion' }
    '$success'
    "@
    
                New-Item -Path $scriptPath -Value $script -Force
                & $scriptPath | Should -BeExactly $success
            }
        }
    }
    (This second case actually supports alt separators, which I didn't expect. But still no module dir. And no relative paths either)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Issue-Enhancementthe issue is more of a feature request than a bugResolution-No ActivityIssue has had no activity for 6 months or moreWG-Cmdlets-Corecmdlets in the Microsoft.PowerShell.Core module

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions