From 99d696f31fe9929681821a20db30448c67a5f6cb Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Wed, 1 Mar 2017 10:36:02 -0800 Subject: [PATCH] Fix Test-Modulemanifest to normalize paths correctly before validating (#3097) Changed hard coded Windows directory separator and resolved path so the slashes are correct. Throw if resolving file path returns more than one result Fixes #2610 --- .../Modules/TestModuleManifestCommand.cs | 52 +++++-- .../resources/Modules.resx | 3 +- .../engine/Module/TestModuleManifest.ps1 | 127 ++++++++++++++++++ 3 files changed, 166 insertions(+), 16 deletions(-) create mode 100644 test/powershell/engine/Module/TestModuleManifest.ps1 diff --git a/src/System.Management.Automation/engine/Modules/TestModuleManifestCommand.cs b/src/System.Management.Automation/engine/Modules/TestModuleManifestCommand.cs index 7f387a7e5..8115c840a 100644 --- a/src/System.Management.Automation/engine/Modules/TestModuleManifestCommand.cs +++ b/src/System.Management.Automation/engine/Modules/TestModuleManifestCommand.cs @@ -131,6 +131,24 @@ namespace Microsoft.PowerShell.Commands } } + //RootModule can be null, empty string or point to a valid .psm1, , .cdxml, .xaml or .dll. Anything else is invalid. + if (module.RootModule != null && module.RootModule != "") + { + string rootModuleExt = System.IO.Path.GetExtension(module.RootModule); + if ((!IsValidFilePath(module.RootModule, module, true) && !IsValidGacAssembly(module.RootModule)) || + (!rootModuleExt.Equals(StringLiterals.PowerShellModuleFileExtension, StringComparison.OrdinalIgnoreCase) && + !rootModuleExt.Equals(".dll", StringComparison.OrdinalIgnoreCase) && + !rootModuleExt.Equals(".cdxml", StringComparison.OrdinalIgnoreCase) && + !rootModuleExt.Equals(".xaml", StringComparison.OrdinalIgnoreCase)) + ) + { + string errorMsg = StringUtil.Format(Modules.InvalidModuleManifest, module.RootModule, filePath); + var errorRecord = new ErrorRecord(new ArgumentException(errorMsg), "Modules_InvalidRootModuleInModuleManifest", + ErrorCategory.InvalidArgument, _path); + WriteError(errorRecord); + } + } + Hashtable data = null; Hashtable localizedData = null; bool containerErrors = false; @@ -147,18 +165,8 @@ namespace Microsoft.PowerShell.Commands && !IsValidFilePath(nestedModule.Name + StringLiterals.PowerShellModuleFileExtension, module, true) && !IsValidGacAssembly(nestedModule.Name)) { - // The nested module could be dependencies. We compare if it can be loaded by loadmanifest - bool isDependency = false; - foreach (PSModuleInfo loadedNestedModule in module.NestedModules) - { - if (string.Equals(loadedNestedModule.Name, nestedModule.Name, StringComparison.OrdinalIgnoreCase)) - { - isDependency = true; - break; - } - } - - if (!isDependency) + Collection modules = GetModuleIfAvailable(nestedModule); + if (0 == modules.Count) { string errorMsg = StringUtil.Format(Modules.InvalidNestedModuleinModuleManifest, nestedModule.Name, filePath); var errorRecord = new ErrorRecord(new DirectoryNotFoundException(errorMsg), "Modules_InvalidNestedModuleinModuleManifest", @@ -291,9 +299,21 @@ namespace Microsoft.PowerShell.Commands if (!System.IO.Path.IsPathRooted(path)) { // we assume the relative path is under module scope, otherwise we will throw error anyway. - path = System.IO.Path.GetFullPath(module.ModuleBase + "\\" + path); + path = System.IO.Path.GetFullPath(module.ModuleBase + System.IO.Path.DirectorySeparatorChar + path); } + // resolve the path so slashes are in the right direction + CmdletProviderContext cmdContext = new CmdletProviderContext(this); + Collection pathInfos = SessionState.Path.GetResolvedPSPathFromPSPath(path, cmdContext); + if (pathInfos.Count != 1) + { + string message = StringUtil.Format(Modules.InvalidModuleManifestPath, path); + InvalidOperationException ioe = new InvalidOperationException(message); + ErrorRecord er = new ErrorRecord(ioe, "Modules_InvalidModuleManifestPath", ErrorCategory.InvalidArgument, path); + ThrowTerminatingError(er); + } + path = pathInfos[0].Path; + // First, we validate if the path does exist. if (!File.Exists(path) && !Directory.Exists(path)) { @@ -308,7 +328,7 @@ namespace Microsoft.PowerShell.Commands } catch (Exception exception) { - if (exception is ArgumentException || exception is ArgumentNullException || exception is NotSupportedException || exception is PathTooLongException) + if (exception is ArgumentException || exception is ArgumentNullException || exception is NotSupportedException || exception is PathTooLongException || exception is ItemNotFoundException) { return false; } @@ -324,6 +344,9 @@ namespace Microsoft.PowerShell.Commands /// private bool IsValidGacAssembly(string assemblyName) { +#if UNIX + return false; +#else string gacPath = System.Environment.GetEnvironmentVariable("windir") + "\\Microsoft.NET\\assembly"; string assemblyFile = assemblyName; string ngenAssemblyFile = assemblyName; @@ -351,6 +374,7 @@ namespace Microsoft.PowerShell.Commands } return true; +#endif } } diff --git a/src/System.Management.Automation/resources/Modules.resx b/src/System.Management.Automation/resources/Modules.resx index 93f52a436..8ec8202e3 100644 --- a/src/System.Management.Automation/resources/Modules.resx +++ b/src/System.Management.Automation/resources/Modules.resx @@ -154,8 +154,7 @@ No custom object was returned for module '{0}' because the -AsCustomObject parameter can only be used with script modules. - The module manifest '{0}' could not be processed because it is not a valid Windows PowerShell restricted language file. Remove the elements that are not permitted by the restricted language: -{1} + The module manifest '{0}' could not be processed because it is not a valid PowerShell module manifest file. Remove the elements that are not permitted: {1} Processing the module manifest file '{0}' did not result in a valid manifest object. Update the file to contain a valid Windows PowerShell module manifest. A valid manifest can be created using the New-ModuleManifest cmdlet. diff --git a/test/powershell/engine/Module/TestModuleManifest.ps1 b/test/powershell/engine/Module/TestModuleManifest.ps1 new file mode 100644 index 000000000..bcc671454 --- /dev/null +++ b/test/powershell/engine/Module/TestModuleManifest.ps1 @@ -0,0 +1,127 @@ +Import-Module $PSScriptRoot\..\..\Common\Test.Helpers.psm1 + +Describe "Test-ModuleManifest tests" -tags "CI" { + + AfterEach { + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue testdrive:/module + } + + It "module manifest containing paths with backslashes or forwardslashes are resolved correctly" { + + New-Item -ItemType Directory -Path testdrive:/module + New-Item -ItemType Directory -Path testdrive:/module/foo + New-Item -ItemType Directory -Path testdrive:/module/bar + New-Item -ItemType File -Path testdrive:/module/foo/bar.psm1 + New-Item -ItemType File -Path testdrive:/module/bar/foo.psm1 + $testModulePath = "testdrive:/module/test.psd1" + $fileList = "foo\bar.psm1","bar/foo.psm1" + + New-ModuleManifest -NestedModules $fileList -RootModule foo\bar.psm1 -RequiredAssemblies $fileList -Path $testModulePath -TypesToProcess $fileList -FormatsToProcess $fileList -ScriptsToProcess $fileList -FileList $fileList -ModuleList $fileList + + Test-Path $testModulePath | Should Be $true + + # use -ErrorAction Stop to cause test to fail if Test-ModuleManifest writes to error stream + Test-ModuleManifest -Path $testModulePath -ErrorAction Stop | Should BeOfType System.Management.Automation.PSModuleInfo + } + + It "module manifest containing missing files returns error" -TestCases ( + @{parameter = "RequiredAssemblies"; error = "Modules_InvalidRequiredAssembliesInModuleManifest"}, + @{parameter = "NestedModules"; error = "Modules_InvalidNestedModuleinModuleManifest"}, + @{parameter = "RequiredModules"; error = "Modules_InvalidRequiredModulesinModuleManifest"}, + @{parameter = "FileList"; error = "Modules_InvalidFilePathinModuleManifest"}, + @{parameter = "ModuleList"; error = "Modules_InvalidModuleListinModuleManifest"}, + @{parameter = "TypesToProcess"; error = "Modules_InvalidManifest"}, + @{parameter = "FormatsToProcess"; error = "Modules_InvalidManifest"}, + @{parameter = "RootModule"; error = "Modules_InvalidRootModuleInModuleManifest"}, + @{parameter = "ScriptsToProcess"; error = "Modules_InvalidManifest"} + ) { + + param ($parameter, $error) + + New-Item -ItemType Directory -Path testdrive:/module + New-Item -ItemType Directory -Path testdrive:/module/foo + New-Item -ItemType File -Path testdrive:/module/foo/bar.psm1 + $testModulePath = "testdrive:/module/test.psd1" + + $args = @{$parameter = "doesnotexist.psm1"} + New-ModuleManifest -Path $testModulePath @args + [string]$errorId = "$error,Microsoft.PowerShell.Commands.TestModuleManifestCommand" + + { Test-ModuleManifest -Path $testModulePath -ErrorAction Stop } | ShouldBeErrorId $errorId + } + + It "module manifest containing valid unprocessed rootmodule file type succeeds" -TestCases ( + @{rootModuleValue = "foo.psm1"}, + @{rootModuleValue = "foo.dll"} + ) { + + param($rootModuleValue) + + New-Item -ItemType Directory -Path testdrive:/module + $testModulePath = "testdrive:/module/test.psd1" + + New-Item -ItemType File -Path testdrive:/module/$rootModuleValue + New-ModuleManifest -Path $testModulePath -RootModule $rootModuleValue + $moduleManifest = Test-ModuleManifest -Path $testModulePath -ErrorAction Stop + $moduleManifest | Should BeOfType System.Management.Automation.PSModuleInfo + $moduleManifest.RootModule | Should Be $rootModuleValue + } + + It "module manifest containing valid processed empty rootmodule file type fails" -TestCases ( + @{rootModuleValue = "foo.cdxml"; error = "System.Xml.XmlException"}, # fails when cmdlet tries to read it as XML + @{rootModuleValue = "foo.xaml"; error = "NotSupported"} # not supported on PowerShell Core + ) { + + param($rootModuleValue, $error) + + New-Item -ItemType Directory -Path testdrive:/module + $testModulePath = "testdrive:/module/test.psd1" + + New-Item -ItemType File -Path testdrive:/module/$rootModuleValue + New-ModuleManifest -Path $testModulePath -RootModule $rootModuleValue + { Test-ModuleManifest -Path $testModulePath -ErrorAction Stop } | ShouldBeErrorId "$error,Microsoft.PowerShell.Commands.TestModuleManifestCommand" + } + + It "module manifest containing empty rootmodule succeeds" -TestCases ( + @{rootModuleValue = $null}, + @{rootModuleValue = ""} + ) { + + param($rootModuleValue) + + New-Item -ItemType Directory -Path testdrive:/module + $testModulePath = "testdrive:/module/test.psd1" + + New-ModuleManifest -Path $testModulePath -RootModule $rootModuleValue + $moduleManifest = Test-ModuleManifest -Path $testModulePath -ErrorAction Stop + $moduleManifest | Should BeOfType System.Management.Automation.PSModuleInfo + $moduleManifest.RootModule | Should BeNullOrEmpty + } + + It "module manifest containing invalid rootmodule returns error" -TestCases ( + @{rootModuleValue = "foo.psd1"; error = "Modules_InvalidManifest"} + ) { + + param($rootModuleValue, $error) + + $testModulePath = "testdrive:/module/test.psd1" + New-Item -ItemType Directory -Path testdrive:/module + New-Item -ItemType File -Path testdrive:/module/$rootModuleValue + + New-ModuleManifest -Path $testModulePath -RootModule $rootModuleValue + { Test-ModuleManifest -Path $testModulePath -ErrorAction Stop } | ShouldBeErrorId "$error,Microsoft.PowerShell.Commands.TestModuleManifestCommand" + } + + It "module manifest containing non-existing rootmodule returns error" -TestCases ( + @{rootModuleValue = "doesnotexist.psm1"; error = "Modules_InvalidRootModuleInModuleManifest"} + ) { + + param($rootModuleValue, $error) + + $testModulePath = "testdrive:/module/test.psd1" + New-Item -ItemType Directory -Path testdrive:/module + + New-ModuleManifest -Path $testModulePath -RootModule $rootModuleValue + { Test-ModuleManifest -Path $testModulePath -ErrorAction Stop } | ShouldBeErrorId "$error,Microsoft.PowerShell.Commands.TestModuleManifestCommand" + } +}