Skip to content

Script file validation and metadata properties population for Publishing, Installing a script. #781

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
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
30 changes: 30 additions & 0 deletions src/code/InstallHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,10 @@ private List<PSResourceInfo> InstallPackage(
}

moduleManifestVersion = parsedMetadataHashtable["ModuleVersion"] as string;
pkg.CompanyName = parsedMetadataHashtable.ContainsKey("CompanyName") ? parsedMetadataHashtable["CompanyName"] as string : String.Empty;
pkg.Copyright = parsedMetadataHashtable.ContainsKey("Copyright") ? parsedMetadataHashtable["Copyright"] as string : String.Empty;
pkg.ReleaseNotes = parsedMetadataHashtable.ContainsKey("ReleaseNotes") ? parsedMetadataHashtable["ReleaseNotes"] as string : String.Empty;
pkg.RepositorySourceLocation = repoUri;

// Accept License verification
if (!_savePkg && !CallAcceptLicense(pkg, moduleManifest, tempInstallPath, newVersion))
Expand All @@ -558,6 +562,32 @@ private List<PSResourceInfo> InstallPackage(
continue;
}
}
else
{
// is script
if (!PSScriptFileInfo.TryTestPSScriptFile(
scriptFileInfoPath: scriptPath,
parsedScript: out PSScriptFileInfo scriptToInstall,
out ErrorRecord[] errors,
out string[] _
))
{
foreach (ErrorRecord error in errors)
{
_cmdletPassedIn.WriteError(error);
}

continue;
}

Hashtable parsedMetadataHashtable = scriptToInstall.ToHashtable();

moduleManifestVersion = parsedMetadataHashtable["ModuleVersion"] as string;
pkg.CompanyName = parsedMetadataHashtable.ContainsKey("CompanyName") ? parsedMetadataHashtable["CompanyName"] as string : String.Empty;
pkg.Copyright = parsedMetadataHashtable.ContainsKey("Copyright") ? parsedMetadataHashtable["Copyright"] as string : String.Empty;
pkg.ReleaseNotes = parsedMetadataHashtable.ContainsKey("ReleaseNotes") ? parsedMetadataHashtable["ReleaseNotes"] as string : String.Empty;
pkg.RepositorySourceLocation = repoUri;
}

// Delete the extra nupkg related files that are not needed and not part of the module/script
DeleteExtraneousFiles(pkgIdentity, tempDirNameVersion);
Expand Down
8 changes: 4 additions & 4 deletions src/code/PSResourceInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,8 @@ public sealed class PSResourceInfo

public Dictionary<string, string> AdditionalMetadata { get; }
public string Author { get; }
public string CompanyName { get; }
public string Copyright { get; }
public string CompanyName { get; internal set; }
public string Copyright { get; internal set; }
public Dependency[] Dependencies { get; }
public string Description { get; }
public Uri IconUri { get; }
Expand All @@ -250,9 +250,9 @@ public sealed class PSResourceInfo
public string Prerelease { get; }
public Uri ProjectUri { get; }
public DateTime? PublishedDate { get; }
public string ReleaseNotes { get; }
public string ReleaseNotes { get; internal set; }
public string Repository { get; }
public string RepositorySourceLocation { get; }
public string RepositorySourceLocation { get; internal set; }
public string[] Tags { get; }
public ResourceType Type { get; }
public DateTime? UpdatedDate { get; }
Expand Down
26 changes: 26 additions & 0 deletions src/code/PSScriptFileInfo.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections;
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

Expand Down Expand Up @@ -489,6 +490,31 @@ out ErrorRecord[] errors
return fileContentsSuccessfullyCreated;
}


internal Hashtable ToHashtable()
{
Hashtable scriptHashtable = new Hashtable(StringComparer.OrdinalIgnoreCase);

Hashtable metadataObjectHashtable = ScriptMetadataComment.ToHashtable();
foreach(string key in metadataObjectHashtable.Keys)
{
if (!scriptHashtable.ContainsKey(key))
{
// shouldn't have duplicate keys, but just for unexpected error handling
scriptHashtable.Add(key, metadataObjectHashtable[key]);
}
}

scriptHashtable.Add(nameof(ScriptHelpComment.Description), ScriptHelpComment.Description);

if (ScriptRequiresComment.RequiredModules.Length != 0)
{
scriptHashtable.Add(nameof(ScriptRequiresComment.RequiredModules), ScriptRequiresComment.RequiredModules);
}

return scriptHashtable;
}

#endregion
}
}
23 changes: 23 additions & 0 deletions src/code/PSScriptMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,29 @@ internal bool UpdateContent(
return true;
}


internal Hashtable ToHashtable()
{
// Constructor would be called first, which handles empty null values for required properties.
Hashtable metadataHashtable = new Hashtable(StringComparer.OrdinalIgnoreCase);
metadataHashtable.Add(nameof(Version), Version);
metadataHashtable.Add(nameof(Guid), Guid);
metadataHashtable.Add(nameof(Author), Author);
metadataHashtable.Add(nameof(CompanyName), CompanyName);
metadataHashtable.Add(nameof(Copyright), Copyright);
metadataHashtable.Add(nameof(Tags), Tags);
metadataHashtable.Add(nameof(LicenseUri), LicenseUri);
metadataHashtable.Add(nameof(ProjectUri), ProjectUri);
metadataHashtable.Add(nameof(IconUri), IconUri);
metadataHashtable.Add(nameof(ExternalModuleDependencies), ExternalModuleDependencies);
metadataHashtable.Add(nameof(RequiredScripts), RequiredScripts);
metadataHashtable.Add(nameof(ExternalScriptDependencies), ExternalScriptDependencies);
metadataHashtable.Add(nameof(ReleaseNotes), ReleaseNotes);
metadataHashtable.Add(nameof(PrivateData), PrivateData);

return metadataHashtable;
}

#endregion
}
}
197 changes: 11 additions & 186 deletions src/code/PublishPSResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -219,29 +219,31 @@ protected override void EndProcessing()
return;
}

Hashtable parsedMetadata;
Hashtable parsedMetadata = new Hashtable(StringComparer.OrdinalIgnoreCase);
if (resourceType == ResourceType.Script)
{
// Check that script metadata is valid
if (!TryParseScriptMetadata(
out parsedMetadata,
pathToScriptFileToPublish,
out ErrorRecord[] errors))
if (!PSScriptFileInfo.TryTestPSScriptFile(
scriptFileInfoPath: pathToScriptFileToPublish,
parsedScript: out PSScriptFileInfo scriptToPublish,
out ErrorRecord[] errors,
out string[] _
))
{
foreach (ErrorRecord err in errors)
foreach (ErrorRecord error in errors)
{
WriteError(err);
WriteError(error);
}

return;
}

parsedMetadata = scriptToPublish.ToHashtable();

_pkgName = System.IO.Path.GetFileNameWithoutExtension(pathToScriptFileToPublish);
}
else
{
// parsedMetadata needs to be initialized for modules, will later be passed in to create nuspec
parsedMetadata = new Hashtable();
if (!string.IsNullOrEmpty(pathToModuleManifestToPublish))
{
_pkgName = System.IO.Path.GetFileNameWithoutExtension(pathToModuleManifestToPublish);
Expand Down Expand Up @@ -746,183 +748,6 @@ private Hashtable ParseRequiredModules(Hashtable parsedMetadataHash)
return dependenciesHash;
}

private bool TryParseScriptMetadata(
out Hashtable parsedMetadata,
string filePath,
out ErrorRecord[] errors)
{
parsedMetadata = new Hashtable();
List<ErrorRecord> parseMetadataErrors = new List<ErrorRecord>();

// a valid example script will have this format:
/* <#PSScriptInfo
.VERSION 1.6
.GUID abf490023 - 9128 - 4323 - sdf9a - jf209888ajkl
.AUTHOR Jane Doe
.COMPANYNAME Microsoft
.COPYRIGHT
.TAGS Windows MacOS
#>

<#

.SYNOPSIS
Synopsis description here
.DESCRIPTION
Description here
.PARAMETER Name
.EXAMPLE
Example cmdlet here

#>
*/

// Parse the script file
var ast = Parser.ParseFile(
filePath,
out System.Management.Automation.Language.Token[] tokens,
out ParseError[] parserErrors);

if (parserErrors.Length > 0)
{
foreach (ParseError err in parserErrors)
{
// we ignore WorkFlowNotSupportedInPowerShellCore errors, as this is common in scripts currently on PSGallery
if (!String.Equals(err.ErrorId, "WorkflowNotSupportedInPowerShellCore", StringComparison.OrdinalIgnoreCase))
{
var message = String.Format("Could not parse '{0}' as a PowerShell script file due to {1}.", filePath, err.Message);
var ex = new ArgumentException(message);
var psScriptFileParseError = new ErrorRecord(ex, err.ErrorId, ErrorCategory.ParserError, null);
parseMetadataErrors.Add(psScriptFileParseError);
}
}

errors = parseMetadataErrors.ToArray();
return false;
}

if (ast == null)
{
var astNullMessage = String.Format(".ps1 file was parsed but AST was null");
var astNullEx = new ArgumentException(astNullMessage);
var astCouldNotBeCreatedError = new ErrorRecord(astNullEx, "ASTCouldNotBeCreated", ErrorCategory.ParserError, null);

parseMetadataErrors.Add(astCouldNotBeCreatedError);
errors = parseMetadataErrors.ToArray();
return false;

}

// Get the block/group comment beginning with <#PSScriptInfo
List<System.Management.Automation.Language.Token> commentTokens = tokens.Where(a => String.Equals(a.Kind.ToString(), "Comment", StringComparison.OrdinalIgnoreCase)).ToList();
string commentPattern = PSScriptInfoCommentString;
Regex rg = new Regex(commentPattern);
List<System.Management.Automation.Language.Token> psScriptInfoCommentTokens = commentTokens.Where(a => rg.IsMatch(a.Extent.Text)).ToList();

if (psScriptInfoCommentTokens.Count() == 0 || psScriptInfoCommentTokens[0] == null)
{
var message = String.Format("PSScriptInfo comment was missing or could not be parsed");
var ex = new ArgumentException(message);
var psCommentMissingError = new ErrorRecord(ex, "psScriptInfoCommentMissingError", ErrorCategory.ParserError, null);
parseMetadataErrors.Add(psCommentMissingError);
errors = parseMetadataErrors.ToArray();
return false;
}

string[] commentLines = Regex.Split(psScriptInfoCommentTokens[0].Text, "[\r\n]").Where(x => !String.IsNullOrEmpty(x)).ToArray();
string keyName = String.Empty;
string value = String.Empty;

/**
If comment line count is not more than two, it doesn't have the any metadata property
comment block would look like:
<#PSScriptInfo
#>
*/

if (commentLines.Count() > 2)
{
for (int i = 1; i < commentLines.Count(); i++)
{
string line = commentLines[i];
if (String.IsNullOrEmpty(line))
{
continue;
}

// A line is starting with . conveys a new metadata property
if (line.Trim().StartsWith("."))
{
string[] parts = line.Trim().TrimStart('.').Split();
keyName = parts[0].ToLower();
value = parts.Count() > 1 ? String.Join(" ", parts.Skip(1)) : String.Empty;
parsedMetadata.Add(keyName, value);
}
}
}

// get .DESCRIPTION comment
CommentHelpInfo scriptCommentInfo = ast.GetHelpContent();
if (scriptCommentInfo == null)
{
var message = String.Format("PSScript file is missing the required Description comment block in the script contents.");
var ex = new ArgumentException(message);
var psScriptMissingHelpContentCommentBlockError = new ErrorRecord(ex, "PSScriptMissingHelpContentCommentBlock", ErrorCategory.ParserError, null);
parseMetadataErrors.Add(psScriptMissingHelpContentCommentBlockError);
errors = parseMetadataErrors.ToArray();
return false;
}

if (!String.IsNullOrEmpty(scriptCommentInfo.Description) && !scriptCommentInfo.Description.Contains("<#") && !scriptCommentInfo.Description.Contains("#>"))
{
parsedMetadata.Add("description", scriptCommentInfo.Description);
}
else
{
var message = String.Format("PSScript is missing the required Description property or Description value contains '<#' or '#>' which is invalid");
var ex = new ArgumentException(message);
var psScriptMissingDescriptionOrInvalidPropertyError = new ErrorRecord(ex, "MissingOrInvalidDescriptionInScriptMetadata", ErrorCategory.ParserError, null);
parseMetadataErrors.Add(psScriptMissingDescriptionOrInvalidPropertyError);
errors = parseMetadataErrors.ToArray();
return false;
}


// Check that the mandatory properites for a script are there (version, author, guid, in addition to description)
if (!parsedMetadata.ContainsKey("version") || String.IsNullOrWhiteSpace(parsedMetadata["version"].ToString()))
{
var message = "No version was provided in the script metadata. Script metadata must specify a version, author, description, and Guid.";
var ex = new ArgumentException(message);
var MissingVersionInScriptMetadataError = new ErrorRecord(ex, "MissingVersionInScriptMetadata", ErrorCategory.InvalidData, null);
parseMetadataErrors.Add(MissingVersionInScriptMetadataError);
errors = parseMetadataErrors.ToArray();
return false;
}

if (!parsedMetadata.ContainsKey("author") || String.IsNullOrWhiteSpace(parsedMetadata["author"].ToString()))
{
var message = "No author was provided in the script metadata. Script metadata must specify a version, author, description, and Guid.";
var ex = new ArgumentException(message);
var MissingAuthorInScriptMetadataError = new ErrorRecord(ex, "MissingAuthorInScriptMetadata", ErrorCategory.InvalidData, null);
parseMetadataErrors.Add(MissingAuthorInScriptMetadataError);
errors = parseMetadataErrors.ToArray();
return false;
}

if (!parsedMetadata.ContainsKey("guid") || String.IsNullOrWhiteSpace(parsedMetadata["guid"].ToString()))
{
var message = "No guid was provided in the script metadata. Script metadata must specify a version, author, description, and Guid.";
var ex = new ArgumentException(message);
var MissingGuidInScriptMetadataError = new ErrorRecord(ex, "MissingGuidInScriptMetadata", ErrorCategory.InvalidData, null);
parseMetadataErrors.Add(MissingGuidInScriptMetadataError);
errors = parseMetadataErrors.ToArray();
return false;
}

errors = parseMetadataErrors.ToArray();
return true;
}

private bool CheckDependenciesExist(Hashtable dependencies, string repositoryName)
{
// Check to see that all dependencies are in the repository
Expand Down
24 changes: 24 additions & 0 deletions test/InstallPSResource.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Describe 'Test Install-PSResource for Module' {

BeforeAll {
$PSGalleryName = Get-PSGalleryName
$PSGalleryUri = Get-PSGalleryLocation
$NuGetGalleryName = Get-NuGetGalleryName
$testModuleName = "test_module"
$testModuleName2 = "TestModule99"
Expand Down Expand Up @@ -180,6 +181,29 @@ Describe 'Test Install-PSResource for Module' {
($env:PSModulePath).Contains($pkg.InstalledLocation)
}

It "Install resource with companyname, copyright and repository source location and validate" {
Install-PSResource -Name $testModuleName -Version "5.2.5-alpha001" -Repository PSGallery -TrustRepository
$pkg = Get-PSResource $testModuleName
$pkg.Version | Should -Be "5.2.5"
$pkg.Prerelease | Should -Be "alpha001"

$pkg.CompanyName | Should -Be "Anam"
$pkg.Copyright | Should -Be "(c) Anam Navied. All rights reserved."
$pkg.RepositorySourceLocation | Should -Be $PSGalleryUri
}


It "Install script with companyname, copyright, and repository source location and validate" {
Install-PSResource -Name "Install-VSCode" -Version "1.4.2" -Repository $PSGalleryName -TrustRepository

$res = Get-PSResource "Install-VSCode" -Version "1.4.2"
$res.Name | Should -Be "Install-VSCode"
$res.Version | Should -Be "1.4.2.0"
$res.CompanyName | Should -Be "Microsoft Corporation"
$res.Copyright | Should -Be "(c) Microsoft Corporation"
$res.RepositorySourceLocation | Should -Be $PSGalleryUri
}

# Windows only
It "Install resource under CurrentUser scope - Windows only" -Skip:(!(Get-IsWindows)) {
Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository -Scope CurrentUser
Expand Down
Loading