From 77118379c58f2861233020fd0c466b48798b7cc3 Mon Sep 17 00:00:00 2001 From: t-ayousuf Date: Tue, 12 Jul 2022 11:02:34 -0700 Subject: [PATCH 01/42] Add .gitattributes, .gitignore, and README.md. --- .gitattributes | 63 +++++++++ .gitignore | 363 +++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 4 + 3 files changed, 430 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9491a2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,363 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd \ No newline at end of file diff --git a/README.md b/README.md index e3c7b8e..961344f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +<<<<<<< HEAD # Microsoft.PowerShell.Archive Module [Microsoft.PowerShell.Archive module](https://technet.microsoft.com/en-us/library/dn818910.aspx) contains cmdlets that let you create and extract ZIP archives. @@ -12,3 +13,6 @@ ## [Expand-Archive](https://technet.microsoft.com/library/dn841359.aspx) examples 1. Extract the contents of an archive in the current folder: `Expand-Archive -Path SampleArchive.zip` 2. Use -Force parameter to overwrite existing files by those in the archive: `Expand-Archive -Path .\SampleArchive.zip -DestinationPath .\ExistingDir -Force` +======= +# Microsoft.PowerShell.Archive-vnext +>>>>>>> 168d6d4 (Add .gitattributes, .gitignore, and README.md.) From 7512b083c64f661d6676fc80f312b1faad0aff2d Mon Sep 17 00:00:00 2001 From: t-ayousuf Date: Tue, 12 Jul 2022 11:02:37 -0700 Subject: [PATCH 02/42] Add project files. --- .vscode/launch.json | 14 + Microsoft.PowerShell.Archive.sln | 25 ++ Tests/Compress-Archive.Tests.ps1 | 367 ++++++++++++++++++++++++ src/CompressArchiveCommand.cs | 174 +++++++++++ src/ErrorMessages.cs | 14 + src/Microsoft.PowerShell.Archive.csproj | 12 + src/Properties/launchSettings.json | 12 + 7 files changed, 618 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 Microsoft.PowerShell.Archive.sln create mode 100644 Tests/Compress-Archive.Tests.ps1 create mode 100644 src/CompressArchiveCommand.cs create mode 100644 src/ErrorMessages.cs create mode 100644 src/Microsoft.PowerShell.Archive.csproj create mode 100644 src/Properties/launchSettings.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..b6c7b60 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "PowerShell: Attach to PowerShell Host Process", + "type": "PowerShell", + "request": "attach", + "runspaceId": 1 + } + ] +} \ No newline at end of file diff --git a/Microsoft.PowerShell.Archive.sln b/Microsoft.PowerShell.Archive.sln new file mode 100644 index 0000000..8e57f5f --- /dev/null +++ b/Microsoft.PowerShell.Archive.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32611.2 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerShell.Archive", "src\Microsoft.PowerShell.Archive.csproj", "{24838E64-AAC0-4E9E-B8B6-269AB1514046}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {24838E64-AAC0-4E9E-B8B6-269AB1514046}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {24838E64-AAC0-4E9E-B8B6-269AB1514046}.Debug|Any CPU.Build.0 = Debug|Any CPU + {24838E64-AAC0-4E9E-B8B6-269AB1514046}.Release|Any CPU.ActiveCfg = Release|Any CPU + {24838E64-AAC0-4E9E-B8B6-269AB1514046}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {BEAF8507-5CAD-4FEF-8BF4-691AC5FE4587} + EndGlobalSection +EndGlobal diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 new file mode 100644 index 0000000..8fe5d3d --- /dev/null +++ b/Tests/Compress-Archive.Tests.ps1 @@ -0,0 +1,367 @@ +<############################################################################################ + # File: Pester.Commands.Cmdlets.ArchiveTests.ps1 + # Commands.Cmdlets.ArchiveTests suite contains Tests that are + # used for validating Microsoft.PowerShell.Archive module. + ############################################################################################> + $script:TestSourceRoot = $PSScriptRoot + Write-Output $script:TestSourceRoot + $DS = [System.IO.Path]::DirectorySeparatorChar + if ($IsWindows -eq $null) { + $IsWindows = $PSVersionTable.PSEdition -eq "Desktop" + } + + Describe("Microsoft.PowerShell.Archive tests") { + BeforeAll { + + $DS = [System.IO.Path]::DirectorySeparatorChar + + $originalProgressPref = $ProgressPreference + $ProgressPreference = "SilentlyContinue" + $originalPSModulePath = $env:PSModulePath + # make sure we use the one in this repo + $env:PSModulePath = "$($script:TestSourceRoot)\..;$($env:PSModulePath)" + + # Add compression assemblies + function Add-CompressionAssemblies { + Add-Type -AssemblyName System.IO.Compression + if ($psedition -eq "Core") + { + Add-Type -AssemblyName System.IO.Compression.ZipFile + } + else + { + Add-Type -AssemblyName System.IO.Compression.FileSystem + } + } + + Add-CompressionAssemblies + + # Used for validating an archive's contents + function Test-Archive { + param + ( + [string] $archivePath, + [string[]] $expectedEntries + ) + + try + { + $archiveFileStreamArgs = @($archivePath, [System.IO.FileMode]::Open) + $archiveFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList $archiveFileStreamArgs + + $zipArchiveArgs = @($archiveFileStream, [System.IO.Compression.ZipArchiveMode]::Read, $false) + $zipArchive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $zipArchiveArgs + + $actualEntryCount = $zipArchive.Entries.Count + $actualEntryCount | Should -Be $expectedEntries.Length + + # Get a list of archive entries + + ForEach ($expectedArchiveEntry in $expectedEntries) { + $expectedArchiveEntry | Should -BeIn $zipArchive.Entries + } + } + finally + { + if ($null -ne $zipArchive) { $zipArchive.Dispose()} + if ($null -ne $archiveFileStream) { $archiveFileStream.Dispose() } + } + } + } + + AfterAll { + $global:ProgressPreference = $originalProgressPref + $env:PSModulePath = $originalPSModulePath + } + + Context "Parameter set validation tests" { + BeforeAll { + function CompressArchivePathParameterSetValidator { + param + ( + [string[]] $path, + [string] $destinationPath + ) + + try + { + Compress-Archive -Path $path -DestinationPath $destinationPath + throw "ValidateNotNullOrEmpty attribute is missing on one of parameters belonging to Path parameterset." + } + catch + { + $_.FullyQualifiedErrorId | Should -Be "ParameterArgumentValidationError,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } + + function CompressArchiveLiteralPathParameterSetValidator { + param + ( + [string[]] $literalPath, + [string] $destinationPath, + [string] $compressionLevel = "Optimal" + ) + + try + { + Compress-Archive -LiteralPath $literalPath -DestinationPath $destinationPath + throw "ValidateNotNullOrEmpty attribute is missing on one of parameters belonging to LiteralPath parameterset." + } + catch + { + $_.FullyQualifiedErrorId | Should -Be "ParameterArgumentValidationError,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } + + + function CompressArchiveInvalidPathValidator { + param + ( + [string[]] $path, + [string] $destinationPath, + [string] $invalidPath, + [string] $expectedFullyQualifiedErrorId + ) + + try + { + Compress-Archive -Path $path -DestinationPath $destinationPath + throw "Failed to validate that an invalid Path $invalidPath was supplied as input to Compress-Archive cmdlet." + } + catch + { + $_.FullyQualifiedErrorId | Should -Be $expectedFullyQualifiedErrorId + } + + try + { + Compress-Archive -LiteralPath $path -DestinationPath $destinationPath + throw "Failed to validate that an invalid Path $invalidPath was supplied as input to Compress-Archive cmdlet." + } + catch + { + $_.FullyQualifiedErrorId | Should -Be $expectedFullyQualifiedErrorId + } + } + + # Set up files for tests + New-Item $TestDrive$($DS)SourceDir -Type Directory | Out-Null + $content = "Some Data" + $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)Sample-1.txt + } + + + It "Validate errors from Compress-Archive with NULL & EMPTY values for Path, LiteralPath, DestinationPath, CompressionLevel parameters" { + $sourcePath = "$TestDrive$($DS)SourceDir" + $destinationPath = "$TestDrive$($DS)SampleSingleFile.zip" + + CompressArchivePathParameterSetValidator $null $destinationPath + CompressArchivePathParameterSetValidator $sourcePath $null + CompressArchivePathParameterSetValidator $null $null + + CompressArchivePathParameterSetValidator "" $destinationPath + CompressArchivePathParameterSetValidator $sourcePath "" + CompressArchivePathParameterSetValidator "" "" + + CompressArchiveLiteralPathParameterSetValidator $null $destinationPath + CompressArchiveLiteralPathParameterSetValidator $sourcePath $null + CompressArchiveLiteralPathParameterSetValidator $null $null + + CompressArchiveLiteralPathParameterSetValidator "" $destinationPath + CompressArchiveLiteralPathParameterSetValidator $sourcePath "" + CompressArchiveLiteralPathParameterSetValidator "" "" + } + + It "Validate errors from Compress-Archive when invalid path (non-existing path / non-filesystem path) is supplied for Path or LiteralPath parameters" { + CompressArchiveInvalidPathValidator "$TestDrive$($DS)InvalidPath" $TestDrive "$TestDrive$($DS)InvalidPath" "PathNotFound,Microsoft.PowerShell.Archive.CompressArchiveCommand" + + $path = @("$TestDrive", "$TestDrive$($DS)InvalidPath") + CompressArchiveInvalidPathValidator $path $TestDrive "$TestDrive$($DS)InvalidPath" "PathNotFound,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + + It "Validate error when non-existant path is supplied to DestinationPath parameter" { + CompressArchiveInvalidPathValidator "$TestDrive" "$TestDrive$($DS)NonExistingDirectory$($DS)sample.zip" "$TestDrive$($DS)NonExistingDirectory$($DS)sample.zip" "PathNotFound,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + + It "Validate error from Compress-Archive when duplicate paths are supplied as input to Path parameter" -Tag this { + $sourcePath = @( + "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt", + "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt") + $destinationPath = "$TestDrive$($DS)DuplicatePaths.zip" + + try + { + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + throw "Failed to detect that duplicate Path $sourcePath is supplied as input to Path parameter." + } + catch + { + $_.FullyQualifiedErrorId | Should -Be "DuplicatePathFound,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } + + It "Validate error from Compress-Archive when duplicate paths are supplied as input to LiteralPath parameter" { + $sourcePath = @( + "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt", + "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt") + $destinationPath = "$TestDrive$($DS)DuplicatePaths.zip" + + try + { + Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath + throw "Failed to detect that duplicate Path $sourcePath is supplied as input to LiteralPath parameter." + } + catch + { + $_.FullyQualifiedErrorId | Should -Be "DuplicatePathFound,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } + + ## From 504 + It "Validate that Source Path can be at SystemDrive location" -skip:(!$IsWindows) { + $sourcePath = "$env:SystemDrive$($DS)SourceDir" + $destinationPath = "$TestDrive$($DS)SampleFromSystemDrive.zip" + New-Item $sourcePath -Type Directory | Out-Null # not enough permissions to write to drive root on Linux + "Some Data" | Out-File -FilePath $sourcePath$($DS)SampleSourceFileForArchive.txt + try + { + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + Test-Path $destinationPath | Should Be $true + } + finally + { + del "$sourcePath" -Force -Recurse -ErrorAction SilentlyContinue + } + } + } + + Context "Basic functional tests" { + BeforeAll { + New-Item $TestDrive$($DS)SourceDir -Type Directory | Out-Null + New-Item $TestDrive$($DS)SourceDir$($DS)ChildDir-1 -Type Directory | Out-Null + New-Item $TestDrive$($DS)SourceDir$($DS)ChildDir-2 -Type Directory | Out-Null + New-Item $TestDrive$($DS)SourceDir$($DS)ChildEmptyDir -Type Directory | Out-Null + + # create an empty directory + New-Item $TestDrive$($DS)EmptyDir -Type Directory | Out-Null + + $content = "Some Data" + $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)Sample-1.txt + $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)Sample-2.txt + $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-3.txt + $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-4.txt + $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)ChildDir-2$($DS)Sample-5.txt + $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)ChildDir-2$($DS)Sample-6.txt + + "Some Text" > $TestDrive$($DS)Sample.unzip + "Some Text" > $TestDrive$($DS)Sample.cab + + $preCreatedArchivePath = Join-Path $PSScriptRoot "SamplePreCreatedArchive.archive" + Copy-Item $preCreatedArchivePath $TestDrive$($DS)SamplePreCreatedArchive.zip -Force + + $preCreatedArchivePath = Join-Path $PSScriptRoot "TrailingSpacer.archive" + Copy-Item $preCreatedArchivePath $TestDrive$($DS)TrailingSpacer.zip -Force + } + + + It "Validate that a single file can be compressed using Compress-Archive cmdlet" { + $sourcePath = "$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-3.txt" + $destinationPath = "$TestDrive$($DS)SampleSingleFile.zip" + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + $destinationPath | Should -Exist + } + + It "Validate that an empty folder can be compressed" -Tag "this" { + $sourcePath = "$TestDrive$($DS)EmptyDir" + $destinationPath = "$TestDrive$($DS)EmptyDir.zip" + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + Test-Archive $destinationPath "EmptyDir/" + } + } + + Context "Update tests" { + BeforeAll { + New-Item $TestDrive$($DS)SourceDir -Type Directory | Out-Null + New-Item $TestDrive$($DS)SourceDir$($DS)ChildDir-1 -Type Directory | Out-Null + + $content = "Some Data" + $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)Sample-1.txt + $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-2.txt + } + + It "Validate error from Compress-Archive when archive file already exists and -Update and -Force parameters is not specified" { + $sourcePath = "$TestDrive$($DS)SourceDir" + $destinationPath = "$TestDrive$($DS)ValidateErrorWhenUpdateNotSpecified.zip" + + try + { + "Some Data" > $destinationPath + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + throw "Failed to validate that an archive file format $destinationPath already exists and -Update switch parameter is not specified while running Compress-Archive command." + } + catch + { + $_.FullyQualifiedErrorId | Should -Be "ArchiveFileExists,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } + } + + Context "Relative Path tests" { + BeforeAll { + New-Item $TestDrive$($DS)SourceDir -Type Directory | Out-Null + New-Item $TestDrive$($DS)SourceDir$($DS)ChildDir-1 -Type Directory | Out-Null + + $content = "Some Data" + $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)Sample-1.txt + $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-2.txt + } + + # From 568 + It "Validate that relative path can be specified as Path parameter of Compress-Archive cmdlet" { + $sourcePath = ".$($DS)SourceDir" + $destinationPath = "RelativePathForPathParameter.zip" + try + { + Push-Location $TestDrive + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + Test-Path $destinationPath | Should Be $true + } + finally + { + Pop-Location + } + } + + # From 582 + It "Validate that relative path can be specified as LiteralPath parameter of Compress-Archive cmdlet" { + $sourcePath = ".$($DS)SourceDir" + $destinationPath = "RelativePathForLiteralPathParameter.zip" + try + { + Push-Location $TestDrive + Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath + Test-Path $destinationPath | Should Be $true + } + finally + { + Pop-Location + } + } + + # From 596 + It "Validate that relative path can be specified as DestinationPath parameter of Compress-Archive cmdlet" { + $sourcePath = "$TestDrive$($DS)SourceDir" + $destinationPath = ".$($DS)RelativePathForDestinationPathParameter.zip" + try + { + Push-Location $TestDrive + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + Test-Path $destinationPath | Should Be $true + } + finally + { + Pop-Location + } + } + } +} \ No newline at end of file diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs new file mode 100644 index 0000000..0ee48c1 --- /dev/null +++ b/src/CompressArchiveCommand.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Management.Automation; + +namespace Microsoft.PowerShell.Archive +{ + [Cmdlet("Compress", "Archive", SupportsShouldProcess = true)] + [OutputType(typeof(System.IO.FileInfo))] + public class CompressArchiveCommand : PSCmdlet + { + [Parameter(Mandatory = true, Position = 0, ParameterSetName = "Path", ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [Parameter(Mandatory = true, Position = 0, ParameterSetName = "PathWithForce", ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [Parameter(Mandatory = true, Position = 0, ParameterSetName = "PathWithUpdate", ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string[]? Path { get; set; } + + [Parameter(Mandatory = true, ParameterSetName = "LiteralPath", ValueFromPipeline = false, ValueFromPipelineByPropertyName = true)] + [Parameter(Mandatory = true, ParameterSetName = "LiteralPathWithForce", ValueFromPipeline = false, ValueFromPipelineByPropertyName = true)] + [Parameter(Mandatory = true, ParameterSetName = "LiteralPathWithUpdate", ValueFromPipeline = false, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + [Alias("PSPath")] + public string[]? LiteralPath { get; set; } + + [Parameter(Mandatory = true, Position = 1, ValueFromPipeline = false, ValueFromPipelineByPropertyName = false)] + [ValidateNotNullOrEmpty] + public string? DestinationPath { get; set; } + + [Parameter(Mandatory = true, ParameterSetName = "PathWithUpdate", ValueFromPipeline = false, ValueFromPipelineByPropertyName = false)] + [Parameter(Mandatory = true, ParameterSetName = "LiteralPathWithUpdate", ValueFromPipeline = false, ValueFromPipelineByPropertyName = false)] + public SwitchParameter Update { get; set; } + + [Parameter(Mandatory = true, ParameterSetName = "PathWithForce", ValueFromPipeline = false, ValueFromPipelineByPropertyName = false)] + [Parameter(Mandatory = true, ParameterSetName = "LiteralPathWithForce", ValueFromPipeline = false, ValueFromPipelineByPropertyName = false)] + public SwitchParameter Force { get; set; } + + [Parameter()] + public SwitchParameter PassThru { get; set; } = false; + + [Parameter()] + [ValidateNotNullOrEmpty] + public System.IO.Compression.CompressionLevel CompressionLevel { get; set; } + + private HashSet _sourcePaths; + + private HashSet _duplicatePaths; + + public CompressArchiveCommand() + { + _sourcePaths = new HashSet(); + _duplicatePaths = new HashSet(); + } + + protected override void BeginProcessing() + { + base.BeginProcessing(); + ResolvePath(DestinationPath); + } + + protected override void ProcessRecord() + { + //Validate paths + string[]? paths; + paths = ParameterSetName.StartsWith("Path") ? ResolvePathWithWildcards(Path) : ResolvePathWithoutWildcards(LiteralPath); + + foreach (var path in paths) + { + //Add path to source paths + if (!_sourcePaths.Add(path)) + { + //If the set already contains the path, add it to the set of duplicates + _duplicatePaths.Add(path); + } + } + + + } + + protected override void EndProcessing() + { + //If there are duplicate paths, throw an error + if (_duplicatePaths.Count > 0) + { + var errorMsg = String.Format(ErrorMessages.DuplicatePathsMessage, _duplicatePaths.ToString()); + var exception = new System.ArgumentException(errorMsg); + ErrorRecord errorRecord = new ErrorRecord(exception, "DuplicatePathFound", ErrorCategory.InvalidArgument, _duplicatePaths); + ThrowTerminatingError(errorRecord); + + } + } + + protected override void StopProcessing() + { + base.StopProcessing(); + } + + private string[] ResolvePathWithWildcards(string[] paths) + { + List outputPaths = new List(); + foreach (var path in paths) + { + var resolvedPaths = GetResolvedProviderPathFromPSPath(path, out var providerInfo); + if (providerInfo.Name != "FileSystem") + { + //Throw an error + } + outputPaths.AddRange(resolvedPaths); + } + + return outputPaths.ToArray(); + } + + private string[] ResolvePathWithoutWildcards(string[] paths) + { + string[] outputPaths = new string[paths.Length]; + for (int i=0; i 1 || providerInfo.Name != "FileSystem") + { + //Throw an error: duplicate paths + } + + if (resolvedPath.Count == 1 && resolvedPath[0] != unresolvedPath) + { + //Throw an error: duplicate paths + } + + if (resolvedPath.Count == 1 && resolvedPath[0] == unresolvedPath) return unresolvedPath; + + } catch (Exception ex) + { + + } + + ////If unresolvedPath doesn't exist, throw an error + //if (!System.IO.File.Exists(unresolvedPath)) + //{ + // //Throw an error: path not found + // var errorMsg = String.Format(ErrorMessages.PathNotFoundMessage, path); + // var exception = new System.InvalidOperationException(errorMsg); + // var errorRecord = new ErrorRecord(exception, "PathNotFound", ErrorCategory.InvalidArgument, path); + // ThrowTerminatingError(errorRecord); + //} + + return null; + } + } +} diff --git a/src/ErrorMessages.cs b/src/ErrorMessages.cs new file mode 100644 index 0000000..0a3b66f --- /dev/null +++ b/src/ErrorMessages.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Text; + +namespace Microsoft.PowerShell.Archive +{ + internal static class ErrorMessages + { + internal static string PathNotFoundMessage = "The path {0} could not be found"; + + internal static string DuplicatePathsMessage = "The path(s) {0} have been specified more than once."; + } +} diff --git a/src/Microsoft.PowerShell.Archive.csproj b/src/Microsoft.PowerShell.Archive.csproj new file mode 100644 index 0000000..4e89a6d --- /dev/null +++ b/src/Microsoft.PowerShell.Archive.csproj @@ -0,0 +1,12 @@ + + + + netstandard2.1 + enable + + + + + + + diff --git a/src/Properties/launchSettings.json b/src/Properties/launchSettings.json new file mode 100644 index 0000000..4f406c4 --- /dev/null +++ b/src/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Microsoft.PowerShell.Archive": { + "commandName": "Project" + }, + "PowerShell 7": { + "commandName": "Executable", + "executablePath": "pwsh", + "commandLineArgs": "-NoExit -Command \"& Import-Module .\\Microsoft.PowerShell.Archive.dll" + } + } +} \ No newline at end of file From aff9728225e90d9a55349a5f41d88a17457e7e51 Mon Sep 17 00:00:00 2001 From: Abdullah Yousuf Date: Tue, 12 Jul 2022 11:33:57 -0700 Subject: [PATCH 03/42] added path helper, archive entry --- src/ArchiveEntry.cs | 19 ++++++++++++++ src/CompressArchiveCommand.cs | 5 ++++ src/PathHelper.cs | 49 +++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 src/ArchiveEntry.cs create mode 100644 src/PathHelper.cs diff --git a/src/ArchiveEntry.cs b/src/ArchiveEntry.cs new file mode 100644 index 0000000..8865d5f --- /dev/null +++ b/src/ArchiveEntry.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.PowerShell.Archive +{ + internal class ArchiveEntry + { + public string Name { get; set; } + + public string FullPath { get; set; } + + public ArchiveEntry(string name, string fullPath) + { + Name = name; + FullPath = fullPath; + } + } +} diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index 0ee48c1..351fe91 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -63,8 +63,13 @@ protected override void ProcessRecord() string[]? paths; paths = ParameterSetName.StartsWith("Path") ? ResolvePathWithWildcards(Path) : ResolvePathWithoutWildcards(LiteralPath); + PathHelper pathHelper = new PathHelper(this); + foreach (var path in paths) { + Sess + pathHelper.GetEntryRecordsForPath(path, false); + //Add path to source paths if (!_sourcePaths.Add(path)) { diff --git a/src/PathHelper.cs b/src/PathHelper.cs new file mode 100644 index 0000000..41ca6cf --- /dev/null +++ b/src/PathHelper.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Text; + +namespace Microsoft.PowerShell.Archive +{ + internal class PathHelper + { + private PSCmdlet _cmdlet; + + internal PathHelper(PSCmdlet cmdlet) + { + _cmdlet = cmdlet; + } + + internal List? GetEntryRecordsForPath(string path, bool hasWildcards) + { + var resolvedPaths = _cmdlet.InvokeProvider.ChildItem.GetNames(new string[] { path }, ReturnContainers.ReturnAllContainers, true, uint.MaxValue, true, hasWildcards); + foreach (var entry in resolvedPaths) + { + + _cmdlet.WriteObject(entry); + } + + return null; + } + + private List? GetArchiveEntriesForLiteralPath(string path) + { + //Get the unresolved path + string unresolvedPath = _cmdlet.GetUnresolvedProviderPathFromPSPath(path); + + //Check if it exists + if (System.IO.Directory.Exists(unresolvedPath)) + { + //Get all descendents + var resolvedPaths = _cmdlet.InvokeProvider.ChildItem.GetNames(new string[] { path }, returnContainers: ReturnContainers.ReturnAllContainers, recurse: true, depth: uint.MaxValue, force: true, literalPath: false); + } else if (!System.IO.File.Exists(unresolvedPath)) + { + //Throw an error + } + + + //Return archive entries + return null; + } + } +} From e535995772821d05a1cd351e780ab3947a305fee Mon Sep 17 00:00:00 2001 From: Abdullah Yousuf Date: Tue, 12 Jul 2022 13:29:26 -0700 Subject: [PATCH 04/42] updated path helper --- src/CompressArchiveCommand.cs | 2 +- src/ErrorMessages.cs | 2 + src/PathHelper.cs | 154 ++++++++++++++++++++++++++++++---- 3 files changed, 141 insertions(+), 17 deletions(-) diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index 351fe91..4fb7bb8 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -67,7 +67,7 @@ protected override void ProcessRecord() foreach (var path in paths) { - Sess + pathHelper.GetEntryRecordsForPath(path, false); //Add path to source paths diff --git a/src/ErrorMessages.cs b/src/ErrorMessages.cs index 0a3b66f..aee5849 100644 --- a/src/ErrorMessages.cs +++ b/src/ErrorMessages.cs @@ -10,5 +10,7 @@ internal static class ErrorMessages internal static string PathNotFoundMessage = "The path {0} could not be found"; internal static string DuplicatePathsMessage = "The path(s) {0} have been specified more than once."; + + internal static string InvalidPathMessage = "The path(s) {0} are invalid."; } } diff --git a/src/PathHelper.cs b/src/PathHelper.cs index 41ca6cf..2f0de54 100644 --- a/src/PathHelper.cs +++ b/src/PathHelper.cs @@ -1,10 +1,14 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Management.Automation; using System.Text; namespace Microsoft.PowerShell.Archive { + //To-do: Add exception handling + internal class PathHelper { private PSCmdlet _cmdlet; @@ -14,36 +18,154 @@ internal PathHelper(PSCmdlet cmdlet) _cmdlet = cmdlet; } - internal List? GetEntryRecordsForPath(string path, bool hasWildcards) + internal List GetEntryRecordsForPath(string[] paths, bool literalPath) + { + if (literalPath) return GetArchiveEntriesForLiteralPath(paths); + else return GetArchiveEntriesForNonLiteralPaths(paths); + } + + private List GetArchiveEntriesForNonLiteralPaths(string[] paths) { - var resolvedPaths = _cmdlet.InvokeProvider.ChildItem.GetNames(new string[] { path }, ReturnContainers.ReturnAllContainers, true, uint.MaxValue, true, hasWildcards); - foreach (var entry in resolvedPaths) + List entries = new List(); + + //Used to keep track of non-filesystem paths + HashSet nonfilesystemPaths = new HashSet(); + + foreach (var path in paths) { - - _cmdlet.WriteObject(entry); + //Resolve the path + var resolvedPaths = _cmdlet.GetResolvedProviderPathFromPSPath(path, out var providerInfo); + + //Check if the path if from the filesystem + if (providerInfo.Name != "FileSystem") + { + //Add the path to the set of non-filesystem paths + nonfilesystemPaths.Add(path); + continue; + } + + //Go through each resolved path and add it to the list of entries + foreach (var resolvedPath in resolvedPaths) + { + //Get the prefix + string prefix = System.IO.Path.GetDirectoryName(resolvedPath) ?? String.Empty; + + AddDescendentEntriesIfPathIsFolder(path: resolvedPath, prefix: prefix, entries: entries); + + //Add an entry for the item + entries.Add(new ArchiveEntry(name: GetEntryName(path: resolvedPath, prefix: prefix), fullPath: resolvedPath)); + } } - return null; + //Throw an invalid path error + if (nonfilesystemPaths.Count > 0) ThrowInvalidPathError(nonfilesystemPaths); + + //Check for duplicate paths + var duplicates = GetDuplicatePaths(entries); + if (duplicates.Count() > 0) ThrowDuplicatePathsError(duplicates); + + return entries; } - private List? GetArchiveEntriesForLiteralPath(string path) + private List GetArchiveEntriesForLiteralPath(string[] paths) { - //Get the unresolved path - string unresolvedPath = _cmdlet.GetUnresolvedProviderPathFromPSPath(path); + List entries = new List(); - //Check if it exists - if (System.IO.Directory.Exists(unresolvedPath)) + foreach (var path in paths) + { + //Get the unresolved path + string unresolvedPath = _cmdlet.GetUnresolvedProviderPathFromPSPath(path); + + //Get the prefix of the path + string prefix = System.IO.Path.GetDirectoryName(unresolvedPath) ?? String.Empty; + + // If unresolvedPath is not a file or folder, throw a path not found error + // If it is a folder, add its descendents to the list of ArchiveEntry + if (!AddDescendentEntriesIfPathIsFolder(path: unresolvedPath, prefix: prefix, entries: entries) && !System.IO.File.Exists(unresolvedPath)) ThrowPathNotFoundError(path); + + //Add an entry for the item + entries.Add(new ArchiveEntry(name: GetEntryName(path: unresolvedPath, prefix: prefix), fullPath: unresolvedPath)); + } + + //Check for duplicate paths + var duplicates = GetDuplicatePaths(entries); + if (duplicates.Count() > 0) ThrowDuplicatePathsError(duplicates); + + //Return archive entries + return entries; + } + + private bool AddDescendentEntriesIfPathIsFolder(string path, string prefix, List entries) + { + if (System.IO.Directory.Exists(path)) { //Get all descendents - var resolvedPaths = _cmdlet.InvokeProvider.ChildItem.GetNames(new string[] { path }, returnContainers: ReturnContainers.ReturnAllContainers, recurse: true, depth: uint.MaxValue, force: true, literalPath: false); - } else if (!System.IO.File.Exists(unresolvedPath)) + var childPaths = _cmdlet.InvokeProvider.ChildItem.GetNames(new string[] { path }, returnContainers: ReturnContainers.ReturnAllContainers, + recurse: true, depth: uint.MaxValue, force: true, literalPath: false); + + foreach (var childPath in childPaths) + { + //Add an entry for each child path + entries.Add(new ArchiveEntry(name: GetEntryName(path: childPath, prefix: prefix), fullPath: childPath)); + } + + return true; + } + return false; + } + + private string GetEntryName(string path, string prefix) + { + if (prefix == String.Empty) return path; + + //If the path does not start with the prefix, throw an exception + if (!path.StartsWith(prefix)) { - //Throw an error + throw new ArgumentException($"{path} does not begin with {prefix}"); } + string entryName = path.Substring(path.Length - prefix.Length + 1); - //Return archive entries - return null; + //Normalize entryName to use forwardslashes instead of backslashes + entryName.Replace(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar); + + //If the path is a folder, ensure entryName has a forwardslash at the end + if (System.IO.Directory.Exists(path) && !path.EndsWith(System.IO.Path.AltDirectorySeparatorChar)) path += System.IO.Path.AltDirectorySeparatorChar; + + return entryName; + } + + private IEnumerable GetDuplicatePaths(List entries) + { + return entries.GroupBy(x => x.FullPath) + .Where(group => group.Count() > 1) + .Select(x => x.Key); + } + + private void ThrowPathNotFoundError(string path) + { + var errorMsg = String.Format(ErrorMessages.PathNotFoundMessage, path); + var exception = new System.InvalidOperationException(errorMsg); + var errorRecord = new ErrorRecord(exception, "PathNotFound", ErrorCategory.InvalidArgument, path); + _cmdlet.ThrowTerminatingError(errorRecord); + } + + private void ThrowInvalidPathError(HashSet paths) + { + string commaSeperatedPaths = String.Join(',', paths); + var errorMsg = String.Format(ErrorMessages.InvalidPathMessage, commaSeperatedPaths); + var exception = new System.InvalidOperationException(errorMsg); + var errorRecord = new ErrorRecord(exception, "InvalidPath", ErrorCategory.InvalidArgument, commaSeperatedPaths); + _cmdlet.ThrowTerminatingError(errorRecord); + } + + private void ThrowDuplicatePathsError(IEnumerable paths) + { + string commaSeperatedPaths = String.Join(',', paths); + var errorMsg = String.Format(ErrorMessages.DuplicatePathsMessage, commaSeperatedPaths); + var exception = new System.InvalidOperationException(errorMsg); + var errorRecord = new ErrorRecord(exception, "DuplicatePath", ErrorCategory.InvalidArgument, commaSeperatedPaths); + _cmdlet.ThrowTerminatingError(errorRecord); } } } From 04adc0b468cc7764aff50d4ba5db232a3fdc7509 Mon Sep 17 00:00:00 2001 From: Abdullah Yousuf Date: Tue, 12 Jul 2022 15:33:59 -0700 Subject: [PATCH 05/42] resolved bug where GetEntryName was throwing an ArgumentException --- Tests/Compress-Archive.Tests.ps1 | 2 +- src/CompressArchiveCommand.cs | 79 +++------------------ src/PathHelper.cs | 115 +++++++++++++++++++++++++++---- 3 files changed, 113 insertions(+), 83 deletions(-) diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index 8fe5d3d..1dbea87 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -173,7 +173,7 @@ } It "Validate errors from Compress-Archive when invalid path (non-existing path / non-filesystem path) is supplied for Path or LiteralPath parameters" { - CompressArchiveInvalidPathValidator "$TestDrive$($DS)InvalidPath" $TestDrive "$TestDrive$($DS)InvalidPath" "PathNotFound,Microsoft.PowerShell.Archive.CompressArchiveCommand" + #CompressArchiveInvalidPathValidator "$TestDrive$($DS)InvalidPath" $TestDrive "$TestDrive$($DS)InvalidPath" "PathNotFound,Microsoft.PowerShell.Archive.CompressArchiveCommand" $path = @("$TestDrive", "$TestDrive$($DS)InvalidPath") CompressArchiveInvalidPathValidator $path $TestDrive "$TestDrive$($DS)InvalidPath" "PathNotFound,Microsoft.PowerShell.Archive.CompressArchiveCommand" diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index 4fb7bb8..efd6f56 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -41,14 +41,11 @@ public class CompressArchiveCommand : PSCmdlet [ValidateNotNullOrEmpty] public System.IO.Compression.CompressionLevel CompressionLevel { get; set; } - private HashSet _sourcePaths; - - private HashSet _duplicatePaths; + private List _sourcePaths; public CompressArchiveCommand() { - _sourcePaths = new HashSet(); - _duplicatePaths = new HashSet(); + _sourcePaths = new List(); } protected override void BeginProcessing() @@ -59,39 +56,18 @@ protected override void BeginProcessing() protected override void ProcessRecord() { - //Validate paths - string[]? paths; - paths = ParameterSetName.StartsWith("Path") ? ResolvePathWithWildcards(Path) : ResolvePathWithoutWildcards(LiteralPath); - - PathHelper pathHelper = new PathHelper(this); - - foreach (var path in paths) - { - - pathHelper.GetEntryRecordsForPath(path, false); - - //Add path to source paths - if (!_sourcePaths.Add(path)) - { - //If the set already contains the path, add it to the set of duplicates - _duplicatePaths.Add(path); - } - } - - + if (ParameterSetName.StartsWith("Path")) _sourcePaths.AddRange(Path); + else _sourcePaths.AddRange(LiteralPath); } protected override void EndProcessing() { - //If there are duplicate paths, throw an error - if (_duplicatePaths.Count > 0) - { - var errorMsg = String.Format(ErrorMessages.DuplicatePathsMessage, _duplicatePaths.ToString()); - var exception = new System.ArgumentException(errorMsg); - ErrorRecord errorRecord = new ErrorRecord(exception, "DuplicatePathFound", ErrorCategory.InvalidArgument, _duplicatePaths); - ThrowTerminatingError(errorRecord); + PathHelper pathHelper = new PathHelper(this); - } + //Get archive entries, validation is performed by PathHelper + List archiveEntries = pathHelper.GetEntryRecordsForPath(_sourcePaths.ToArray(), ParameterSetName.StartsWith("LiteralPath")); + + // } protected override void StopProcessing() @@ -99,43 +75,6 @@ protected override void StopProcessing() base.StopProcessing(); } - private string[] ResolvePathWithWildcards(string[] paths) - { - List outputPaths = new List(); - foreach (var path in paths) - { - var resolvedPaths = GetResolvedProviderPathFromPSPath(path, out var providerInfo); - if (providerInfo.Name != "FileSystem") - { - //Throw an error - } - outputPaths.AddRange(resolvedPaths); - } - - return outputPaths.ToArray(); - } - - private string[] ResolvePathWithoutWildcards(string[] paths) - { - string[] outputPaths = new string[paths.Length]; - for (int i=0; i GetArchiveEntriesForNonLiteralPaths(string[] paths) foreach (var path in paths) { //Resolve the path - var resolvedPaths = _cmdlet.GetResolvedProviderPathFromPSPath(path, out var providerInfo); + var resolvedPaths = GetResolvedProviderPathFromPSPath(path, out var providerInfo); //Check if the path if from the filesystem - if (providerInfo.Name != "FileSystem") + if (providerInfo?.Name != "FileSystem") { //Add the path to the set of non-filesystem paths nonfilesystemPaths.Add(path); @@ -74,7 +75,7 @@ private List GetArchiveEntriesForLiteralPath(string[] paths) foreach (var path in paths) { //Get the unresolved path - string unresolvedPath = _cmdlet.GetUnresolvedProviderPathFromPSPath(path); + string unresolvedPath = GetUnresolvedProviderPathFromPSPath(path); //Get the prefix of the path string prefix = System.IO.Path.GetDirectoryName(unresolvedPath) ?? String.Empty; @@ -99,17 +100,23 @@ private bool AddDescendentEntriesIfPathIsFolder(string path, string prefix, List { if (System.IO.Directory.Exists(path)) { - //Get all descendents - var childPaths = _cmdlet.InvokeProvider.ChildItem.GetNames(new string[] { path }, returnContainers: ReturnContainers.ReturnAllContainers, - recurse: true, depth: uint.MaxValue, force: true, literalPath: false); - - foreach (var childPath in childPaths) + try + { + var directoryInfo = new System.IO.DirectoryInfo(path); + foreach (var childPath in directoryInfo.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)) + { + //Add an entry for each child path + entries.Add(new ArchiveEntry(name: GetEntryName(path: childPath.FullName, prefix: prefix), fullPath: childPath.Name)); + } + + return true; + } catch (System.Management.Automation.ItemNotFoundException itemNotFoundException) { - //Add an entry for each child path - entries.Add(new ArchiveEntry(name: GetEntryName(path: childPath, prefix: prefix), fullPath: childPath)); + //Throw a path not found error + ErrorRecord errorRecord = new ErrorRecord(itemNotFoundException, "PathNotFound", ErrorCategory.InvalidArgument, path); + _cmdlet.ThrowTerminatingError(errorRecord); } - - return true; + } return false; } @@ -142,6 +149,74 @@ private IEnumerable GetDuplicatePaths(List entries) .Select(x => x.Key); } + private System.Collections.ObjectModel.Collection? GetResolvedProviderPathFromPSPath(string path, out ProviderInfo? providerInfo) + { + try + { + ProviderInfo info; + var resolvedPaths = _cmdlet.GetResolvedProviderPathFromPSPath(path, out info); + providerInfo = info; + return resolvedPaths; + } + catch (ProviderNotFoundException providerNotFoundException) + { + //Throw an invalid path error + ThrowInvalidPathError(path, providerNotFoundException); + } + catch (System.Management.Automation.DriveNotFoundException driveNotFoundException) + { + ThrowInvalidPathError(path, driveNotFoundException); + } + catch (System.Management.Automation.ProviderInvocationException providerInvocationException) + { + ThrowInvalidPathError(path, providerInvocationException); + } + catch (NotSupportedException providerNotSupportedException) + { + ThrowInvalidPathError(path, providerNotSupportedException); + } + catch (InvalidOperationException invalidOperationException) + { + ThrowInvalidPathError(path, invalidOperationException); + } + catch (ItemNotFoundException itemNotFoundException) + { + ThrowPathNotFoundError(path, itemNotFoundException); + } + providerInfo = null; + return null; + } + + private string? GetUnresolvedProviderPathFromPSPath(string path) + { + try + { + return _cmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath(path); + } + catch (ProviderNotFoundException providerNotFoundException) + { + //Throw an invalid path error + ThrowInvalidPathError(path, providerNotFoundException); + } + catch (System.Management.Automation.DriveNotFoundException driveNotFoundException) + { + ThrowInvalidPathError(path, driveNotFoundException); + } + catch (System.Management.Automation.ProviderInvocationException providerInvocationException) + { + ThrowInvalidPathError(path, providerInvocationException); + } + catch (NotSupportedException providerNotSupportedException) + { + ThrowInvalidPathError(path, providerNotSupportedException); + } + catch (InvalidOperationException invalidOperationException) + { + ThrowInvalidPathError(path, invalidOperationException); + } + return null; + } + private void ThrowPathNotFoundError(string path) { var errorMsg = String.Format(ErrorMessages.PathNotFoundMessage, path); @@ -150,6 +225,14 @@ private void ThrowPathNotFoundError(string path) _cmdlet.ThrowTerminatingError(errorRecord); } + private void ThrowPathNotFoundError(string path, Exception innerException) + { + var errorMsg = String.Format(ErrorMessages.PathNotFoundMessage, path); + var exception = new System.ArgumentException(errorMsg); + var errorRecord = new ErrorRecord(exception, "PathNotFound", ErrorCategory.InvalidArgument, path); + _cmdlet.ThrowTerminatingError(errorRecord); + } + private void ThrowInvalidPathError(HashSet paths) { string commaSeperatedPaths = String.Join(',', paths); @@ -159,6 +242,14 @@ private void ThrowInvalidPathError(HashSet paths) _cmdlet.ThrowTerminatingError(errorRecord); } + private void ThrowInvalidPathError(string path, Exception innerException) + { + var errorMsg = String.Format(ErrorMessages.InvalidPathMessage, path); + var exception = new System.ArgumentException(errorMsg, innerException); + var errorRecord = new ErrorRecord(exception, "InvalidPath", ErrorCategory.InvalidArgument, path); + _cmdlet.ThrowTerminatingError(errorRecord); + } + private void ThrowDuplicatePathsError(IEnumerable paths) { string commaSeperatedPaths = String.Join(',', paths); From 915f34467ff2f4a2a39d9442a6d61c1ffa29287b Mon Sep 17 00:00:00 2001 From: Abdullah Yousuf Date: Tue, 12 Jul 2022 17:34:06 -0700 Subject: [PATCH 06/42] fixed a bug, started work on zip support --- Tests/Compress-Archive.Tests.ps1 | 20 ++++---- src/ArchiveMode.cs | 13 ++++++ src/CompressArchiveCommand.cs | 49 ++------------------ src/ErrorMessages.cs | 2 + src/IArchive.cs | 28 ++++++++++++ src/PathHelper.cs | 47 +++++++++++++++++-- src/ZipArchive.cs | 78 ++++++++++++++++++++++++++++++++ 7 files changed, 177 insertions(+), 60 deletions(-) create mode 100644 src/ArchiveMode.cs create mode 100644 src/IArchive.cs create mode 100644 src/ZipArchive.cs diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index 1dbea87..12970b1 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -173,17 +173,13 @@ } It "Validate errors from Compress-Archive when invalid path (non-existing path / non-filesystem path) is supplied for Path or LiteralPath parameters" { - #CompressArchiveInvalidPathValidator "$TestDrive$($DS)InvalidPath" $TestDrive "$TestDrive$($DS)InvalidPath" "PathNotFound,Microsoft.PowerShell.Archive.CompressArchiveCommand" + CompressArchiveInvalidPathValidator "$TestDrive$($DS)InvalidPath" $TestDrive "$TestDrive$($DS)InvalidPath" "PathNotFound,Microsoft.PowerShell.Archive.CompressArchiveCommand" $path = @("$TestDrive", "$TestDrive$($DS)InvalidPath") CompressArchiveInvalidPathValidator $path $TestDrive "$TestDrive$($DS)InvalidPath" "PathNotFound,Microsoft.PowerShell.Archive.CompressArchiveCommand" } - It "Validate error when non-existant path is supplied to DestinationPath parameter" { - CompressArchiveInvalidPathValidator "$TestDrive" "$TestDrive$($DS)NonExistingDirectory$($DS)sample.zip" "$TestDrive$($DS)NonExistingDirectory$($DS)sample.zip" "PathNotFound,Microsoft.PowerShell.Archive.CompressArchiveCommand" - } - - It "Validate error from Compress-Archive when duplicate paths are supplied as input to Path parameter" -Tag this { + It "Validate error from Compress-Archive when duplicate paths are supplied as input to Path parameter" { $sourcePath = @( "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt", "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt") @@ -200,7 +196,7 @@ } } - It "Validate error from Compress-Archive when duplicate paths are supplied as input to LiteralPath parameter" { + It "Validate error from Compress-Archive when duplicate paths are supplied as input to LiteralPath parameter" -tag th { $sourcePath = @( "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt", "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt") @@ -226,7 +222,7 @@ try { Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - Test-Path $destinationPath | Should Be $true + Test-Path $destinationPath | Should -Be $true } finally { @@ -256,11 +252,11 @@ "Some Text" > $TestDrive$($DS)Sample.unzip "Some Text" > $TestDrive$($DS)Sample.cab - $preCreatedArchivePath = Join-Path $PSScriptRoot "SamplePreCreatedArchive.archive" - Copy-Item $preCreatedArchivePath $TestDrive$($DS)SamplePreCreatedArchive.zip -Force + #$preCreatedArchivePath = Join-Path $PSScriptRoot "SamplePreCreatedArchive.archive" + #Copy-Item $preCreatedArchivePath $TestDrive$($DS)SamplePreCreatedArchive.zip -Force - $preCreatedArchivePath = Join-Path $PSScriptRoot "TrailingSpacer.archive" - Copy-Item $preCreatedArchivePath $TestDrive$($DS)TrailingSpacer.zip -Force + #$preCreatedArchivePath = Join-Path $PSScriptRoot "TrailingSpacer.archive" + #Copy-Item $preCreatedArchivePath $TestDrive$($DS)TrailingSpacer.zip -Force } diff --git a/src/ArchiveMode.cs b/src/ArchiveMode.cs new file mode 100644 index 0000000..126a30d --- /dev/null +++ b/src/ArchiveMode.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.PowerShell.Archive +{ + internal enum ArchiveMode + { + Create, + Update, + Read + } +} diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index efd6f56..43f5f35 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -43,15 +43,18 @@ public class CompressArchiveCommand : PSCmdlet private List _sourcePaths; + private PathHelper _pathHelper; + public CompressArchiveCommand() { _sourcePaths = new List(); + _pathHelper = new PathHelper(this); } protected override void BeginProcessing() { base.BeginProcessing(); - ResolvePath(DestinationPath); + DestinationPath = _pathHelper.ResolveToSingleFullyQualifiedPath(DestinationPath); } protected override void ProcessRecord() @@ -62,10 +65,8 @@ protected override void ProcessRecord() protected override void EndProcessing() { - PathHelper pathHelper = new PathHelper(this); - //Get archive entries, validation is performed by PathHelper - List archiveEntries = pathHelper.GetEntryRecordsForPath(_sourcePaths.ToArray(), ParameterSetName.StartsWith("LiteralPath")); + List archiveEntries = _pathHelper.GetEntryRecordsForPath(_sourcePaths.ToArray(), ParameterSetName.StartsWith("LiteralPath")); // } @@ -74,45 +75,5 @@ protected override void StopProcessing() { base.StopProcessing(); } - - private string ResolvePath(string path) - { - //Get unresolved path - var unresolvedPath = GetUnresolvedProviderPathFromPSPath(path); - - //Get resolved path - try - { - var resolvedPath = GetResolvedProviderPathFromPSPath(path, out var providerInfo); - - if (resolvedPath.Count > 1 || providerInfo.Name != "FileSystem") - { - //Throw an error: duplicate paths - } - - if (resolvedPath.Count == 1 && resolvedPath[0] != unresolvedPath) - { - //Throw an error: duplicate paths - } - - if (resolvedPath.Count == 1 && resolvedPath[0] == unresolvedPath) return unresolvedPath; - - } catch (Exception ex) - { - - } - - ////If unresolvedPath doesn't exist, throw an error - //if (!System.IO.File.Exists(unresolvedPath)) - //{ - // //Throw an error: path not found - // var errorMsg = String.Format(ErrorMessages.PathNotFoundMessage, path); - // var exception = new System.InvalidOperationException(errorMsg); - // var errorRecord = new ErrorRecord(exception, "PathNotFound", ErrorCategory.InvalidArgument, path); - // ThrowTerminatingError(errorRecord); - //} - - return null; - } } } diff --git a/src/ErrorMessages.cs b/src/ErrorMessages.cs index aee5849..dcf7c29 100644 --- a/src/ErrorMessages.cs +++ b/src/ErrorMessages.cs @@ -12,5 +12,7 @@ internal static class ErrorMessages internal static string DuplicatePathsMessage = "The path(s) {0} have been specified more than once."; internal static string InvalidPathMessage = "The path(s) {0} are invalid."; + + internal static string ResolvesToMultiplePathsMessage = "The path {0} resolves to multiple possible paths."; } } diff --git a/src/IArchive.cs b/src/IArchive.cs new file mode 100644 index 0000000..2bd55f1 --- /dev/null +++ b/src/IArchive.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.PowerShell.Archive +{ + internal interface IArchive: IDisposable + { + // Get what mode the archive is in + internal ArchiveMode Mode { get; } + + // Get the fully qualified path of the archive + internal string ArchivePath { get; } + + // Add a file or folder to the archive. The entry name of the added item in the + // will be ArchiveEntry.Name. + // Throws an exception if the archive is in read mode. + internal void AddFilesytemEntry(ArchiveEntry entry); + + // Get the entries in the archive. + // Throws an exception if the archive is in create mode. + internal string[] GetEntries(); + + // Expands an archive to a destination folder. + // Throws an exception if the archive is not in read mode. + internal void Expand(string destinationPath); + } +} diff --git a/src/PathHelper.cs b/src/PathHelper.cs index fef6bbb..d321429 100644 --- a/src/PathHelper.cs +++ b/src/PathHelper.cs @@ -35,7 +35,7 @@ private List GetArchiveEntriesForNonLiteralPaths(string[] paths) foreach (var path in paths) { //Resolve the path - var resolvedPaths = GetResolvedProviderPathFromPSPath(path, out var providerInfo); + var resolvedPaths = GetResolvedProviderPathFromPSPath(path, out var providerInfo, mustExist: true); //Check if the path if from the filesystem if (providerInfo?.Name != "FileSystem") @@ -149,7 +149,7 @@ private IEnumerable GetDuplicatePaths(List entries) .Select(x => x.Key); } - private System.Collections.ObjectModel.Collection? GetResolvedProviderPathFromPSPath(string path, out ProviderInfo? providerInfo) + private System.Collections.ObjectModel.Collection? GetResolvedProviderPathFromPSPath(string path, out ProviderInfo? providerInfo, bool mustExist) { try { @@ -181,7 +181,7 @@ private IEnumerable GetDuplicatePaths(List entries) } catch (ItemNotFoundException itemNotFoundException) { - ThrowPathNotFoundError(path, itemNotFoundException); + if (mustExist) ThrowPathNotFoundError(path, itemNotFoundException); } providerInfo = null; return null; @@ -217,6 +217,29 @@ private IEnumerable GetDuplicatePaths(List entries) return null; } + internal string ResolveToSingleFullyQualifiedPath(string path) + { + //path be literal or non-literal + //First, get non-literal path + string nonLiteralPath = GetUnresolvedProviderPathFromPSPath(path) ?? throw new ArgumentException($"Path {path} was resolved to null"); + + //Second, get literal path + var literalPaths = GetResolvedProviderPathFromPSPath(path, out var providerInfo, mustExist: false); + if (literalPaths != null) + { + //Ensure the literal paths came from the filesystem + if (providerInfo != null && providerInfo?.Name != "FileSystem") ThrowInvalidPathError(path); + + //If there are >1 literalPaths, throw an error + if (literalPaths.Count > 1) ThrowResolvesToMultiplePathsError(path); + + //If there is one item in literalPaths, compare it to nonLiteralPath + if (literalPaths[0] != nonLiteralPath) ThrowResolvesToMultiplePathsError(path); + } + + return nonLiteralPath; + } + private void ThrowPathNotFoundError(string path) { var errorMsg = String.Format(ErrorMessages.PathNotFoundMessage, path); @@ -242,6 +265,14 @@ private void ThrowInvalidPathError(HashSet paths) _cmdlet.ThrowTerminatingError(errorRecord); } + private void ThrowInvalidPathError(string path) + { + var errorMsg = String.Format(ErrorMessages.InvalidPathMessage, path); + var exception = new System.ArgumentException(errorMsg); + var errorRecord = new ErrorRecord(exception, "InvalidPath", ErrorCategory.InvalidArgument, path); + _cmdlet.ThrowTerminatingError(errorRecord); + } + private void ThrowInvalidPathError(string path, Exception innerException) { var errorMsg = String.Format(ErrorMessages.InvalidPathMessage, path); @@ -255,7 +286,15 @@ private void ThrowDuplicatePathsError(IEnumerable paths) string commaSeperatedPaths = String.Join(',', paths); var errorMsg = String.Format(ErrorMessages.DuplicatePathsMessage, commaSeperatedPaths); var exception = new System.InvalidOperationException(errorMsg); - var errorRecord = new ErrorRecord(exception, "DuplicatePath", ErrorCategory.InvalidArgument, commaSeperatedPaths); + var errorRecord = new ErrorRecord(exception, "DuplicatePathFound", ErrorCategory.InvalidArgument, commaSeperatedPaths); + _cmdlet.ThrowTerminatingError(errorRecord); + } + + private void ThrowResolvesToMultiplePathsError(string path) + { + var errorMsg = String.Format(ErrorMessages.ResolvesToMultiplePathsMessage, path); + var exception = new System.ArgumentException(errorMsg); + var errorRecord = new ErrorRecord(exception, "DuplicatePathFound", ErrorCategory.InvalidArgument, path); _cmdlet.ThrowTerminatingError(errorRecord); } } diff --git a/src/ZipArchive.cs b/src/ZipArchive.cs new file mode 100644 index 0000000..29f7022 --- /dev/null +++ b/src/ZipArchive.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.PowerShell.Archive +{ + internal class ZipArchive : IArchive + { + private bool disposedValue; + + private ArchiveMode _mode; + + private string _archivePath; + + private System.IO.Compression.ZipArchive _zipArchive; + + ArchiveMode Mode => _mode; + + string ArchivePath => _archivePath; + + public ZipArchive(string archivePath, ArchiveMode mode, System.IO.FileStream _archiveStream) + { + disposedValue = false; + _mode = mode; + _archivePath = archivePath; + _zipArchive = new System.IO.Compression.ZipArchive(_archiveStream, ) + } + + void AddFilesytemEntry(ArchiveEntry entry) + { + throw new NotImplementedException(); + } + + string[] GetEntries() + { + throw new NotImplementedException(); + } + + void Expand(string destinationPath) + { + throw new NotImplementedException(); + } + + private System.IO.Compression.ZipArchiveMode GetZipArchiveMode(ArchiveMode archiveMode) + { + + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~ZipArchive() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} From c05e063caf6c7d69be12514d863a58b9076d426c Mon Sep 17 00:00:00 2001 From: Abdullah Yousuf Date: Wed, 13 Jul 2022 13:15:45 -0700 Subject: [PATCH 07/42] worked on zip archive support --- src/ArchiveFormat.cs | 11 +++++ src/CompressArchiveCommand.cs | 15 ++++++- src/PathHelper.cs | 81 ++++++++++++++++++++++++----------- src/ZipArchive.cs | 53 ++++++++++++++++++----- 4 files changed, 123 insertions(+), 37 deletions(-) create mode 100644 src/ArchiveFormat.cs diff --git a/src/ArchiveFormat.cs b/src/ArchiveFormat.cs new file mode 100644 index 0000000..df05bea --- /dev/null +++ b/src/ArchiveFormat.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.PowerShell.Archive +{ + internal enum ArchiveFormat + { + zip, + } +} diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index 43f5f35..d12c38d 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -54,13 +54,18 @@ public CompressArchiveCommand() protected override void BeginProcessing() { base.BeginProcessing(); + // TODO: Add exception handling DestinationPath = _pathHelper.ResolveToSingleFullyQualifiedPath(DestinationPath); + + // TODO: If we are in update mode, check if archive exists + // TODO: If we are not in update mode, check if archive does not exist or Overwrite is true and the archive is not read-only } protected override void ProcessRecord() { if (ParameterSetName.StartsWith("Path")) _sourcePaths.AddRange(Path); else _sourcePaths.AddRange(LiteralPath); + } protected override void EndProcessing() @@ -68,7 +73,15 @@ protected override void EndProcessing() //Get archive entries, validation is performed by PathHelper List archiveEntries = _pathHelper.GetEntryRecordsForPath(_sourcePaths.ToArray(), ParameterSetName.StartsWith("LiteralPath")); - // + //Create a zip archive + using (var archive = ArchiveFactory.GetArchive(ArchiveFormat.zip, DestinationPath, Update ? ArchiveMode.Update : ArchiveMode.Create, CompressionLevel)) + { + //Add entries to the archive + foreach (ArchiveEntry entry in archiveEntries) + { + archive.AddFilesytemEntry(entry); + } + } } protected override void StopProcessing() diff --git a/src/PathHelper.cs b/src/PathHelper.cs index d321429..8d629e3 100644 --- a/src/PathHelper.cs +++ b/src/PathHelper.cs @@ -45,13 +45,26 @@ private List GetArchiveEntriesForNonLiteralPaths(string[] paths) continue; } + //Check if the entered path is relative to the current working directory + bool isPathRelativeToWorkingDirectory = CanPreservePathStructure(path); + //Go through each resolved path and add it to the list of entries - foreach (var resolvedPath in resolvedPaths) + for (int i=0; i GetArchiveEntriesForLiteralPath(string[] paths) //Get the unresolved path string unresolvedPath = GetUnresolvedProviderPathFromPSPath(path); + // TODO: Factor out this part -- adding an entry + //Get the prefix of the path string prefix = System.IO.Path.GetDirectoryName(unresolvedPath) ?? String.Empty; // If unresolvedPath is not a file or folder, throw a path not found error // If it is a folder, add its descendents to the list of ArchiveEntry - if (!AddDescendentEntriesIfPathIsFolder(path: unresolvedPath, prefix: prefix, entries: entries) && !System.IO.File.Exists(unresolvedPath)) ThrowPathNotFoundError(path); + if (System.IO.Directory.Exists(unresolvedPath)) + { + //Add directory seperator to end + if (!unresolvedPath.EndsWith(System.IO.Path.DirectorySeparatorChar)) unresolvedPath += System.IO.Path.DirectorySeparatorChar; + AddDescendentEntries(path: unresolvedPath, prefix: prefix, entries: entries); + } else if (!System.IO.File.Exists(unresolvedPath)) + { + ThrowPathNotFoundError(path); + } //Add an entry for the item entries.Add(new ArchiveEntry(name: GetEntryName(path: unresolvedPath, prefix: prefix), fullPath: unresolvedPath)); @@ -96,29 +119,23 @@ private List GetArchiveEntriesForLiteralPath(string[] paths) return entries; } - private bool AddDescendentEntriesIfPathIsFolder(string path, string prefix, List entries) + private void AddDescendentEntries(string path, string prefix, List entries) { - if (System.IO.Directory.Exists(path)) + try { - try - { - var directoryInfo = new System.IO.DirectoryInfo(path); - foreach (var childPath in directoryInfo.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)) - { - //Add an entry for each child path - entries.Add(new ArchiveEntry(name: GetEntryName(path: childPath.FullName, prefix: prefix), fullPath: childPath.Name)); - } - - return true; - } catch (System.Management.Automation.ItemNotFoundException itemNotFoundException) + var directoryInfo = new System.IO.DirectoryInfo(path); + foreach (var childPath in directoryInfo.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)) { - //Throw a path not found error - ErrorRecord errorRecord = new ErrorRecord(itemNotFoundException, "PathNotFound", ErrorCategory.InvalidArgument, path); - _cmdlet.ThrowTerminatingError(errorRecord); + //Add an entry for each child path + entries.Add(new ArchiveEntry(name: GetEntryName(path: childPath.FullName, prefix: prefix), fullPath: childPath.Name)); } - } - return false; + catch (System.Management.Automation.ItemNotFoundException itemNotFoundException) + { + //Throw a path not found error + ErrorRecord errorRecord = new ErrorRecord(itemNotFoundException, "PathNotFound", ErrorCategory.InvalidArgument, path); + _cmdlet.ThrowTerminatingError(errorRecord); + } } private string GetEntryName(string path, string prefix) @@ -131,13 +148,12 @@ private string GetEntryName(string path, string prefix) throw new ArgumentException($"{path} does not begin with {prefix}"); } - string entryName = path.Substring(path.Length - prefix.Length + 1); + if (path.Length <= prefix.Length) throw new ArgumentException($"The length of {path} is shorter than or equal to the length of {prefix}"); - //Normalize entryName to use forwardslashes instead of backslashes - entryName.Replace(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar); + string entryName = path.Substring(prefix.Length + 1); - //If the path is a folder, ensure entryName has a forwardslash at the end - if (System.IO.Directory.Exists(path) && !path.EndsWith(System.IO.Path.AltDirectorySeparatorChar)) path += System.IO.Path.AltDirectorySeparatorChar; + //Normalize entryName to use forwardslashes instead of backslashes + entryName = entryName.Replace(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar); return entryName; } @@ -217,6 +233,7 @@ private IEnumerable GetDuplicatePaths(List entries) return null; } + // TODO: Add directory seperator char at end internal string ResolveToSingleFullyQualifiedPath(string path) { //path be literal or non-literal @@ -297,5 +314,17 @@ private void ThrowResolvesToMultiplePathsError(string path) var errorRecord = new ErrorRecord(exception, "DuplicatePathFound", ErrorCategory.InvalidArgument, path); _cmdlet.ThrowTerminatingError(errorRecord); } + + private bool CanPreservePathStructure(string path) + { + return System.IO.Path.IsPathRooted(path); + } + + private bool IsPathRelativeToCurrentWorkingDirectory(string path) + { + // TODO: Add exception handling + string relativePath = System.IO.Path.GetRelativePath(_cmdlet.SessionState.Path.CurrentFileSystemLocation.Path, path); + return !relativePath.Contains(".."); + } } } diff --git a/src/ZipArchive.cs b/src/ZipArchive.cs index 29f7022..c5d3572 100644 --- a/src/ZipArchive.cs +++ b/src/ZipArchive.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO.Compression; using System.Text; namespace Microsoft.PowerShell.Archive @@ -12,38 +13,68 @@ internal class ZipArchive : IArchive private string _archivePath; + private System.IO.FileStream _archiveStream; + private System.IO.Compression.ZipArchive _zipArchive; - ArchiveMode Mode => _mode; + private System.IO.Compression.CompressionLevel _compressionLevel; + + ArchiveMode IArchive.Mode => _mode; - string ArchivePath => _archivePath; + string IArchive.ArchivePath => _archivePath; - public ZipArchive(string archivePath, ArchiveMode mode, System.IO.FileStream _archiveStream) + public ZipArchive(string archivePath, ArchiveMode mode, System.IO.FileStream archiveStream, CompressionLevel compressionLevel) { disposedValue = false; _mode = mode; _archivePath = archivePath; - _zipArchive = new System.IO.Compression.ZipArchive(_archiveStream, ) + _archiveStream = archiveStream; + _zipArchive = new System.IO.Compression.ZipArchive(stream: archiveStream, mode: ConvertToZipArchiveMode(_mode), leaveOpen: true); + _compressionLevel = compressionLevel; } - void AddFilesytemEntry(ArchiveEntry entry) + void IArchive.AddFilesytemEntry(ArchiveEntry entry) { - throw new NotImplementedException(); + if (_mode == ArchiveMode.Read) throw new InvalidOperationException("Cannot add a filesystem entry to an archive in read mode"); + // TODO: Add exception handling for _zipArchive.GetEntry + var entryInArchive = (_mode == ArchiveMode.Create) ? null : _zipArchive.GetEntry(entry.Name); + if (entry.Name.EndsWith(System.IO.Path.AltDirectorySeparatorChar)) + { + //Create an entry only + // TODO: Add exception handling for CreateEntry + if (entryInArchive == null) _zipArchive.CreateEntry(entry.Name); + } + else + { + if (entryInArchive != null) + { + entryInArchive.Delete(); + } + // TODO: Add exception handling + _zipArchive.CreateEntryFromFile(sourceFileName: entry.FullPath, entryName: entry.Name, compressionLevel: _compressionLevel); + } + // TODO: Check what happens when we add a folder with children and then add a file } - string[] GetEntries() + string[] IArchive.GetEntries() { throw new NotImplementedException(); } - void Expand(string destinationPath) + void IArchive.Expand(string destinationPath) { throw new NotImplementedException(); } - private System.IO.Compression.ZipArchiveMode GetZipArchiveMode(ArchiveMode archiveMode) + private System.IO.Compression.ZipArchiveMode ConvertToZipArchiveMode(ArchiveMode archiveMode) { - + switch (archiveMode) + { + case ArchiveMode.Create: return System.IO.Compression.ZipArchiveMode.Create; + case ArchiveMode.Update: return System.IO.Compression.ZipArchiveMode.Update; + case ArchiveMode.Read: return System.IO.Compression.ZipArchiveMode.Read; + default: return System.IO.Compression.ZipArchiveMode.Update; + } } protected virtual void Dispose(bool disposing) @@ -53,6 +84,8 @@ protected virtual void Dispose(bool disposing) if (disposing) { // TODO: dispose managed state (managed objects) + _zipArchive.Dispose(); + _archiveStream.Dispose(); } // TODO: free unmanaged resources (unmanaged objects) and override finalizer From ef66ef9b76b3994f6607b5c0e4d8f85d577db2e7 Mon Sep 17 00:00:00 2001 From: Abdullah Yousuf Date: Wed, 13 Jul 2022 13:16:19 -0700 Subject: [PATCH 08/42] added ArchiveFactory class --- src/ArchiveFactory.cs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/ArchiveFactory.cs diff --git a/src/ArchiveFactory.cs b/src/ArchiveFactory.cs new file mode 100644 index 0000000..035df8c --- /dev/null +++ b/src/ArchiveFactory.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Net; +using System.Text; + +namespace Microsoft.PowerShell.Archive +{ + internal static class ArchiveFactory + { + internal static IArchive GetArchive(ArchiveFormat format, string archivePath, ArchiveMode archiveMode, System.IO.Compression.CompressionLevel compressionLevel) + { + System.IO.FileStream archiveFileStream = archiveMode switch + { + ArchiveMode.Create => new System.IO.FileStream(archivePath, mode: System.IO.FileMode.CreateNew, access: System.IO.FileAccess.Write, share: System.IO.FileShare.None), + ArchiveMode.Update => new System.IO.FileStream(archivePath, mode: System.IO.FileMode.Open, access: System.IO.FileAccess.ReadWrite, share: System.IO.FileShare.None), + ArchiveMode.Read => new System.IO.FileStream(archivePath, mode: System.IO.FileMode.Open, access: System.IO.FileAccess.Read, share: System.IO.FileShare.Read), + // TODO: Add message to exception + _ => throw new NotImplementedException() + }; + + return format switch + { + ArchiveFormat.zip => new ZipArchive(archivePath, archiveMode, archiveFileStream, compressionLevel), + // TODO: Add message to exception + _ => throw new NotImplementedException() + }; + } + } +} From dde4deb9924cf68fb303d2d292e3840d03af5745 Mon Sep 17 00:00:00 2001 From: Abdullah Yousuf Date: Wed, 13 Jul 2022 15:30:33 -0700 Subject: [PATCH 09/42] imporved zip support --- src/PathHelper.cs | 70 +++++++++++++++++++++++++++++++++-------------- src/ZipArchive.cs | 13 ++++++--- 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/src/PathHelper.cs b/src/PathHelper.cs index 8d629e3..29cb928 100644 --- a/src/PathHelper.cs +++ b/src/PathHelper.cs @@ -46,28 +46,16 @@ private List GetArchiveEntriesForNonLiteralPaths(string[] paths) } //Check if the entered path is relative to the current working directory - bool isPathRelativeToWorkingDirectory = CanPreservePathStructure(path); + bool shouldPreservePathStructure = CanPreservePathStructure(path); //Go through each resolved path and add it to the list of entries for (int i=0; i GetArchiveEntriesForLiteralPath(string[] paths) { //Add directory seperator to end if (!unresolvedPath.EndsWith(System.IO.Path.DirectorySeparatorChar)) unresolvedPath += System.IO.Path.DirectorySeparatorChar; - AddDescendentEntries(path: unresolvedPath, prefix: prefix, entries: entries); + AddDescendentEntries(path: unresolvedPath, entries: entries, shouldPreservePathStructure: true); } else if (!System.IO.File.Exists(unresolvedPath)) { ThrowPathNotFoundError(path); @@ -119,7 +107,26 @@ private List GetArchiveEntriesForLiteralPath(string[] paths) return entries; } - private void AddDescendentEntries(string path, string prefix, List entries) + private void AddEntryForFullyQualifiedPath(string path, List entries, bool shouldPreservePathStructure) + { + // If unresolvedPath is not a file or folder, throw a path not found error + // If it is a folder, add its descendents to the list of ArchiveEntry + if (System.IO.Directory.Exists(path)) + { + //Add directory seperator to end + if (!path.EndsWith(System.IO.Path.DirectorySeparatorChar)) path += System.IO.Path.DirectorySeparatorChar; + AddDescendentEntries(path: path, entries: entries, shouldPreservePathStructure: shouldPreservePathStructure); + } + else if (!System.IO.File.Exists(path)) + { + ThrowPathNotFoundError(path); + } + + //Add an entry for the item + entries.Add(new ArchiveEntry(name: GetEntryName(path: path, shouldPreservePathStructure: shouldPreservePathStructure), fullPath: path)); + } + + private void AddDescendentEntries(string path, List entries, bool shouldPreservePathStructure) { try { @@ -127,7 +134,7 @@ private void AddDescendentEntries(string path, string prefix, List foreach (var childPath in directoryInfo.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)) { //Add an entry for each child path - entries.Add(new ArchiveEntry(name: GetEntryName(path: childPath.FullName, prefix: prefix), fullPath: childPath.Name)); + entries.Add(new ArchiveEntry(name: GetEntryName(path: childPath.FullName, shouldPreservePathStructure: shouldPreservePathStructure), fullPath: childPath.Name)); } } catch (System.Management.Automation.ItemNotFoundException itemNotFoundException) @@ -138,6 +145,27 @@ private void AddDescendentEntries(string path, string prefix, List } } + private string GetEntryName(string path, bool shouldPreservePathStructure) + { + //If the path is relative to the current working directory, return the relative path as name + if (shouldPreservePathStructure && IsPathRelativeToCurrentWorkingDirectory(path, out var relativePath)) + { + return relativePath; + } + //Otherwise, return the name of the directory or file + if (path.EndsWith(System.IO.Path.DirectorySeparatorChar)) + { + //Get substring from second-last directory seperator char till end + int secondLastIndex = path.LastIndexOf(System.IO.Path.DirectorySeparatorChar, path.Length - 2); + if (secondLastIndex == -1) return path; + else return path.Substring(secondLastIndex + 1); + } + else + { + return System.IO.Path.GetFileName(path); + } + } + private string GetEntryName(string path, string prefix) { if (prefix == String.Empty) return path; @@ -320,10 +348,10 @@ private bool CanPreservePathStructure(string path) return System.IO.Path.IsPathRooted(path); } - private bool IsPathRelativeToCurrentWorkingDirectory(string path) + private bool IsPathRelativeToCurrentWorkingDirectory(string path, out string relativePath) { // TODO: Add exception handling - string relativePath = System.IO.Path.GetRelativePath(_cmdlet.SessionState.Path.CurrentFileSystemLocation.Path, path); + relativePath = System.IO.Path.GetRelativePath(_cmdlet.SessionState.Path.CurrentFileSystemLocation.Path, path); return !relativePath.Contains(".."); } } diff --git a/src/ZipArchive.cs b/src/ZipArchive.cs index c5d3572..578876c 100644 --- a/src/ZipArchive.cs +++ b/src/ZipArchive.cs @@ -36,13 +36,16 @@ public ZipArchive(string archivePath, ArchiveMode mode, System.IO.FileStream arc void IArchive.AddFilesytemEntry(ArchiveEntry entry) { if (_mode == ArchiveMode.Read) throw new InvalidOperationException("Cannot add a filesystem entry to an archive in read mode"); + + var entryName = entry.Name.Replace(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar); + // TODO: Add exception handling for _zipArchive.GetEntry - var entryInArchive = (_mode == ArchiveMode.Create) ? null : _zipArchive.GetEntry(entry.Name); - if (entry.Name.EndsWith(System.IO.Path.AltDirectorySeparatorChar)) + var entryInArchive = (_mode == ArchiveMode.Create) ? null : _zipArchive.GetEntry(entryName); + if (entryName.EndsWith(System.IO.Path.AltDirectorySeparatorChar)) { //Create an entry only // TODO: Add exception handling for CreateEntry - if (entryInArchive == null) _zipArchive.CreateEntry(entry.Name); + if (entryInArchive == null) _zipArchive.CreateEntry(entryName); } else { @@ -51,9 +54,11 @@ void IArchive.AddFilesytemEntry(ArchiveEntry entry) entryInArchive.Delete(); } // TODO: Add exception handling - _zipArchive.CreateEntryFromFile(sourceFileName: entry.FullPath, entryName: entry.Name, compressionLevel: _compressionLevel); + _zipArchive.CreateEntryFromFile(sourceFileName: entry.FullPath, entryName: entryName, compressionLevel: _compressionLevel); } // TODO: Check what happens when we add a folder with children and then add a file + // TODO: Check if an entry also has a folder w/same name, if so, delete it and its children + // TODO: Schedule meeting } string[] IArchive.GetEntries() From fcde5dd6fe6f224ceca20f2cac5aebe23f8798f6 Mon Sep 17 00:00:00 2001 From: Abdullah Yousuf Date: Wed, 13 Jul 2022 20:31:43 -0700 Subject: [PATCH 10/42] fixed a bug in tests, added error handling for DestinationPath --- Tests/Compress-Archive.Tests.ps1 | 49 +++++++++++++++----------------- src/CompressArchiveCommand.cs | 40 ++++++++++++++++++++------ src/ErrorMessages.cs | 41 +++++++++++++++++++++++++- src/PathHelper.cs | 2 +- src/ZipArchive.cs | 6 ++-- 5 files changed, 99 insertions(+), 39 deletions(-) diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index 12970b1..e9c4e23 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -37,7 +37,7 @@ Add-CompressionAssemblies # Used for validating an archive's contents - function Test-Archive { + function Test-ZipArchive { param ( [string] $archivePath, @@ -136,7 +136,7 @@ try { Compress-Archive -LiteralPath $path -DestinationPath $destinationPath - throw "Failed to validate that an invalid Path $invalidPath was supplied as input to Compress-Archive cmdlet." + throw "Failed to validate that an invalid LiteralPath $invalidPath was supplied as input to Compress-Archive cmdlet." } catch { @@ -172,11 +172,11 @@ CompressArchiveLiteralPathParameterSetValidator "" "" } - It "Validate errors from Compress-Archive when invalid path (non-existing path / non-filesystem path) is supplied for Path or LiteralPath parameters" { - CompressArchiveInvalidPathValidator "$TestDrive$($DS)InvalidPath" $TestDrive "$TestDrive$($DS)InvalidPath" "PathNotFound,Microsoft.PowerShell.Archive.CompressArchiveCommand" + It "Validate errors from Compress-Archive when invalid path (non-existing path / non-filesystem path) is supplied for Path or LiteralPath parameters" -Tag this{ + CompressArchiveInvalidPathValidator "$TestDrive$($DS)InvalidPath" "$TestDrive($DS)archive.zip" "$TestDrive$($DS)InvalidPath" "PathNotFound,Microsoft.PowerShell.Archive.CompressArchiveCommand" $path = @("$TestDrive", "$TestDrive$($DS)InvalidPath") - CompressArchiveInvalidPathValidator $path $TestDrive "$TestDrive$($DS)InvalidPath" "PathNotFound,Microsoft.PowerShell.Archive.CompressArchiveCommand" + CompressArchiveInvalidPathValidator $path "$TestDrive($DS)archive.zip" "$TestDrive$($DS)InvalidPath" "PathNotFound,Microsoft.PowerShell.Archive.CompressArchiveCommand" } It "Validate error from Compress-Archive when duplicate paths are supplied as input to Path parameter" { @@ -243,35 +243,32 @@ $content = "Some Data" $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)Sample-1.txt - $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)Sample-2.txt - $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-3.txt - $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-4.txt - $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)ChildDir-2$($DS)Sample-5.txt - $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)ChildDir-2$($DS)Sample-6.txt - - "Some Text" > $TestDrive$($DS)Sample.unzip - "Some Text" > $TestDrive$($DS)Sample.cab - - #$preCreatedArchivePath = Join-Path $PSScriptRoot "SamplePreCreatedArchive.archive" - #Copy-Item $preCreatedArchivePath $TestDrive$($DS)SamplePreCreatedArchive.zip -Force - - #$preCreatedArchivePath = Join-Path $PSScriptRoot "TrailingSpacer.archive" - #Copy-Item $preCreatedArchivePath $TestDrive$($DS)TrailingSpacer.zip -Force + $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-2.txt + $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)ChildDir-2$($DS)Sample-3.txt } - - It "Validate that a single file can be compressed using Compress-Archive cmdlet" { - $sourcePath = "$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-3.txt" - $destinationPath = "$TestDrive$($DS)SampleSingleFile.zip" + It "Validate that a single file can be compressed" { + $sourcePath = "$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-2.txt" + $destinationPath = "$TestDrive$($DS)archive1.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath $destinationPath | Should -Exist + Test-ZipArchive $destinationPath "Sample-2.txt" } - It "Validate that an empty folder can be compressed" -Tag "this" { + It "Validate that an empty folder can be compressed" { $sourcePath = "$TestDrive$($DS)EmptyDir" - $destinationPath = "$TestDrive$($DS)EmptyDir.zip" + $destinationPath = "$TestDrive$($DS)archive2.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - Test-Archive $destinationPath "EmptyDir/" + $destinationPath | Should -Exist + Test-ZipArchive $destinationPath "EmptyDir/" + } + + It "Validate a folder containing files, non-empty folders, and empty folders can be compressed" { + $sourcePath = "$TestDrive$($DS)SourceDir" + $destinationPath = "$TestDrive$($DS)archive3.zip" + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + $destinationPath | Should -Exist + Test-ZipArchive $destinationPath "Sample-2.txt" } } diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index d12c38d..62a578c 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Management.Automation; +using System.Reflection; namespace Microsoft.PowerShell.Archive { @@ -10,13 +11,13 @@ namespace Microsoft.PowerShell.Archive public class CompressArchiveCommand : PSCmdlet { [Parameter(Mandatory = true, Position = 0, ParameterSetName = "Path", ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] - [Parameter(Mandatory = true, Position = 0, ParameterSetName = "PathWithForce", ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [Parameter(Mandatory = true, Position = 0, ParameterSetName = "PathWithOverwrite", ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] [Parameter(Mandatory = true, Position = 0, ParameterSetName = "PathWithUpdate", ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] public string[]? Path { get; set; } [Parameter(Mandatory = true, ParameterSetName = "LiteralPath", ValueFromPipeline = false, ValueFromPipelineByPropertyName = true)] - [Parameter(Mandatory = true, ParameterSetName = "LiteralPathWithForce", ValueFromPipeline = false, ValueFromPipelineByPropertyName = true)] + [Parameter(Mandatory = true, ParameterSetName = "LiteralPathWithOverwrite", ValueFromPipeline = false, ValueFromPipelineByPropertyName = true)] [Parameter(Mandatory = true, ParameterSetName = "LiteralPathWithUpdate", ValueFromPipeline = false, ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] [Alias("PSPath")] @@ -30,9 +31,9 @@ public class CompressArchiveCommand : PSCmdlet [Parameter(Mandatory = true, ParameterSetName = "LiteralPathWithUpdate", ValueFromPipeline = false, ValueFromPipelineByPropertyName = false)] public SwitchParameter Update { get; set; } - [Parameter(Mandatory = true, ParameterSetName = "PathWithForce", ValueFromPipeline = false, ValueFromPipelineByPropertyName = false)] - [Parameter(Mandatory = true, ParameterSetName = "LiteralPathWithForce", ValueFromPipeline = false, ValueFromPipelineByPropertyName = false)] - public SwitchParameter Force { get; set; } + [Parameter(Mandatory = true, ParameterSetName = "PathWithOverwrite", ValueFromPipeline = false, ValueFromPipelineByPropertyName = false)] + [Parameter(Mandatory = true, ParameterSetName = "LiteralPathWithOverwrite", ValueFromPipeline = false, ValueFromPipelineByPropertyName = false)] + public SwitchParameter Overwrite { get; set; } [Parameter()] public SwitchParameter PassThru { get; set; } = false; @@ -57,15 +58,37 @@ protected override void BeginProcessing() // TODO: Add exception handling DestinationPath = _pathHelper.ResolveToSingleFullyQualifiedPath(DestinationPath); - // TODO: If we are in update mode, check if archive exists - // TODO: If we are not in update mode, check if archive does not exist or Overwrite is true and the archive is not read-only + System.IO.FileInfo archiveFileInfo = new System.IO.FileInfo(DestinationPath); + System.IO.DirectoryInfo directoryInfo = new System.IO.DirectoryInfo(DestinationPath); + + // TODO: Add tests cases for conditions below + + //Throw an error if DestinationPath exists and the cmdlet is not in Update mode or Overwrite is not specified + if ((archiveFileInfo.Exists || directoryInfo.Exists) && !Update.IsPresent && !Overwrite.IsPresent) + { + ThrowTerminatingError(ErrorMessages.GetErrorRecordForArgumentException(ErrorCode.ArchiveExists, DestinationPath)); + } + //Throw an error if the cmdlet is in Update mode but the archive is read only + else if (archiveFileInfo.Exists && Update.IsPresent && archiveFileInfo.Attributes.HasFlag(FileAttributes.ReadOnly)) + { + ThrowTerminatingError(ErrorMessages.GetErrorRecordForArgumentException(ErrorCode.ArchiveReadOnly, DestinationPath)); + } + //Throw an error if the DestinationPath is a directory and the cmdlet is in Update mode + else if (directoryInfo.Exists && Update.IsPresent) + { + ThrowTerminatingError(ErrorMessages.GetErrorRecordForArgumentException(ErrorCode.ArchiveExistsAsDirectory, DestinationPath)); + } + //Throw an error if the DestinationPath is a directory with at least item and the cmdlet is in Overwrite mode + else if (directoryInfo.Exists && Overwrite.IsPresent && directoryInfo.GetFileSystemInfos().Length > 0) + { + ThrowTerminatingError(ErrorMessages.GetErrorRecordForArgumentException(ErrorCode.ArchiveExistsAsDirectory, DestinationPath)); + } } protected override void ProcessRecord() { if (ParameterSetName.StartsWith("Path")) _sourcePaths.AddRange(Path); else _sourcePaths.AddRange(LiteralPath); - } protected override void EndProcessing() @@ -77,6 +100,7 @@ protected override void EndProcessing() using (var archive = ArchiveFactory.GetArchive(ArchiveFormat.zip, DestinationPath, Update ? ArchiveMode.Update : ArchiveMode.Create, CompressionLevel)) { //Add entries to the archive + // TODO: Update progress foreach (ArchiveEntry entry in archiveEntries) { archive.AddFilesytemEntry(entry); diff --git a/src/ErrorMessages.cs b/src/ErrorMessages.cs index dcf7c29..e3935b5 100644 --- a/src/ErrorMessages.cs +++ b/src/ErrorMessages.cs @@ -13,6 +13,45 @@ internal static class ErrorMessages internal static string InvalidPathMessage = "The path(s) {0} are invalid."; - internal static string ResolvesToMultiplePathsMessage = "The path {0} resolves to multiple possible paths."; + internal static string PathResolvesToMultiplePathsMessage = "The path {0} resolves to multiple possible paths."; + + internal static string ArchiveExistsMessage = "The destination path {0} already exists"; + + internal static string ArchiveExistsAsDirectoryMessage = "The destination path {0} is a directory"; + + internal static string ArchiveIsReadOnlyMessage = "The archive at {0} is read-only."; + + internal static ErrorRecord GetErrorRecordForArgumentException(ErrorCode errorCode, string errorItem) + { + var errorMsg = String.Format(GetErrorMessage(errorCode: errorCode), errorItem); + var exception = new ArgumentException(errorMsg); + return new ErrorRecord(exception, errorCode.ToString(), ErrorCategory.InvalidArgument, errorItem); + } + + internal static string GetErrorMessage(ErrorCode errorCode) + { + return errorCode switch + { + ErrorCode.PathNotFound => PathNotFoundMessage, + ErrorCode.InvalidPath => InvalidPathMessage, + ErrorCode.DuplicatePaths => DuplicatePathsMessage, + ErrorCode.ArchiveExists => ArchiveExistsMessage, + ErrorCode.ArchiveExistsAsDirectory => ArchiveExistsAsDirectoryMessage, + ErrorCode.ArchiveReadOnly => ArchiveIsReadOnlyMessage, + ErrorCode.PathResolvesToMultiplePaths => PathResolvesToMultiplePathsMessage, + _ => throw new NotImplementedException("Error code has not been implemented") + }; + } + } + + internal enum ErrorCode + { + PathNotFound, + InvalidPath, + DuplicatePaths, + ArchiveExists, + ArchiveExistsAsDirectory, + ArchiveReadOnly, + PathResolvesToMultiplePaths } } diff --git a/src/PathHelper.cs b/src/PathHelper.cs index 29cb928..0deac3f 100644 --- a/src/PathHelper.cs +++ b/src/PathHelper.cs @@ -337,7 +337,7 @@ private void ThrowDuplicatePathsError(IEnumerable paths) private void ThrowResolvesToMultiplePathsError(string path) { - var errorMsg = String.Format(ErrorMessages.ResolvesToMultiplePathsMessage, path); + var errorMsg = String.Format(ErrorMessages.PathResolvesToMultiplePathsMessage, path); var exception = new System.ArgumentException(errorMsg); var errorRecord = new ErrorRecord(exception, "DuplicatePathFound", ErrorCategory.InvalidArgument, path); _cmdlet.ThrowTerminatingError(errorRecord); diff --git a/src/ZipArchive.cs b/src/ZipArchive.cs index 578876c..beac3d3 100644 --- a/src/ZipArchive.cs +++ b/src/ZipArchive.cs @@ -33,6 +33,8 @@ public ZipArchive(string archivePath, ArchiveMode mode, System.IO.FileStream arc _compressionLevel = compressionLevel; } + // If a file is added to the archive when it already contains a folder with the same name, + // it is up to the extraction software to deal with it (this is how it's done in other archive software) void IArchive.AddFilesytemEntry(ArchiveEntry entry) { if (_mode == ArchiveMode.Read) throw new InvalidOperationException("Cannot add a filesystem entry to an archive in read mode"); @@ -56,9 +58,7 @@ void IArchive.AddFilesytemEntry(ArchiveEntry entry) // TODO: Add exception handling _zipArchive.CreateEntryFromFile(sourceFileName: entry.FullPath, entryName: entryName, compressionLevel: _compressionLevel); } - // TODO: Check what happens when we add a folder with children and then add a file - // TODO: Check if an entry also has a folder w/same name, if so, delete it and its children - // TODO: Schedule meeting + } string[] IArchive.GetEntries() From 636444dea919582cd79999b37b6b0597fc7e5664 Mon Sep 17 00:00:00 2001 From: Abdullah Yousuf Date: Thu, 14 Jul 2022 13:22:50 -0700 Subject: [PATCH 11/42] fixed bug in tests where Test-ZipArchive was not working correctly --- Tests/Compress-Archive.Tests.ps1 | 37 +++++++++++++++++--------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index e9c4e23..429c812 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -1,10 +1,7 @@ <############################################################################################ - # File: Pester.Commands.Cmdlets.ArchiveTests.ps1 - # Commands.Cmdlets.ArchiveTests suite contains Tests that are - # used for validating Microsoft.PowerShell.Archive module. + # File: Compress-Archive.Tests.ps1 ############################################################################################> $script:TestSourceRoot = $PSScriptRoot - Write-Output $script:TestSourceRoot $DS = [System.IO.Path]::DirectorySeparatorChar if ($IsWindows -eq $null) { $IsWindows = $PSVersionTable.PSEdition -eq "Desktop" @@ -55,11 +52,17 @@ $actualEntryCount = $zipArchive.Entries.Count $actualEntryCount | Should -Be $expectedEntries.Length - # Get a list of archive entries + # Get a list of entry names in the zip archive + $archiveEntries = @() + ForEach ($archiveEntry in $zipArchive.Entries) { + $archiveEntries += $archiveEntry.FullName + } - ForEach ($expectedArchiveEntry in $expectedEntries) { - $expectedArchiveEntry | Should -BeIn $zipArchive.Entries + # Ensure each entry in the archive is in the list of expected entries + ForEach ($expectedEntry in $expectedEntries) { + $expectedEntry | Should -BeIn $archiveEntries } + } finally { @@ -172,7 +175,7 @@ CompressArchiveLiteralPathParameterSetValidator "" "" } - It "Validate errors from Compress-Archive when invalid path (non-existing path / non-filesystem path) is supplied for Path or LiteralPath parameters" -Tag this{ + It "Validate errors from Compress-Archive when invalid path (non-existing path / non-filesystem path) is supplied for Path or LiteralPath parameters" { CompressArchiveInvalidPathValidator "$TestDrive$($DS)InvalidPath" "$TestDrive($DS)archive.zip" "$TestDrive$($DS)InvalidPath" "PathNotFound,Microsoft.PowerShell.Archive.CompressArchiveCommand" $path = @("$TestDrive", "$TestDrive$($DS)InvalidPath") @@ -196,7 +199,7 @@ } } - It "Validate error from Compress-Archive when duplicate paths are supplied as input to LiteralPath parameter" -tag th { + It "Validate error from Compress-Archive when duplicate paths are supplied as input to LiteralPath parameter" { $sourcePath = @( "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt", "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt") @@ -214,7 +217,7 @@ } ## From 504 - It "Validate that Source Path can be at SystemDrive location" -skip:(!$IsWindows) { + It "Validate that Source Path can be at SystemDrive location" -Skip { $sourcePath = "$env:SystemDrive$($DS)SourceDir" $destinationPath = "$TestDrive$($DS)SampleFromSystemDrive.zip" New-Item $sourcePath -Type Directory | Out-Null # not enough permissions to write to drive root on Linux @@ -226,7 +229,7 @@ } finally { - del "$sourcePath" -Force -Recurse -ErrorAction SilentlyContinue + Remove-Item "$sourcePath" -Force -Recurse -ErrorAction SilentlyContinue } } } @@ -252,15 +255,15 @@ $destinationPath = "$TestDrive$($DS)archive1.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath $destinationPath | Should -Exist - Test-ZipArchive $destinationPath "Sample-2.txt" + Test-ZipArchive $destinationPath @('Sample-2.txt') } - It "Validate that an empty folder can be compressed" { + It "Validate that an empty folder can be compressed" -Tag this{ $sourcePath = "$TestDrive$($DS)EmptyDir" $destinationPath = "$TestDrive$($DS)archive2.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath $destinationPath | Should -Exist - Test-ZipArchive $destinationPath "EmptyDir/" + Test-ZipArchive $destinationPath @('EmptyDir/') } It "Validate a folder containing files, non-empty folders, and empty folders can be compressed" { @@ -268,11 +271,11 @@ $destinationPath = "$TestDrive$($DS)archive3.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath $destinationPath | Should -Exist - Test-ZipArchive $destinationPath "Sample-2.txt" + Test-ZipArchive $destinationPath @("SourceDir/", "Sample-2.txt") } } - Context "Update tests" { + Context "Update tests" -Skip { BeforeAll { New-Item $TestDrive$($DS)SourceDir -Type Directory | Out-Null New-Item $TestDrive$($DS)SourceDir$($DS)ChildDir-1 -Type Directory | Out-Null @@ -299,7 +302,7 @@ } } - Context "Relative Path tests" { + Context "Relative Path tests" -Skip { BeforeAll { New-Item $TestDrive$($DS)SourceDir -Type Directory | Out-Null New-Item $TestDrive$($DS)SourceDir$($DS)ChildDir-1 -Type Directory | Out-Null From 50e0da39dcb61d0f5ffc6c2622745942e7148977 Mon Sep 17 00:00:00 2001 From: Abdullah Yousuf Date: Thu, 14 Jul 2022 18:06:29 -0700 Subject: [PATCH 12/42] added tests for DestinationPath --- Tests/Compress-Archive.Tests.ps1 | 78 +++++++++++++++++++++++++++++--- src/CompressArchiveCommand.cs | 30 +++++++----- src/ErrorMessages.cs | 3 +- src/PathHelper.cs | 2 +- 4 files changed, 93 insertions(+), 20 deletions(-) diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index 429c812..0089de2 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -258,7 +258,7 @@ Test-ZipArchive $destinationPath @('Sample-2.txt') } - It "Validate that an empty folder can be compressed" -Tag this{ + It "Validate that an empty folder can be compressed" { $sourcePath = "$TestDrive$($DS)EmptyDir" $destinationPath = "$TestDrive$($DS)archive2.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath @@ -276,30 +276,94 @@ } Context "Update tests" -Skip { + + } + + Context "DestinationPath tests" { BeforeAll { New-Item $TestDrive$($DS)SourceDir -Type Directory | Out-Null - New-Item $TestDrive$($DS)SourceDir$($DS)ChildDir-1 -Type Directory | Out-Null $content = "Some Data" $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)Sample-1.txt - $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-2.txt + + New-Item $TestDrive$($DS)archive3.zip -Type Directory | Out-Null + + New-Item $TestDrive$($DS)EmptyDirectory -Type Directory | Out-Null } - It "Validate error from Compress-Archive when archive file already exists and -Update and -Force parameters is not specified" { + It "Throws an error when archive file already exists and -Update and -Overwrite parameters are not specified" { $sourcePath = "$TestDrive$($DS)SourceDir" - $destinationPath = "$TestDrive$($DS)ValidateErrorWhenUpdateNotSpecified.zip" + $destinationPath = "$TestDrive$($DS)archive1.zip" try { "Some Data" > $destinationPath Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - throw "Failed to validate that an archive file format $destinationPath already exists and -Update switch parameter is not specified while running Compress-Archive command." + throw "Failed to validate that an archive file format $destinationPath already exists and -Update switch parameter is not specified." + } + catch + { + $_.FullyQualifiedErrorId | Should -Be "ArchiveExists,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } + + It "Throws a terminating error when archive does not exist and -Update mode is specified" { + $sourcePath = "$TestDrive$($DS)SourceDir" + $destinationPath = "$TestDrive$($DS)archive2.zip" + + try + { + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Update + throw "Failed to validate that an archive file format $destinationPath does not exist and -Update switch parameter is specified." + } + catch + { + $_.FullyQualifiedErrorId | Should -Be "ArchiveDoesNotExist,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } + + It "Throws a terminating error when DestinationPath is a directory and -Update is specified" { + $sourcePath = "$TestDrive$($DS)SourceDir" + $destinationPath = "$TestDrive$($DS)archive3.zip" + + try + { + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Update + throw "Failed to validate that a directory $destinationPath exists and -Update switch parameter is specified." + } + catch + { + $_.FullyQualifiedErrorId | Should -Be "ArchiveExistsAsDirectory,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } + + It "Throws a terminating error when DestinationPath is a folder containing at least 1 item and Overwrite is specified" { + $sourcePath = "$TestDrive$($DS)SourceDir" + $destinationPath = "$TestDrive" + + try + { + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Overwrite + throw "Failed to detect an error when $destinationPath is an existing directory containing at least 1 item and -Overwrite switch parameter is specified." } catch { - $_.FullyQualifiedErrorId | Should -Be "ArchiveFileExists,Microsoft.PowerShell.Archive.CompressArchiveCommand" + $_.FullyQualifiedErrorId | Should -Be "ArchiveExistsAsDirectory,Microsoft.PowerShell.Archive.CompressArchiveCommand" } } + + It "Overwrites a directory containing no items when -Overwrite is specified" { + $sourcePath = "$TestDrive$($DS)SourceDir" + $destinationPath = "$TestDrive$($DS)EmptyDirectory" + + (Get-Item $destinationPath) -is [System.IO.DirectoryInfo] | Should -Be $true + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Overwrite + + # Ensure $destiationPath is now a file + $destinationPathInfo = Get-Item $destinationPath + $destinationPathInfo -is [System.IO.DirectoryInfo] | Should -Be $false + $destinationPathInfo -is [System.IO.FileInfo] | Should -Be $true + } } Context "Relative Path tests" -Skip { diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index 62a578c..70fb027 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -58,31 +58,39 @@ protected override void BeginProcessing() // TODO: Add exception handling DestinationPath = _pathHelper.ResolveToSingleFullyQualifiedPath(DestinationPath); - System.IO.FileInfo archiveFileInfo = new System.IO.FileInfo(DestinationPath); - System.IO.DirectoryInfo directoryInfo = new System.IO.DirectoryInfo(DestinationPath); - // TODO: Add tests cases for conditions below + bool isArchiveAnExistingFile = System.IO.File.Exists(DestinationPath); + bool isArchiveAnExistingDirectory = System.IO.Directory.Exists(DestinationPath); + + var archiveFileInfo = new System.IO.FileInfo(DestinationPath); + var archiveDirectoryInfo = new System.IO.DirectoryInfo(DestinationPath); + //Throw an error if DestinationPath exists and the cmdlet is not in Update mode or Overwrite is not specified - if ((archiveFileInfo.Exists || directoryInfo.Exists) && !Update.IsPresent && !Overwrite.IsPresent) + if ((isArchiveAnExistingDirectory || isArchiveAnExistingFile) && !Update.IsPresent && !Overwrite.IsPresent) { ThrowTerminatingError(ErrorMessages.GetErrorRecordForArgumentException(ErrorCode.ArchiveExists, DestinationPath)); } - //Throw an error if the cmdlet is in Update mode but the archive is read only - else if (archiveFileInfo.Exists && Update.IsPresent && archiveFileInfo.Attributes.HasFlag(FileAttributes.ReadOnly)) - { - ThrowTerminatingError(ErrorMessages.GetErrorRecordForArgumentException(ErrorCode.ArchiveReadOnly, DestinationPath)); - } //Throw an error if the DestinationPath is a directory and the cmdlet is in Update mode - else if (directoryInfo.Exists && Update.IsPresent) + else if (isArchiveAnExistingDirectory && Update.IsPresent) { ThrowTerminatingError(ErrorMessages.GetErrorRecordForArgumentException(ErrorCode.ArchiveExistsAsDirectory, DestinationPath)); } + //Throw an error if the cmdlet is in Update mode but the archive is read only + else if (isArchiveAnExistingFile && Update.IsPresent && archiveFileInfo.Attributes.HasFlag(FileAttributes.ReadOnly)) + { + ThrowTerminatingError(ErrorMessages.GetErrorRecordForArgumentException(ErrorCode.ArchiveReadOnly, DestinationPath)); + } //Throw an error if the DestinationPath is a directory with at least item and the cmdlet is in Overwrite mode - else if (directoryInfo.Exists && Overwrite.IsPresent && directoryInfo.GetFileSystemInfos().Length > 0) + else if (isArchiveAnExistingDirectory && Overwrite.IsPresent && archiveDirectoryInfo.GetFileSystemInfos().Length > 0) { ThrowTerminatingError(ErrorMessages.GetErrorRecordForArgumentException(ErrorCode.ArchiveExistsAsDirectory, DestinationPath)); } + //Throw an error if the cmdlet is in Update mode but the archive does not exist + else if (!isArchiveAnExistingFile && Update.IsPresent) + { + + } } protected override void ProcessRecord() diff --git a/src/ErrorMessages.cs b/src/ErrorMessages.cs index e3935b5..82946d8 100644 --- a/src/ErrorMessages.cs +++ b/src/ErrorMessages.cs @@ -52,6 +52,7 @@ internal enum ErrorCode ArchiveExists, ArchiveExistsAsDirectory, ArchiveReadOnly, - PathResolvesToMultiplePaths + PathResolvesToMultiplePaths, + ArchiveDoesNotExist } } diff --git a/src/PathHelper.cs b/src/PathHelper.cs index 0deac3f..2d81ca2 100644 --- a/src/PathHelper.cs +++ b/src/PathHelper.cs @@ -264,7 +264,7 @@ private IEnumerable GetDuplicatePaths(List entries) // TODO: Add directory seperator char at end internal string ResolveToSingleFullyQualifiedPath(string path) { - //path be literal or non-literal + //path can be literal or non-literal //First, get non-literal path string nonLiteralPath = GetUnresolvedProviderPathFromPSPath(path) ?? throw new ArgumentException($"Path {path} was resolved to null"); From 5cede7ed8baf0cd14306adff12560ab93b4cc6ef Mon Sep 17 00:00:00 2001 From: Abdullah Yousuf Date: Sat, 16 Jul 2022 20:15:12 -0700 Subject: [PATCH 13/42] added additional tests, added TODOs --- Tests/Compress-Archive.Tests.ps1 | 80 ++++++++++++++++++++++++++++++++ src/ArchiveFactory.cs | 1 + src/CompressArchiveCommand.cs | 24 ++++++++-- src/ErrorMessages.cs | 1 + src/ZipArchive.cs | 1 + 5 files changed, 103 insertions(+), 4 deletions(-) diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index 0089de2..0c169ad 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -151,6 +151,8 @@ New-Item $TestDrive$($DS)SourceDir -Type Directory | Out-Null $content = "Some Data" $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)Sample-1.txt + + New-Item $TestDrive$($DS)EmptyDirectory -Type Directory | Out-Null } @@ -232,6 +234,80 @@ Remove-Item "$sourcePath" -Force -Recurse -ErrorAction SilentlyContinue } } + + It "Throws an error when Path and DestinationPath are the same" { + $sourcePath = "$TestDrive$($DS)EmptyDirectory" + $destinationPath = $sourcePath + + try { + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + throw "Failed to detect an error when Path and DestinationPath are the same" + } catch { + $_.FullyQualifiedErrorId | Should -Be "SamePathAndDestinationPath,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } + + It "Throws an error when Path and DestinationPath are the same and -Update is specified" { + $sourcePath = "$TestDrive$($DS)EmptyDirectory" + $destinationPath = $sourcePath + + try { + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Update + throw "Failed to detect an error when Path and DestinationPath are the same and -Update is specified" + } catch { + $_.FullyQualifiedErrorId | Should -Be "SamePathAndDestinationPath,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } + + It "Throws an error when Path and DestinationPath are the same and -Overwrite is specified" { + $sourcePath = "$TestDrive$($DS)EmptyDirectory" + $destinationPath = $sourcePath + + try { + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Overwrite + throw "Failed to detect an error when Path and DestinationPath are the same and -Overwrite is specified" + } catch { + $_.FullyQualifiedErrorId | Should -Be "SamePathAndDestinationPath,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } + + It "Throws an error when LiteralPath and DestinationPath are the same" { + $sourcePath = "$TestDrive$($DS)EmptyDirectory" + $destinationPath = $sourcePath + + try { + Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath + throw "Failed to detect an error when LiteralPath and DestinationPath are the same" + } catch { + $_.FullyQualifiedErrorId | Should -Be "SameLiteralPathAndDestinationPath,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } + + It "Throws an error when LiteralPath and DestinationPath are the same and -Update is specified" { + $sourcePath = "$TestDrive$($DS)EmptyDirectory" + $destinationPath = $sourcePath + + try { + Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath -Update + throw "Failed to detect an error when LiteralPath and DestinationPath are the same and -Update is specified" + } catch { + $_.FullyQualifiedErrorId | Should -Be "SamePathAndDestinationPath,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } + + It "Throws an error when LiteralPath and DestinationPath are the same and -Overwrite is specified" { + $sourcePath = "$TestDrive$($DS)EmptyDirectory" + $destinationPath = $sourcePath + + try { + Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath -Overwrite + throw "Failed to detect an error when LiteralPath and DestinationPath are the same and -Overwrite is specified" + } catch { + $_.FullyQualifiedErrorId | Should -Be "SamePathAndDestinationPath,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } + + } Context "Basic functional tests" { @@ -351,6 +427,10 @@ $_.FullyQualifiedErrorId | Should -Be "ArchiveExistsAsDirectory,Microsoft.PowerShell.Archive.CompressArchiveCommand" } } + } + + Context "-Overwrite Tests" { + It "Overwrites a directory containing no items when -Overwrite is specified" { $sourcePath = "$TestDrive$($DS)SourceDir" diff --git a/src/ArchiveFactory.cs b/src/ArchiveFactory.cs index 035df8c..c728dc8 100644 --- a/src/ArchiveFactory.cs +++ b/src/ArchiveFactory.cs @@ -22,6 +22,7 @@ internal static IArchive GetArchive(ArchiveFormat format, string archivePath, Ar return format switch { ArchiveFormat.zip => new ZipArchive(archivePath, archiveMode, archiveFileStream, compressionLevel), + // TODO: Add archive types here // TODO: Add message to exception _ => throw new NotImplementedException() }; diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index 70fb027..dae6a4f 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -10,6 +10,8 @@ namespace Microsoft.PowerShell.Archive [OutputType(typeof(System.IO.FileInfo))] public class CompressArchiveCommand : PSCmdlet { + // TODO: Add filter parameter + [Parameter(Mandatory = true, Position = 0, ParameterSetName = "Path", ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] [Parameter(Mandatory = true, Position = 0, ParameterSetName = "PathWithOverwrite", ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] [Parameter(Mandatory = true, Position = 0, ParameterSetName = "PathWithUpdate", ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] @@ -54,7 +56,6 @@ public CompressArchiveCommand() protected override void BeginProcessing() { - base.BeginProcessing(); // TODO: Add exception handling DestinationPath = _pathHelper.ResolveToSingleFullyQualifiedPath(DestinationPath); @@ -95,16 +96,26 @@ protected override void BeginProcessing() protected override void ProcessRecord() { - if (ParameterSetName.StartsWith("Path")) _sourcePaths.AddRange(Path); - else _sourcePaths.AddRange(LiteralPath); + if (ParameterSetName.StartsWith("Path")) + { + _sourcePaths.AddRange(Path); + } + else + { + _sourcePaths.AddRange(LiteralPath); + } + } protected override void EndProcessing() { //Get archive entries, validation is performed by PathHelper + List archiveEntries = _pathHelper.GetEntryRecordsForPath(_sourcePaths.ToArray(), ParameterSetName.StartsWith("LiteralPath")); - //Create a zip archive + //Create an archive + // This is where we will switch between different types of archives + // TODO: Add shouldprocess support using (var archive = ArchiveFactory.GetArchive(ArchiveFormat.zip, DestinationPath, Update ? ArchiveMode.Update : ArchiveMode.Create, CompressionLevel)) { //Add entries to the archive @@ -114,6 +125,11 @@ protected override void EndProcessing() archive.AddFilesytemEntry(entry); } } + + try + { + + } } protected override void StopProcessing() diff --git a/src/ErrorMessages.cs b/src/ErrorMessages.cs index 82946d8..0de8761 100644 --- a/src/ErrorMessages.cs +++ b/src/ErrorMessages.cs @@ -7,6 +7,7 @@ namespace Microsoft.PowerShell.Archive { internal static class ErrorMessages { + // TODO: Move error messages to .resx file internal static string PathNotFoundMessage = "The path {0} could not be found"; internal static string DuplicatePathsMessage = "The path(s) {0} have been specified more than once."; diff --git a/src/ZipArchive.cs b/src/ZipArchive.cs index beac3d3..8a65a6a 100644 --- a/src/ZipArchive.cs +++ b/src/ZipArchive.cs @@ -35,6 +35,7 @@ public ZipArchive(string archivePath, ArchiveMode mode, System.IO.FileStream arc // If a file is added to the archive when it already contains a folder with the same name, // it is up to the extraction software to deal with it (this is how it's done in other archive software) + // TODO: Explain how to add folders to the archive void IArchive.AddFilesytemEntry(ArchiveEntry entry) { if (_mode == ArchiveMode.Read) throw new InvalidOperationException("Cannot add a filesystem entry to an archive in read mode"); From 88ea929c0abc42c301f6a89587f9ed0d71b100d5 Mon Sep 17 00:00:00 2001 From: Abdullah Yousuf Date: Mon, 18 Jul 2022 09:49:41 -0700 Subject: [PATCH 14/42] updated tests --- Tests/Compress-Archive.Tests.ps1 | 62 +++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index 0c169ad..856cd1f 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -430,7 +430,20 @@ } Context "-Overwrite Tests" { - + BeforeAll { + New-Item $TestDrive$($DS)SourceDir -Type Directory | Out-Null + + $content = "Some Data" + $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)Sample-1.txt + + New-Item $TestDrive$($DS)EmptyDirectory -Type Directory | Out-Null + + # Create $TestDrive$($DS)archive.zip + Compress-Archive -Path $TestDrive$($DS)SourceDir$($DS)Sample-1.txt -DestinationPath "$TestDrive$($DS)archive.zip" + + # Create Sample-2.txt + $content | Out-File -FilePath $TestDrive$($DS)Sample-2.txt + } It "Overwrites a directory containing no items when -Overwrite is specified" { $sourcePath = "$TestDrive$($DS)SourceDir" @@ -444,6 +457,20 @@ $destinationPathInfo -is [System.IO.DirectoryInfo] | Should -Be $false $destinationPathInfo -is [System.IO.FileInfo] | Should -Be $true } + + It "Overwrites an archive that already exists" { + $destinationPath = "$TestDrive$($DS)archive.zip" + + # Get the entries of the original zip archive + Test-ZipArchive $destinationPath @("Sample-1.txt") + + # Overwrite the archive + $sourcePath = "$TestDrive$($DS)Sample-2.txt" + Compress-Archive -Path $sourcePath -DestinationPath "$TestDrive$($DS)archive.zip" -Overwrite + + # Ensure the original entries and different than the new entries + Test-ZipArchive $destinationPath @("Sample-2.txt") + } } Context "Relative Path tests" -Skip { @@ -504,4 +531,37 @@ } } } + + Contect "Special and Wildcard Characters Tests" { + BeforeAll { + New-Item $TestDrive$($DS)SourceDir -Type Directory | Out-Null + + $content = "Some Data" + $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)Sample-1.txt + } + + + It "Accepts DestinationPath parameter with wildcard characters that resolves to one path" { + $sourcePath = "$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-3.txt" + $destinationPath = "$TestDrive$($DS)Sample[]SingleFile.zip" + try + { + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + Test-Path -LiteralPath $destinationPath | Should Be $true + } + finally + { + Remove-Item -LiteralPath $destinationPath -Force + } + } + + It "Accepts DestinationPath parameter with [ but no matching ]" { + $sourcePath = "$TestDrive$($DS)SourceDir" + $destinationPath = "$TestDrive$($DS)archive[2.zip" + + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + Test-Path -LiteralPath $destinationPath | Should Be $true + Test-ZipArchive $destinationPath @("SourceDir/", "SourceDir/Sample-1.txt") + } + } } \ No newline at end of file From 3dffd1dc941d5b61364a6347fdd03c2eb13e76ff Mon Sep 17 00:00:00 2001 From: Abdullah Yousuf Date: Mon, 18 Jul 2022 17:25:58 -0700 Subject: [PATCH 15/42] added resx files for messages, refactored code, updated structure, etc. --- src/Action.cs | 13 ++ src/ArchiveAddition.cs | 45 +++++ src/ArchiveEntry.cs | 19 -- src/ArchiveFormat.cs | 2 +- src/CompressArchiveCommand.cs | 222 +++++++++++++++++------- src/ErrorMessages.cs | 52 +++--- src/IArchive.cs | 2 +- src/Localized/Messages.Designer.cs | 144 +++++++++++++++ src/Localized/Messages.en-US.resx | 141 +++++++++++++++ src/Localized/Messages.resx | 147 ++++++++++++++++ src/Microsoft.PowerShell.Archive.csproj | 16 ++ src/PathHelper.cs | 176 ++++++++++++------- src/ZipArchive.cs | 9 +- 13 files changed, 807 insertions(+), 181 deletions(-) create mode 100644 src/Action.cs create mode 100644 src/ArchiveAddition.cs delete mode 100644 src/ArchiveEntry.cs create mode 100644 src/Localized/Messages.Designer.cs create mode 100644 src/Localized/Messages.en-US.resx create mode 100644 src/Localized/Messages.resx diff --git a/src/Action.cs b/src/Action.cs new file mode 100644 index 0000000..f21c881 --- /dev/null +++ b/src/Action.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.PowerShell.Archive +{ + public enum Action + { + Create, + Update, + Overwrite + } +} diff --git a/src/ArchiveAddition.cs b/src/ArchiveAddition.cs new file mode 100644 index 0000000..5092300 --- /dev/null +++ b/src/ArchiveAddition.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.PowerShell.Archive +{ + /// + /// ArchiveAddition represents an filesystem entry that we want to add to or update in the archive. + /// ArchiveAddition DOES NOT represent an entry in the archive -- rather, it represents an entry to be created or updated using the information contained in an instance of this class. + /// + internal class ArchiveAddition + { + /// + /// The name of the file or directory in the archive. + /// This is a path of the file or directory in the archive (e.g., 'file1.txt` means the file is a top-level file in the archive). + /// + public string EntryName { get; set; } + + /// + /// The fully qualified path of the file or directory to add to or update in the archive. + /// + public string FullPath { get; set; } + + /// + /// The type of filesystem entry to add. + /// + public ArchiveAdditionType Type { get; set; } + + public ArchiveAddition(string entryName, string fullPath, ArchiveAdditionType type) + { + EntryName = entryName; + FullPath = fullPath; + Type = type; + } + + /// + /// This enum tracks types of filesystem entries + /// + internal enum ArchiveAdditionType + { + File, + Directory, + } + } +} diff --git a/src/ArchiveEntry.cs b/src/ArchiveEntry.cs deleted file mode 100644 index 8865d5f..0000000 --- a/src/ArchiveEntry.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Microsoft.PowerShell.Archive -{ - internal class ArchiveEntry - { - public string Name { get; set; } - - public string FullPath { get; set; } - - public ArchiveEntry(string name, string fullPath) - { - Name = name; - FullPath = fullPath; - } - } -} diff --git a/src/ArchiveFormat.cs b/src/ArchiveFormat.cs index df05bea..51ea3a4 100644 --- a/src/ArchiveFormat.cs +++ b/src/ArchiveFormat.cs @@ -4,7 +4,7 @@ namespace Microsoft.PowerShell.Archive { - internal enum ArchiveFormat + public enum ArchiveFormat { zip, } diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index dae6a4f..91435bf 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -1,6 +1,9 @@ -using System; +using Microsoft.PowerShell.Archive.Localized; +using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; using System.Management.Automation; using System.Reflection; @@ -10,41 +13,57 @@ namespace Microsoft.PowerShell.Archive [OutputType(typeof(System.IO.FileInfo))] public class CompressArchiveCommand : PSCmdlet { + // TODO: Add filter parameter + // TODO: Add format parameter + // TODO: Add flatten parameter + // TODO: Add comments to methods + + // TODO: Add warnings for archive file extension + // TODO: Add tar support + + // TODO: Add comments to ArchiveEntry and for adding filesystem entry to zip + + // TODO: Add error messages for each error code + /// + /// The Path parameter - specifies paths of files or directories from the filesystem to add to or update in the archive. + /// This parameter does expand wildcard characters. + /// [Parameter(Mandatory = true, Position = 0, ParameterSetName = "Path", ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] - [Parameter(Mandatory = true, Position = 0, ParameterSetName = "PathWithOverwrite", ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] - [Parameter(Mandatory = true, Position = 0, ParameterSetName = "PathWithUpdate", ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] public string[]? Path { get; set; } - [Parameter(Mandatory = true, ParameterSetName = "LiteralPath", ValueFromPipeline = false, ValueFromPipelineByPropertyName = true)] - [Parameter(Mandatory = true, ParameterSetName = "LiteralPathWithOverwrite", ValueFromPipeline = false, ValueFromPipelineByPropertyName = true)] - [Parameter(Mandatory = true, ParameterSetName = "LiteralPathWithUpdate", ValueFromPipeline = false, ValueFromPipelineByPropertyName = true)] + /// + /// The LiteralPath parameter - specifies paths of files or directories from the filesystem to add to or update in the archive. + /// This parameter does not expand wildcard characters. + /// + [Parameter(Mandatory = true, Position = 1, ParameterSetName = "LiteralPath", ValueFromPipeline = false, ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] [Alias("PSPath")] public string[]? LiteralPath { get; set; } - [Parameter(Mandatory = true, Position = 1, ValueFromPipeline = false, ValueFromPipelineByPropertyName = false)] + /// + /// The DestinationPath parameter - specifies the location of the archive in the filesystem. + /// + [Parameter(Mandatory = true, Position = 2, ValueFromPipeline = false, ValueFromPipelineByPropertyName = false)] [ValidateNotNullOrEmpty] + [NotNull] public string? DestinationPath { get; set; } - [Parameter(Mandatory = true, ParameterSetName = "PathWithUpdate", ValueFromPipeline = false, ValueFromPipelineByPropertyName = false)] - [Parameter(Mandatory = true, ParameterSetName = "LiteralPathWithUpdate", ValueFromPipeline = false, ValueFromPipelineByPropertyName = false)] - public SwitchParameter Update { get; set; } - - [Parameter(Mandatory = true, ParameterSetName = "PathWithOverwrite", ValueFromPipeline = false, ValueFromPipelineByPropertyName = false)] - [Parameter(Mandatory = true, ParameterSetName = "LiteralPathWithOverwrite", ValueFromPipeline = false, ValueFromPipelineByPropertyName = false)] - public SwitchParameter Overwrite { get; set; } + [Parameter()] + public Action Action { get; set; } = Action.Create; [Parameter()] public SwitchParameter PassThru { get; set; } = false; [Parameter()] [ValidateNotNullOrEmpty] - public System.IO.Compression.CompressionLevel CompressionLevel { get; set; } + public System.IO.Compression.CompressionLevel CompressionLevel { get; set; } = System.IO.Compression.CompressionLevel.Optimal; - private List _sourcePaths; + public ArchiveFormat Format { get; set; } = ArchiveFormat.zip; + + private List? _sourcePaths; private PathHelper _pathHelper; @@ -59,76 +78,85 @@ protected override void BeginProcessing() // TODO: Add exception handling DestinationPath = _pathHelper.ResolveToSingleFullyQualifiedPath(DestinationPath); - // TODO: Add tests cases for conditions below - - bool isArchiveAnExistingFile = System.IO.File.Exists(DestinationPath); - bool isArchiveAnExistingDirectory = System.IO.Directory.Exists(DestinationPath); - - var archiveFileInfo = new System.IO.FileInfo(DestinationPath); - var archiveDirectoryInfo = new System.IO.DirectoryInfo(DestinationPath); - - //Throw an error if DestinationPath exists and the cmdlet is not in Update mode or Overwrite is not specified - if ((isArchiveAnExistingDirectory || isArchiveAnExistingFile) && !Update.IsPresent && !Overwrite.IsPresent) - { - ThrowTerminatingError(ErrorMessages.GetErrorRecordForArgumentException(ErrorCode.ArchiveExists, DestinationPath)); - } - //Throw an error if the DestinationPath is a directory and the cmdlet is in Update mode - else if (isArchiveAnExistingDirectory && Update.IsPresent) - { - ThrowTerminatingError(ErrorMessages.GetErrorRecordForArgumentException(ErrorCode.ArchiveExistsAsDirectory, DestinationPath)); - } - //Throw an error if the cmdlet is in Update mode but the archive is read only - else if (isArchiveAnExistingFile && Update.IsPresent && archiveFileInfo.Attributes.HasFlag(FileAttributes.ReadOnly)) - { - ThrowTerminatingError(ErrorMessages.GetErrorRecordForArgumentException(ErrorCode.ArchiveReadOnly, DestinationPath)); - } - //Throw an error if the DestinationPath is a directory with at least item and the cmdlet is in Overwrite mode - else if (isArchiveAnExistingDirectory && Overwrite.IsPresent && archiveDirectoryInfo.GetFileSystemInfos().Length > 0) - { - ThrowTerminatingError(ErrorMessages.GetErrorRecordForArgumentException(ErrorCode.ArchiveExistsAsDirectory, DestinationPath)); - } - //Throw an error if the cmdlet is in Update mode but the archive does not exist - else if (!isArchiveAnExistingFile && Update.IsPresent) - { - - } + // Validate DestinationPath + ValidateDestinationPath(); } protected override void ProcessRecord() { + // Add each path from -Path or -LiteralPath to _sourcePaths because they can get lost when the next item in the pipeline is sent if (ParameterSetName.StartsWith("Path")) { - _sourcePaths.AddRange(Path); + _sourcePaths?.AddRange(Path); } else { - _sourcePaths.AddRange(LiteralPath); + _sourcePaths?.AddRange(LiteralPath); } - } protected override void EndProcessing() { - //Get archive entries, validation is performed by PathHelper - - List archiveEntries = _pathHelper.GetEntryRecordsForPath(_sourcePaths.ToArray(), ParameterSetName.StartsWith("LiteralPath")); - - //Create an archive - // This is where we will switch between different types of archives - // TODO: Add shouldprocess support - using (var archive = ArchiveFactory.GetArchive(ArchiveFormat.zip, DestinationPath, Update ? ArchiveMode.Update : ArchiveMode.Create, CompressionLevel)) + // Get archive entries, validation is performed by PathHelper + // _sourcePaths should not be null at this stage, but if it is, prevent a NullReferenceException by doing the following + List archiveAddtions = _sourcePaths != null ? _pathHelper.GetArchiveAdditionsForPath(_sourcePaths.ToArray(), ParameterSetName.StartsWith("LiteralPath")) : new List(); + + // Remove references to _sourcePaths, Path, and LiteralPath to free up memory + // The user could have supplied a lot of paths, so we should do this + Path = null; + LiteralPath = null; + _sourcePaths = null; + + // Throw a terminating error if there is a source path as same as DestinationPath. + // We don't want to overwrite the file or directory that we want to add to the archive. + var additionsWithSamePathAsDestination = archiveAddtions.Where(addition => addition.FullPath == DestinationPath).ToList(); + if (additionsWithSamePathAsDestination.Count() > 0) { - //Add entries to the archive - // TODO: Update progress - foreach (ArchiveEntry entry in archiveEntries) - { - archive.AddFilesytemEntry(entry); - } + // Since duplicate checking is performed earlier, there must a single ArchiveAddition such that ArchiveAddition.FullPath == DestinationPath + var errorCode = ParameterSetName.StartsWith("Path") ? ErrorCode.SamePathAndDestinationPath : ErrorCode.SameLiteralPathAndDestinationPath; + var errorRecord = ErrorMessages.GetErrorRecord(errorCode, errorItem: additionsWithSamePathAsDestination[0].FullPath); + ThrowTerminatingError(errorRecord); } + // Warn the user if there are no items to add for some reason (e.g., no items matched the filter) + if (archiveAddtions.Count == 0) + { + WriteWarning(Messages.NoItemsToAddWarning); + } + + // Get the ArchiveMode for the archive to be created or updated + ArchiveMode archiveMode = ArchiveMode.Create; + if (Action == Action.Update) + { + archiveMode = ArchiveMode.Update; + } + + // Don't create the archive object yet because the user could have specified -WhatIf or -Confirm + IArchive? archive = null; try + { + if (ShouldProcess(target: DestinationPath, action: "Create")) + { + // Create an archive -- this is where we will switch between different types of archives + archive = ArchiveFactory.GetArchive(format: Format, archivePath: DestinationPath, archiveMode: archiveMode, compressionLevel: CompressionLevel); + } + + // TODO: Update progress + foreach (ArchiveAddition entry in archiveAddtions) + { + if (ShouldProcess(target: entry.FullPath, action: "Add")) + { + archive?.AddFilesytemEntry(entry); + } + } + } + catch { + } + finally + { + archive?.Dispose(); } } @@ -136,5 +164,67 @@ protected override void StopProcessing() { base.StopProcessing(); } + + /// + /// Validate DestinationPath parameter + /// + private void ValidateDestinationPath() + { + // TODO: Add tests cases for conditions below + ErrorCode? errorCode = null; + + var archiveAsFile = new System.IO.FileInfo(DestinationPath); + var archiveAsDirectory = new System.IO.DirectoryInfo(DestinationPath); + + // Check if DestinationPath is an existing file + if (archiveAsFile.Exists) + { + // Throw an error if DestinationPath exists and the cmdlet is not in Update mode or Overwrite is not specified + if (Action == Action.Create) + { + errorCode = ErrorCode.ArchiveExists; + } + // Throw an error if the cmdlet is in Update mode but the archive is read only + if (Action == Action.Update && archiveAsFile.Attributes.HasFlag(FileAttributes.ReadOnly)) + { + errorCode = ErrorCode.ArchiveReadOnly; + } + } + // Check if DestinationPath is an existing directory + else if (archiveAsDirectory.Exists) + { + // Throw an error if DestinationPath exists and the cmdlet is not in Update mode or Overwrite is not specified + if (Action == Action.Create) + { + errorCode = ErrorCode.ArchiveExistsAsDirectory; + } + // Throw an error if the DestinationPath is a directory and the cmdlet is in Update mode + if (Action == Action.Update) + { + errorCode = ErrorCode.ArchiveExistsAsDirectory; + } + // Throw an error if the DestinationPath is a directory with at least item and the cmdlet is in Overwrite mode + if (Action == Action.Overwrite && archiveAsDirectory.GetFileSystemInfos().Length > 0) + { + errorCode = ErrorCode.ArchiveIsNonEmptyDirectory; + } + } + // In this case, DestinationPath does not exist + else + { + // Throw an error if DestinationPath does not exist and cmdlet is in Update mode + if (Action == Action.Update) + { + errorCode = ErrorCode.ArchiveDoesNotExist; + } + } + + if (errorCode != null) + { + // Throw an error -- since we are validating DestinationPath, the problem is with DestinationPath + var errorRecord = ErrorMessages.GetErrorRecord(errorCode: errorCode.Value, errorItem: DestinationPath); + ThrowTerminatingError(errorRecord); + } + } } } diff --git a/src/ErrorMessages.cs b/src/ErrorMessages.cs index 0de8761..02c2b1e 100644 --- a/src/ErrorMessages.cs +++ b/src/ErrorMessages.cs @@ -1,28 +1,12 @@ -using System; -using System.Collections.Generic; +using Microsoft.PowerShell.Archive.Localized; +using System; using System.Management.Automation; -using System.Text; namespace Microsoft.PowerShell.Archive { internal static class ErrorMessages { - // TODO: Move error messages to .resx file - internal static string PathNotFoundMessage = "The path {0} could not be found"; - - internal static string DuplicatePathsMessage = "The path(s) {0} have been specified more than once."; - - internal static string InvalidPathMessage = "The path(s) {0} are invalid."; - - internal static string PathResolvesToMultiplePathsMessage = "The path {0} resolves to multiple possible paths."; - - internal static string ArchiveExistsMessage = "The destination path {0} already exists"; - - internal static string ArchiveExistsAsDirectoryMessage = "The destination path {0} is a directory"; - - internal static string ArchiveIsReadOnlyMessage = "The archive at {0} is read-only."; - - internal static ErrorRecord GetErrorRecordForArgumentException(ErrorCode errorCode, string errorItem) + internal static ErrorRecord GetErrorRecord(ErrorCode errorCode, string errorItem) { var errorMsg = String.Format(GetErrorMessage(errorCode: errorCode), errorItem); var exception = new ArgumentException(errorMsg); @@ -33,13 +17,13 @@ internal static string GetErrorMessage(ErrorCode errorCode) { return errorCode switch { - ErrorCode.PathNotFound => PathNotFoundMessage, - ErrorCode.InvalidPath => InvalidPathMessage, - ErrorCode.DuplicatePaths => DuplicatePathsMessage, - ErrorCode.ArchiveExists => ArchiveExistsMessage, - ErrorCode.ArchiveExistsAsDirectory => ArchiveExistsAsDirectoryMessage, - ErrorCode.ArchiveReadOnly => ArchiveIsReadOnlyMessage, - ErrorCode.PathResolvesToMultiplePaths => PathResolvesToMultiplePathsMessage, + ErrorCode.PathNotFound => Messages.PathNotFoundMessage, + ErrorCode.InvalidPath => Messages.InvalidPathMessage, + ErrorCode.DuplicatePaths => Messages.DuplicatePathsMessage, + ErrorCode.ArchiveExists => Messages.ArchiveExistsMessage, + ErrorCode.ArchiveExistsAsDirectory => Messages.ArchiveExistsAsDirectoryMessage, + ErrorCode.ArchiveReadOnly => Messages.ArchiveIsReadOnlyMessage, + ErrorCode.PathResolvesToMultiplePaths => Messages.PathResolvesToMultiplePathsMessage, _ => throw new NotImplementedException("Error code has not been implemented") }; } @@ -47,13 +31,27 @@ internal static string GetErrorMessage(ErrorCode errorCode) internal enum ErrorCode { + // Used when a path does not resolve to a file or directory on the filesystem PathNotFound, + // Used when a path is invalid (e.g., if the path is for a non-filesystem provider) InvalidPath, + // Used when when a path has been supplied to the cmdlet at least twice DuplicatePaths, + // Used when DestinationPath is an existing file ArchiveExists, + // Used when DestinationPath is an existing directory ArchiveExistsAsDirectory, + // Used when DestinationPath is a non-empty directory and Action Overwrite is specified + ArchiveIsNonEmptyDirectory, + // Used when Compress-Archive cmdlet is in Update mode but the archive is read-only ArchiveReadOnly, + // May be removed PathResolvesToMultiplePaths, - ArchiveDoesNotExist + // Used when DestinationPath does not exist and the Compress-Archive cmdlet is in Update mode + ArchiveDoesNotExist, + // Used when Path and DestinationPath are the same + SamePathAndDestinationPath, + // Used when LiteralPath and DestinationPath are the same + SameLiteralPathAndDestinationPath } } diff --git a/src/IArchive.cs b/src/IArchive.cs index 2bd55f1..97fe175 100644 --- a/src/IArchive.cs +++ b/src/IArchive.cs @@ -15,7 +15,7 @@ internal interface IArchive: IDisposable // Add a file or folder to the archive. The entry name of the added item in the // will be ArchiveEntry.Name. // Throws an exception if the archive is in read mode. - internal void AddFilesytemEntry(ArchiveEntry entry); + internal void AddFilesytemEntry(ArchiveAddition entry); // Get the entries in the archive. // Throws an exception if the archive is in create mode. diff --git a/src/Localized/Messages.Designer.cs b/src/Localized/Messages.Designer.cs new file mode 100644 index 0000000..dfbbfe7 --- /dev/null +++ b/src/Localized/Messages.Designer.cs @@ -0,0 +1,144 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.PowerShell.Archive.Localized { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Messages { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Messages() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.PowerShell.Archive.Localized.Messages", typeof(Messages).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to . + /// + internal static string ArchiveExistsAsDirectoryMessage { + get { + return ResourceManager.GetString("ArchiveExistsAsDirectoryMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to . + /// + internal static string ArchiveExistsMessage { + get { + return ResourceManager.GetString("ArchiveExistsMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to . + /// + internal static string ArchiveIsReadOnlyMessage { + get { + return ResourceManager.GetString("ArchiveIsReadOnlyMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to . + /// + internal static string DuplicatePathsMessage { + get { + return ResourceManager.GetString("DuplicatePathsMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to . + /// + internal static string GetResolvedPathFromPSPathProviderReturnedNullMessage { + get { + return ResourceManager.GetString("GetResolvedPathFromPSPathProviderReturnedNullMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to . + /// + internal static string InvalidPathMessage { + get { + return ResourceManager.GetString("InvalidPathMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to . + /// + internal static string NoItemsToAddWarning { + get { + return ResourceManager.GetString("NoItemsToAddWarning", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to . + /// + internal static string PathNotFoundMessage { + get { + return ResourceManager.GetString("PathNotFoundMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to . + /// + internal static string PathResolvesToMultiplePathsMessage { + get { + return ResourceManager.GetString("PathResolvesToMultiplePathsMessage", resourceCulture); + } + } + } +} diff --git a/src/Localized/Messages.en-US.resx b/src/Localized/Messages.en-US.resx new file mode 100644 index 0000000..f544e04 --- /dev/null +++ b/src/Localized/Messages.en-US.resx @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The destination path {0} is a directory. + + + The destination path {0} already exists. + + + The archive at {0} is read-only. + + + The path(s) {0} have been specified more than once. + + + The path(s) {0} are invalid. + + + The path {0} could not be found. + + + The path {0} resolves to multiple possible paths. + + \ No newline at end of file diff --git a/src/Localized/Messages.resx b/src/Localized/Messages.resx new file mode 100644 index 0000000..650a593 --- /dev/null +++ b/src/Localized/Messages.resx @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Microsoft.PowerShell.Archive.csproj b/src/Microsoft.PowerShell.Archive.csproj index 4e89a6d..82a956d 100644 --- a/src/Microsoft.PowerShell.Archive.csproj +++ b/src/Microsoft.PowerShell.Archive.csproj @@ -3,10 +3,26 @@ netstandard2.1 enable + en-US + + + True + True + Messages.resx + + + + + + ResXFileCodeGenerator + Messages.Designer.cs + + + diff --git a/src/PathHelper.cs b/src/PathHelper.cs index 2d81ca2..fffbdc1 100644 --- a/src/PathHelper.cs +++ b/src/PathHelper.cs @@ -1,4 +1,5 @@ -using System; +using Microsoft.PowerShell.Archive.Localized; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -19,122 +20,169 @@ internal PathHelper(PSCmdlet cmdlet) _cmdlet = cmdlet; } - internal List GetEntryRecordsForPath(string[] paths, bool literalPath) + /// + /// Get a list of ArchiveAddition objects from an array of paths depending on whether we want to use the path literally or not. + /// + /// An array of paths, relative or absolute -- they do not necessarily have to be fully qualifed paths. + /// If true, wildcard characters in each path are expanded. If false, wildcard characters are not expanded. + /// + internal List GetArchiveAdditionsForPath(string[] paths, bool literalPath) { if (literalPath) return GetArchiveEntriesForLiteralPath(paths); else return GetArchiveEntriesForNonLiteralPaths(paths); } - private List GetArchiveEntriesForNonLiteralPaths(string[] paths) + /// + /// Get a list of ArchiveAddition objects from an array of paths by expanding wildcard characters in each path (if found). + /// + /// See above + /// See summary + private List GetArchiveEntriesForNonLiteralPaths(string[] paths) { - List entries = new List(); + List additions = new List(); //Used to keep track of non-filesystem paths HashSet nonfilesystemPaths = new HashSet(); foreach (var path in paths) { - //Resolve the path - var resolvedPaths = GetResolvedProviderPathFromPSPath(path, out var providerInfo, mustExist: true); + // Resolve the path + // GetResolvedProviderPathFromPSPath should not return null even if the path does not exist, but if it does, something went horribly wrong, so throw an exception + var resolvedPaths = GetResolvedProviderPathFromPSPath(path, out var providerInfo, mustExist: true) ?? throw new InvalidOperationException(message: Messages.GetResolvedPathFromPSPathProviderReturnedNullMessage); - //Check if the path if from the filesystem + // Check if the path if from the filesystem if (providerInfo?.Name != "FileSystem") { - //Add the path to the set of non-filesystem paths + // If not, add the path to the set of non-filesystem paths. We will throw an error later so we can show the user all invalid paths at once nonfilesystemPaths.Add(path); continue; } - //Check if the entered path is relative to the current working directory + // Check if the cmdlet can preserve paths based on path variable bool shouldPreservePathStructure = CanPreservePathStructure(path); - //Go through each resolved path and add it to the list of entries + // Go through each resolved path and add an ArchiveAddition for it to additions for (int i=0; i 0) ThrowInvalidPathError(nonfilesystemPaths); + // If there is at least 1 non-filesystem path, throw an invalid path error + if (nonfilesystemPaths.Count > 0) + { + // Get an error record and throw it + var commaSperatedPaths = String.Join(separator: ',', values: nonfilesystemPaths); + var errorRecord = ErrorMessages.GetErrorRecord(errorCode: ErrorCode.InvalidPath, errorItem: commaSperatedPaths); + _cmdlet.ThrowTerminatingError(errorRecord: errorRecord); + } - //Check for duplicate paths - var duplicates = GetDuplicatePaths(entries); - if (duplicates.Count() > 0) ThrowDuplicatePathsError(duplicates); + // If there are duplicate paths, throw an error + var duplicates = GetDuplicatePaths(additions); + if (duplicates.Count() > 0) + { + // Get an error record and throw it + var commaSperatedPaths = String.Join(separator: ',', values: duplicates); + var errorRecord = ErrorMessages.GetErrorRecord(errorCode: ErrorCode.DuplicatePaths, errorItem: commaSperatedPaths); + _cmdlet.ThrowTerminatingError(errorRecord: errorRecord); + } - return entries; + return additions; } - private List GetArchiveEntriesForLiteralPath(string[] paths) + /// + /// Get a list of ArchiveAddition objects from an array of paths by NOT expanding wildcard characters. + /// + /// + /// + private List GetArchiveEntriesForLiteralPath(string[] paths) { - List entries = new List(); + List additions = new List(); foreach (var path in paths) { - //Get the unresolved path - string unresolvedPath = GetUnresolvedProviderPathFromPSPath(path); - - // TODO: Factor out this part -- adding an entry + // Resolve the path -- gets the fully qualified path + string fullyQualifiedPath = GetUnresolvedProviderPathFromPSPath(path); - //Get the prefix of the path - string prefix = System.IO.Path.GetDirectoryName(unresolvedPath) ?? String.Empty; + // Check if we can preserve the path structure -- this is based on the original path the user entered (not fully qualified) + bool canPreservePathStructure = CanPreservePathStructure(path: path); - // If unresolvedPath is not a file or folder, throw a path not found error - // If it is a folder, add its descendents to the list of ArchiveEntry - if (System.IO.Directory.Exists(unresolvedPath)) - { - //Add directory seperator to end - if (!unresolvedPath.EndsWith(System.IO.Path.DirectorySeparatorChar)) unresolvedPath += System.IO.Path.DirectorySeparatorChar; - AddDescendentEntries(path: unresolvedPath, entries: entries, shouldPreservePathStructure: true); - } else if (!System.IO.File.Exists(unresolvedPath)) - { - ThrowPathNotFoundError(path); - } - - //Add an entry for the item - entries.Add(new ArchiveEntry(name: GetEntryName(path: unresolvedPath, prefix: prefix), fullPath: unresolvedPath)); + // Add an ArchiveAddition for the path to the list of additions + AddAdditionForFullyQualifiedPath(path: fullyQualifiedPath, additions: additions, shouldPreservePathStructure: canPreservePathStructure); } - //Check for duplicate paths - var duplicates = GetDuplicatePaths(entries); - if (duplicates.Count() > 0) ThrowDuplicatePathsError(duplicates); + // If there are duplicate paths, throw an error + var duplicates = GetDuplicatePaths(additions); + if (duplicates.Count() > 0) + { + // Get an error record and throw it + var commaSperatedPaths = String.Join(separator: ',', values: duplicates); + var errorRecord = ErrorMessages.GetErrorRecord(errorCode: ErrorCode.DuplicatePaths, errorItem: commaSperatedPaths); + _cmdlet.ThrowTerminatingError(errorRecord: errorRecord); + } - //Return archive entries - return entries; + // Return archive additions + return additions; } - private void AddEntryForFullyQualifiedPath(string path, List entries, bool shouldPreservePathStructure) + /// + /// Adds an ArchiveAddition object for a path to a list of ArchiveAddition objects + /// + /// The fully qualified path + /// The list where to add the ArchiveAddition object for the path + /// If true, relative path structure will be preserved. If false, relative path structure will NOT be preserved. + private void AddAdditionForFullyQualifiedPath(string path, List additions, bool shouldPreservePathStructure) { - // If unresolvedPath is not a file or folder, throw a path not found error - // If it is a folder, add its descendents to the list of ArchiveEntry + var additionType = ArchiveAddition.ArchiveAdditionType.File; if (System.IO.Directory.Exists(path)) { - //Add directory seperator to end + // Add directory seperator to end if it does not already have it if (!path.EndsWith(System.IO.Path.DirectorySeparatorChar)) path += System.IO.Path.DirectorySeparatorChar; - AddDescendentEntries(path: path, entries: entries, shouldPreservePathStructure: shouldPreservePathStructure); + // Recurse through the child items and add them to additions + AddDescendentEntries(path: path, additions: additions, shouldPreservePathStructure: shouldPreservePathStructure); + additionType = ArchiveAddition.ArchiveAdditionType.Directory; } else if (!System.IO.File.Exists(path)) { - ThrowPathNotFoundError(path); + // Throw an error if the path does not exist + var errorRecord = ErrorMessages.GetErrorRecord(errorCode: ErrorCode.PathNotFound, errorItem: path); + _cmdlet.ThrowTerminatingError(errorRecord: errorRecord); } - //Add an entry for the item - entries.Add(new ArchiveEntry(name: GetEntryName(path: path, shouldPreservePathStructure: shouldPreservePathStructure), fullPath: path)); + // Add an entry for the item + additions.Add(new ArchiveAddition(entryName: GetEntryName(path: path, shouldPreservePathStructure: shouldPreservePathStructure), fullPath: path, type: additionType)); } - private void AddDescendentEntries(string path, List entries, bool shouldPreservePathStructure) + /// + /// Creates an ArchiveAdditon object for each child item of the directory and adds it to a list of ArchiveAddition objects + /// + /// A fully qualifed path referring to a directory + /// Where the ArchiveAddtion object for each child item of the directory will be added + /// See above + private void AddDescendentEntries(string path, List additions, bool shouldPreservePathStructure) { try { var directoryInfo = new System.IO.DirectoryInfo(path); foreach (var childPath in directoryInfo.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)) { - //Add an entry for each child path - entries.Add(new ArchiveEntry(name: GetEntryName(path: childPath.FullName, shouldPreservePathStructure: shouldPreservePathStructure), fullPath: childPath.Name)); + ArchiveAddition.ArchiveAdditionType? type = null; + if (childPath is System.IO.FileInfo) + { + type = ArchiveAddition.ArchiveAdditionType.File; + } + if (childPath is System.IO.DirectoryInfo) + { + type = ArchiveAddition.ArchiveAdditionType.Directory; + } else + { + // Throw a terminating error if the childPath is neither a file or a directory -- seems impossible, but done for safety + // TODO: Throw an error + } + // Add an entry for each child path + + additions.Add(new ArchiveAddition(entryName: GetEntryName(path: childPath.FullName, shouldPreservePathStructure: shouldPreservePathStructure), fullPath: childPath.Name)); } } catch (System.Management.Automation.ItemNotFoundException itemNotFoundException) @@ -186,7 +234,7 @@ private string GetEntryName(string path, string prefix) return entryName; } - private IEnumerable GetDuplicatePaths(List entries) + private IEnumerable GetDuplicatePaths(List entries) { return entries.GroupBy(x => x.FullPath) .Where(group => group.Count() > 1) @@ -268,19 +316,19 @@ internal string ResolveToSingleFullyQualifiedPath(string path) //First, get non-literal path string nonLiteralPath = GetUnresolvedProviderPathFromPSPath(path) ?? throw new ArgumentException($"Path {path} was resolved to null"); - //Second, get literal path + /*//Second, get literal path var literalPaths = GetResolvedProviderPathFromPSPath(path, out var providerInfo, mustExist: false); if (literalPaths != null) { - //Ensure the literal paths came from the filesystem + // Ensure the literal paths came from the filesystem if (providerInfo != null && providerInfo?.Name != "FileSystem") ThrowInvalidPathError(path); - //If there are >1 literalPaths, throw an error + // If there are >1 literalPaths, throw an error if (literalPaths.Count > 1) ThrowResolvesToMultiplePathsError(path); - //If there is one item in literalPaths, compare it to nonLiteralPath + // If there is one item in literalPaths, compare it to nonLiteralPath if (literalPaths[0] != nonLiteralPath) ThrowResolvesToMultiplePathsError(path); - } + }*/ return nonLiteralPath; } diff --git a/src/ZipArchive.cs b/src/ZipArchive.cs index 8a65a6a..6106141 100644 --- a/src/ZipArchive.cs +++ b/src/ZipArchive.cs @@ -36,18 +36,21 @@ public ZipArchive(string archivePath, ArchiveMode mode, System.IO.FileStream arc // If a file is added to the archive when it already contains a folder with the same name, // it is up to the extraction software to deal with it (this is how it's done in other archive software) // TODO: Explain how to add folders to the archive - void IArchive.AddFilesytemEntry(ArchiveEntry entry) + void IArchive.AddFilesytemEntry(ArchiveAddition entry) { if (_mode == ArchiveMode.Read) throw new InvalidOperationException("Cannot add a filesystem entry to an archive in read mode"); - var entryName = entry.Name.Replace(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar); + var entryName = entry.EntryName.Replace(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar); // TODO: Add exception handling for _zipArchive.GetEntry var entryInArchive = (_mode == ArchiveMode.Create) ? null : _zipArchive.GetEntry(entryName); - if (entryName.EndsWith(System.IO.Path.AltDirectorySeparatorChar)) + if (entry.Type == ArchiveAddition.ArchiveAdditionType.Directory) { //Create an entry only // TODO: Add exception handling for CreateEntry + + // TODO: Ensure entryName has / at the end + if (entryInArchive == null) _zipArchive.CreateEntry(entryName); } else From 63490c61b59dd59a79a1b0da05390dc7d3d97598 Mon Sep 17 00:00:00 2001 From: Abdullah Yousuf Date: Tue, 19 Jul 2022 11:37:04 -0700 Subject: [PATCH 16/42] refactored PathHelper class --- src/ErrorMessages.cs | 4 +- src/PathHelper.cs | 343 ++++++++++++++----------------------------- 2 files changed, 111 insertions(+), 236 deletions(-) diff --git a/src/ErrorMessages.cs b/src/ErrorMessages.cs index 02c2b1e..ed5779b 100644 --- a/src/ErrorMessages.cs +++ b/src/ErrorMessages.cs @@ -52,6 +52,8 @@ internal enum ErrorCode // Used when Path and DestinationPath are the same SamePathAndDestinationPath, // Used when LiteralPath and DestinationPath are the same - SameLiteralPathAndDestinationPath + SameLiteralPathAndDestinationPath, + // Used when the user does not have sufficient permissions to access a path + InsufficientPermissionsToAccessPath, } } diff --git a/src/PathHelper.cs b/src/PathHelper.cs index fffbdc1..c1c8c4e 100644 --- a/src/PathHelper.cs +++ b/src/PathHelper.cs @@ -9,12 +9,13 @@ namespace Microsoft.PowerShell.Archive { - //To-do: Add exception handling internal class PathHelper { private PSCmdlet _cmdlet; + private const string FilesystemProviderName = "FileSystem"; + internal PathHelper(PSCmdlet cmdlet) { _cmdlet = cmdlet; @@ -27,17 +28,6 @@ internal PathHelper(PSCmdlet cmdlet) /// If true, wildcard characters in each path are expanded. If false, wildcard characters are not expanded. /// internal List GetArchiveAdditionsForPath(string[] paths, bool literalPath) - { - if (literalPath) return GetArchiveEntriesForLiteralPath(paths); - else return GetArchiveEntriesForNonLiteralPaths(paths); - } - - /// - /// Get a list of ArchiveAddition objects from an array of paths by expanding wildcard characters in each path (if found). - /// - /// See above - /// See summary - private List GetArchiveEntriesForNonLiteralPaths(string[] paths) { List additions = new List(); @@ -46,26 +36,14 @@ private List GetArchiveEntriesForNonLiteralPaths(string[] paths foreach (var path in paths) { - // Resolve the path - // GetResolvedProviderPathFromPSPath should not return null even if the path does not exist, but if it does, something went horribly wrong, so throw an exception - var resolvedPaths = GetResolvedProviderPathFromPSPath(path, out var providerInfo, mustExist: true) ?? throw new InvalidOperationException(message: Messages.GetResolvedPathFromPSPathProviderReturnedNullMessage); + // Based on the value of literalPath, call the appropriate method - // Check if the path if from the filesystem - if (providerInfo?.Name != "FileSystem") + if (literalPath) { - // If not, add the path to the set of non-filesystem paths. We will throw an error later so we can show the user all invalid paths at once - nonfilesystemPaths.Add(path); - continue; - } - - // Check if the cmdlet can preserve paths based on path variable - bool shouldPreservePathStructure = CanPreservePathStructure(path); - - // Go through each resolved path and add an ArchiveAddition for it to additions - for (int i=0; i GetArchiveEntriesForNonLiteralPaths(string[] paths } /// - /// Get a list of ArchiveAddition objects from an array of paths by NOT expanding wildcard characters. + /// Resolves a user-entered path while expanding wildcards, creates an ArchiveAddition object for it, and its to the list of ArchiveAddition objects /// - /// - /// - private List GetArchiveEntriesForLiteralPath(string[] paths) + /// + /// + /// + private void AddArchiveAdditionForUserEnteredNonLiteralPath(string path, List archiveAdditions, HashSet nonfilesystemPaths) { - List additions = new List(); + // Resolve the path -- I don't think we need to handle exceptions here as no special behavior occurs when an exception occurs + var resolvedPaths = _cmdlet.SessionState.Path.GetResolvedProviderPathFromPSPath(path: path, provider: out var providerInfo); - foreach (var path in paths) + // Check if the path if from the filesystem + if (providerInfo?.Name != FilesystemProviderName) { - // Resolve the path -- gets the fully qualified path - string fullyQualifiedPath = GetUnresolvedProviderPathFromPSPath(path); + // If not, add the path to the set of non-filesystem paths. We will throw an error later so we can show the user all invalid paths at once + nonfilesystemPaths.Add(path); + return; + } - // Check if we can preserve the path structure -- this is based on the original path the user entered (not fully qualified) - bool canPreservePathStructure = CanPreservePathStructure(path: path); + // Check if the cmdlet can preserve paths based on path variable + bool shouldPreservePathStructure = CanPreservePathStructure(path); - // Add an ArchiveAddition for the path to the list of additions - AddAdditionForFullyQualifiedPath(path: fullyQualifiedPath, additions: additions, shouldPreservePathStructure: canPreservePathStructure); + // Go through each resolved path and add an ArchiveAddition for it to additions + for (int i = 0; i < resolvedPaths.Count; i++) + { + var resolvedPath = resolvedPaths[i]; + AddAdditionForFullyQualifiedPath(path: resolvedPath, additions: additions, shouldPreservePathStructure: shouldPreservePathStructure); } + } - // If there are duplicate paths, throw an error - var duplicates = GetDuplicatePaths(additions); - if (duplicates.Count() > 0) + /// + /// Resolves a user-entered path without expanding wildcards, creates an ArchiveAddition object for it, and its to the list of ArchiveAddition objects + /// + /// + /// + /// + private void AddArchiveAdditionForUserEnteredLiteralPath(string path, List archiveAdditions, HashSet nonfilesystemPaths) + { + // Resolve the path -- gets the fully qualified path + // I don't think we need to handle exceptions for the call below as the cmdlet does not have any special behaviors when the call below throws an exception + string fullyQualifiedPath = _cmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath(path, out var providerInfo, out var psDriveInfo); + + // Check if the path is from the filesystem + if (providerInfo.Name != FilesystemProviderName) { - // Get an error record and throw it - var commaSperatedPaths = String.Join(separator: ',', values: duplicates); - var errorRecord = ErrorMessages.GetErrorRecord(errorCode: ErrorCode.DuplicatePaths, errorItem: commaSperatedPaths); - _cmdlet.ThrowTerminatingError(errorRecord: errorRecord); + nonfilesystemPaths.Add(path); + return; } - // Return archive additions - return additions; + // Check if we can preserve the path structure -- this is based on the original path the user entered (not fully qualified) + bool canPreservePathStructure = CanPreservePathStructure(path: path); + + // Add an ArchiveAddition for the path to the list of additions + AddAdditionForFullyQualifiedPath(path: fullyQualifiedPath, additions: archiveAdditions, shouldPreservePathStructure: canPreservePathStructure); } /// @@ -167,43 +166,49 @@ private void AddDescendentEntries(string path, List additions, var directoryInfo = new System.IO.DirectoryInfo(path); foreach (var childPath in directoryInfo.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)) { - ArchiveAddition.ArchiveAdditionType? type = null; - if (childPath is System.IO.FileInfo) - { - type = ArchiveAddition.ArchiveAdditionType.File; - } + // childPath can either be a file or directory, and nothing else + ArchiveAddition.ArchiveAdditionType type = ArchiveAddition.ArchiveAdditionType.File; if (childPath is System.IO.DirectoryInfo) { type = ArchiveAddition.ArchiveAdditionType.Directory; - } else - { - // Throw a terminating error if the childPath is neither a file or a directory -- seems impossible, but done for safety - // TODO: Throw an error } + // Add an entry for each child path - - additions.Add(new ArchiveAddition(entryName: GetEntryName(path: childPath.FullName, shouldPreservePathStructure: shouldPreservePathStructure), fullPath: childPath.Name)); + var entryName = GetEntryName(path: childPath.FullName, shouldPreservePathStructure: shouldPreservePathStructure); + additions.Add(new ArchiveAddition(entryName: entryName, fullPath: childPath.Name, type: type)); } + } + // Throw a terminating error if a securityException occurs + catch (System.Security.SecurityException) + { + var errorRecord = ErrorMessages.GetErrorRecord(errorCode: ErrorCode.InsufficientPermissionsToAccessPath, errorItem: path); + _cmdlet.ThrowTerminatingError(errorRecord); } - catch (System.Management.Automation.ItemNotFoundException itemNotFoundException) + // Throw a terminating error if a directoryNotFoundException occurs + catch (System.IO.DirectoryNotFoundException) { - //Throw a path not found error - ErrorRecord errorRecord = new ErrorRecord(itemNotFoundException, "PathNotFound", ErrorCategory.InvalidArgument, path); + var errorRecord = ErrorMessages.GetErrorRecord(errorCode: ErrorCode.PathNotFound, errorItem: path); _cmdlet.ThrowTerminatingError(errorRecord); } } + /// + /// Get the archive path from a fully qualified path + /// + /// A fully qualified path + /// + /// private string GetEntryName(string path, bool shouldPreservePathStructure) { - //If the path is relative to the current working directory, return the relative path as name - if (shouldPreservePathStructure && IsPathRelativeToCurrentWorkingDirectory(path, out var relativePath)) + // If the path is relative to the current working directory, return the relative path as name + if (shouldPreservePathStructure && TryGetPathRelativeToCurrentWorkingDirectory(path, out var relativePath)) { return relativePath; } - //Otherwise, return the name of the directory or file + // Otherwise, return the name of the directory or file if (path.EndsWith(System.IO.Path.DirectorySeparatorChar)) { - //Get substring from second-last directory seperator char till end + // Get substring from second-last directory seperator char till end int secondLastIndex = path.LastIndexOf(System.IO.Path.DirectorySeparatorChar, path.Length - 2); if (secondLastIndex == -1) return path; else return path.Substring(secondLastIndex + 1); @@ -214,191 +219,59 @@ private string GetEntryName(string path, bool shouldPreservePathStructure) } } - private string GetEntryName(string path, string prefix) - { - if (prefix == String.Empty) return path; - - //If the path does not start with the prefix, throw an exception - if (!path.StartsWith(prefix)) - { - throw new ArgumentException($"{path} does not begin with {prefix}"); - } - - if (path.Length <= prefix.Length) throw new ArgumentException($"The length of {path} is shorter than or equal to the length of {prefix}"); - - string entryName = path.Substring(prefix.Length + 1); - - //Normalize entryName to use forwardslashes instead of backslashes - entryName = entryName.Replace(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar); - - return entryName; - } - - private IEnumerable GetDuplicatePaths(List entries) + /// + /// Get the duplicate fully qualified paths from a list of ArchiveAdditions + /// + /// + /// + private IEnumerable GetDuplicatePaths(List additions) { - return entries.GroupBy(x => x.FullPath) + return additions.GroupBy(x => x.FullPath) .Where(group => group.Count() > 1) .Select(x => x.Key); } - private System.Collections.ObjectModel.Collection? GetResolvedProviderPathFromPSPath(string path, out ProviderInfo? providerInfo, bool mustExist) - { - try - { - ProviderInfo info; - var resolvedPaths = _cmdlet.GetResolvedProviderPathFromPSPath(path, out info); - providerInfo = info; - return resolvedPaths; - } - catch (ProviderNotFoundException providerNotFoundException) - { - //Throw an invalid path error - ThrowInvalidPathError(path, providerNotFoundException); - } - catch (System.Management.Automation.DriveNotFoundException driveNotFoundException) - { - ThrowInvalidPathError(path, driveNotFoundException); - } - catch (System.Management.Automation.ProviderInvocationException providerInvocationException) - { - ThrowInvalidPathError(path, providerInvocationException); - } - catch (NotSupportedException providerNotSupportedException) - { - ThrowInvalidPathError(path, providerNotSupportedException); - } - catch (InvalidOperationException invalidOperationException) - { - ThrowInvalidPathError(path, invalidOperationException); - } - catch (ItemNotFoundException itemNotFoundException) - { - if (mustExist) ThrowPathNotFoundError(path, itemNotFoundException); - } - providerInfo = null; - return null; - } - - private string? GetUnresolvedProviderPathFromPSPath(string path) - { - try - { - return _cmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath(path); - } - catch (ProviderNotFoundException providerNotFoundException) - { - //Throw an invalid path error - ThrowInvalidPathError(path, providerNotFoundException); - } - catch (System.Management.Automation.DriveNotFoundException driveNotFoundException) - { - ThrowInvalidPathError(path, driveNotFoundException); - } - catch (System.Management.Automation.ProviderInvocationException providerInvocationException) - { - ThrowInvalidPathError(path, providerInvocationException); - } - catch (NotSupportedException providerNotSupportedException) - { - ThrowInvalidPathError(path, providerNotSupportedException); - } - catch (InvalidOperationException invalidOperationException) - { - ThrowInvalidPathError(path, invalidOperationException); - } - return null; - } - - // TODO: Add directory seperator char at end + /// + /// Resolve a path that may contain wildcard characters and could be a literal or non-literal path to a single fully qualified path. + /// + /// + /// + /// internal string ResolveToSingleFullyQualifiedPath(string path) { - //path can be literal or non-literal - //First, get non-literal path - string nonLiteralPath = GetUnresolvedProviderPathFromPSPath(path) ?? throw new ArgumentException($"Path {path} was resolved to null"); - - /*//Second, get literal path - var literalPaths = GetResolvedProviderPathFromPSPath(path, out var providerInfo, mustExist: false); - if (literalPaths != null) - { - // Ensure the literal paths came from the filesystem - if (providerInfo != null && providerInfo?.Name != "FileSystem") ThrowInvalidPathError(path); - - // If there are >1 literalPaths, throw an error - if (literalPaths.Count > 1) ThrowResolvesToMultiplePathsError(path); - - // If there is one item in literalPaths, compare it to nonLiteralPath - if (literalPaths[0] != nonLiteralPath) ThrowResolvesToMultiplePathsError(path); - }*/ - - return nonLiteralPath; - } - - private void ThrowPathNotFoundError(string path) - { - var errorMsg = String.Format(ErrorMessages.PathNotFoundMessage, path); - var exception = new System.InvalidOperationException(errorMsg); - var errorRecord = new ErrorRecord(exception, "PathNotFound", ErrorCategory.InvalidArgument, path); - _cmdlet.ThrowTerminatingError(errorRecord); - } - - private void ThrowPathNotFoundError(string path, Exception innerException) - { - var errorMsg = String.Format(ErrorMessages.PathNotFoundMessage, path); - var exception = new System.ArgumentException(errorMsg); - var errorRecord = new ErrorRecord(exception, "PathNotFound", ErrorCategory.InvalidArgument, path); - _cmdlet.ThrowTerminatingError(errorRecord); - } - - private void ThrowInvalidPathError(HashSet paths) - { - string commaSeperatedPaths = String.Join(',', paths); - var errorMsg = String.Format(ErrorMessages.InvalidPathMessage, commaSeperatedPaths); - var exception = new System.InvalidOperationException(errorMsg); - var errorRecord = new ErrorRecord(exception, "InvalidPath", ErrorCategory.InvalidArgument, commaSeperatedPaths); - _cmdlet.ThrowTerminatingError(errorRecord); - } - - private void ThrowInvalidPathError(string path) - { - var errorMsg = String.Format(ErrorMessages.InvalidPathMessage, path); - var exception = new System.ArgumentException(errorMsg); - var errorRecord = new ErrorRecord(exception, "InvalidPath", ErrorCategory.InvalidArgument, path); - _cmdlet.ThrowTerminatingError(errorRecord); - } + // Currently, all this function does is return the literal fully qualified path of a path - private void ThrowInvalidPathError(string path, Exception innerException) - { - var errorMsg = String.Format(ErrorMessages.InvalidPathMessage, path); - var exception = new System.ArgumentException(errorMsg, innerException); - var errorRecord = new ErrorRecord(exception, "InvalidPath", ErrorCategory.InvalidArgument, path); - _cmdlet.ThrowTerminatingError(errorRecord); - } + // First, get non-literal path + string fullyQualifiedPath = _cmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath(path, out var providerInfo, out var psDriveInfo); - private void ThrowDuplicatePathsError(IEnumerable paths) - { - string commaSeperatedPaths = String.Join(',', paths); - var errorMsg = String.Format(ErrorMessages.DuplicatePathsMessage, commaSeperatedPaths); - var exception = new System.InvalidOperationException(errorMsg); - var errorRecord = new ErrorRecord(exception, "DuplicatePathFound", ErrorCategory.InvalidArgument, commaSeperatedPaths); - _cmdlet.ThrowTerminatingError(errorRecord); - } + // If the path is not from the filesystem, throw an error + if (providerInfo.Name != FilesystemProviderName) + { + var errorRecord = ErrorMessages.GetErrorRecord(errorCode: ErrorCode.InvalidPath, errorItem: path); + _cmdlet.ThrowTerminatingError(errorRecord); + } - private void ThrowResolvesToMultiplePathsError(string path) - { - var errorMsg = String.Format(ErrorMessages.PathResolvesToMultiplePathsMessage, path); - var exception = new System.ArgumentException(errorMsg); - var errorRecord = new ErrorRecord(exception, "DuplicatePathFound", ErrorCategory.InvalidArgument, path); - _cmdlet.ThrowTerminatingError(errorRecord); + return fullyQualifiedPath; } + /// + /// Determines if the relative path structure can be preserved + /// + /// + /// private bool CanPreservePathStructure(string path) { return System.IO.Path.IsPathRooted(path); } - private bool IsPathRelativeToCurrentWorkingDirectory(string path, out string relativePath) + /// + /// Tries to get a path relative to the current working directory as long as the relative path does not contain ".." + /// + /// + /// + /// + private bool TryGetPathRelativeToCurrentWorkingDirectory(string path, out string relativePath) { - // TODO: Add exception handling relativePath = System.IO.Path.GetRelativePath(_cmdlet.SessionState.Path.CurrentFileSystemLocation.Path, path); return !relativePath.Contains(".."); } From 7986a1ea0dd0833508a58eb55776d41378c7e575 Mon Sep 17 00:00:00 2001 From: Abdullah Yousuf Date: Tue, 19 Jul 2022 12:11:49 -0700 Subject: [PATCH 17/42] worked on automatically determining archive format based on DestinationPath's extension --- src/ArchiveFactory.cs | 20 ++++++++++- src/ArchiveFormat.cs | 2 ++ src/ArchiveMode.cs | 2 +- src/CompressArchiveCommand.cs | 19 +++++++++- src/IArchive.cs | 2 +- src/Localized/Messages.Designer.cs | 9 +++++ src/Localized/Messages.resx | 3 ++ src/ZipArchive.cs | 58 ++++++++++++++++-------------- 8 files changed, 84 insertions(+), 31 deletions(-) diff --git a/src/ArchiveFactory.cs b/src/ArchiveFactory.cs index c728dc8..f803f4a 100644 --- a/src/ArchiveFactory.cs +++ b/src/ArchiveFactory.cs @@ -14,7 +14,7 @@ internal static IArchive GetArchive(ArchiveFormat format, string archivePath, Ar { ArchiveMode.Create => new System.IO.FileStream(archivePath, mode: System.IO.FileMode.CreateNew, access: System.IO.FileAccess.Write, share: System.IO.FileShare.None), ArchiveMode.Update => new System.IO.FileStream(archivePath, mode: System.IO.FileMode.Open, access: System.IO.FileAccess.ReadWrite, share: System.IO.FileShare.None), - ArchiveMode.Read => new System.IO.FileStream(archivePath, mode: System.IO.FileMode.Open, access: System.IO.FileAccess.Read, share: System.IO.FileShare.Read), + ArchiveMode.Extract => new System.IO.FileStream(archivePath, mode: System.IO.FileMode.Open, access: System.IO.FileAccess.Read, share: System.IO.FileShare.Read), // TODO: Add message to exception _ => throw new NotImplementedException() }; @@ -27,5 +27,23 @@ internal static IArchive GetArchive(ArchiveFormat format, string archivePath, Ar _ => throw new NotImplementedException() }; } + + internal static bool TryGetArchiveFormatForPath(string path, out ArchiveFormat? archiveFormat) + { + archiveFormat = null; + if (path.EndsWith(".zip")) + { + archiveFormat = ArchiveFormat.zip; + } + if (path.EndsWith(".tar")) + { + archiveFormat = ArchiveFormat.tar; + } + if (path.EndsWith(".tar.gz") || path.EndsWith(".tgz")) + { + archiveFormat = ArchiveFormat.tgz; + } + return archiveFormat is null; + } } } diff --git a/src/ArchiveFormat.cs b/src/ArchiveFormat.cs index 51ea3a4..783c7b5 100644 --- a/src/ArchiveFormat.cs +++ b/src/ArchiveFormat.cs @@ -7,5 +7,7 @@ namespace Microsoft.PowerShell.Archive public enum ArchiveFormat { zip, + tar, + tgz } } diff --git a/src/ArchiveMode.cs b/src/ArchiveMode.cs index 126a30d..6f32220 100644 --- a/src/ArchiveMode.cs +++ b/src/ArchiveMode.cs @@ -8,6 +8,6 @@ internal enum ArchiveMode { Create, Update, - Read + Extract } } diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index 91435bf..ed1c496 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -75,11 +75,28 @@ public CompressArchiveCommand() protected override void BeginProcessing() { - // TODO: Add exception handling DestinationPath = _pathHelper.ResolveToSingleFullyQualifiedPath(DestinationPath); // Validate DestinationPath ValidateDestinationPath(); + + // We want to get the appropriate archive format based on the destination path or give a warning + + // If the user did not specify which archive format to use, try to determine it automatically + if (Format is null) + { + // Try and get the suitable archive format based on DestinationPath + if (ArchiveFactory.TryGetArchiveFormatForPath(path: DestinationPath, archiveFormat: out var archiveFormat)) { + Format = archiveFormat; + } + // If the archive format could not be determined, use zip by default and emit a warning + else + { + var warningMsg = String.Format(Messages.ArchiveFormatCouldNotBeDeterminedWarning, DestinationPath); + WriteWarning(warningMsg); + Format = ArchiveFormat.zip; + } + } } protected override void ProcessRecord() diff --git a/src/IArchive.cs b/src/IArchive.cs index 97fe175..c7cb82e 100644 --- a/src/IArchive.cs +++ b/src/IArchive.cs @@ -10,7 +10,7 @@ internal interface IArchive: IDisposable internal ArchiveMode Mode { get; } // Get the fully qualified path of the archive - internal string ArchivePath { get; } + internal string Path { get; } // Add a file or folder to the archive. The entry name of the added item in the // will be ArchiveEntry.Name. diff --git a/src/Localized/Messages.Designer.cs b/src/Localized/Messages.Designer.cs index dfbbfe7..b941fa9 100644 --- a/src/Localized/Messages.Designer.cs +++ b/src/Localized/Messages.Designer.cs @@ -78,6 +78,15 @@ internal static string ArchiveExistsMessage { } } + /// + /// Looks up a localized string similar to . + /// + internal static string ArchiveFormatCouldNotBeDeterminedWarning { + get { + return ResourceManager.GetString("ArchiveFormatCouldNotBeDeterminedWarning", resourceCulture); + } + } + /// /// Looks up a localized string similar to . /// diff --git a/src/Localized/Messages.resx b/src/Localized/Messages.resx index 650a593..49acbac 100644 --- a/src/Localized/Messages.resx +++ b/src/Localized/Messages.resx @@ -123,6 +123,9 @@ + + + diff --git a/src/ZipArchive.cs b/src/ZipArchive.cs index 6106141..132b33d 100644 --- a/src/ZipArchive.cs +++ b/src/ZipArchive.cs @@ -19,9 +19,11 @@ internal class ZipArchive : IArchive private System.IO.Compression.CompressionLevel _compressionLevel; + private const char ZipArchiveDirectoryPathTerminator = '/'; + ArchiveMode IArchive.Mode => _mode; - string IArchive.ArchivePath => _archivePath; + string IArchive.Path => _archivePath; public ZipArchive(string archivePath, ArchiveMode mode, System.IO.FileStream archiveStream, CompressionLevel compressionLevel) { @@ -34,35 +36,47 @@ public ZipArchive(string archivePath, ArchiveMode mode, System.IO.FileStream arc } // If a file is added to the archive when it already contains a folder with the same name, - // it is up to the extraction software to deal with it (this is how it's done in other archive software) - // TODO: Explain how to add folders to the archive - void IArchive.AddFilesytemEntry(ArchiveAddition entry) + // it is up to the extraction software to deal with it (this is how it's done in other archive software). + // The .NET API differentiates a file and folder based on the last character being '/'. In other words, if the last character in a path is '/', it is treated as a folder. + // Otherwise, the .NET API treats the path as a file. + void IArchive.AddFilesytemEntry(ArchiveAddition addition) { - if (_mode == ArchiveMode.Read) throw new InvalidOperationException("Cannot add a filesystem entry to an archive in read mode"); + if (_mode == ArchiveMode.Extract) throw new InvalidOperationException("Cannot add a filesystem entry to an archive in read mode"); - var entryName = entry.EntryName.Replace(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar); + var entryName = addition.EntryName.Replace(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar); - // TODO: Add exception handling for _zipArchive.GetEntry - var entryInArchive = (_mode == ArchiveMode.Create) ? null : _zipArchive.GetEntry(entryName); - if (entry.Type == ArchiveAddition.ArchiveAdditionType.Directory) + // If the archive has an entry with the same name as addition.EntryName, then get it, so it can be replaced if necessary + System.IO.Compression.ZipArchiveEntry? entryInArchive = null; + if (_mode != ArchiveMode.Create) { - //Create an entry only - // TODO: Add exception handling for CreateEntry - - // TODO: Ensure entryName has / at the end + // TODO: Add exception handling for _zipArchive.GetEntry + entryInArchive = _zipArchive.GetEntry(entryName); + } - if (entryInArchive == null) _zipArchive.CreateEntry(entryName); + // If the addition is a folder, only create the entry in the archive -- nothing else is needed + if (addition.Type == ArchiveAddition.ArchiveAdditionType.Directory) + { + // If the archive does not have an entry with the same name, then add an entry for the directory + if (entryInArchive == null) + { + // Ensure addition.entryName has '/' at the end + if (!addition.EntryName.EndsWith(ZipArchiveDirectoryPathTerminator)) + { + addition.EntryName += ZipArchiveDirectoryPathTerminator; + } + _zipArchive.CreateEntry(entryName); + } } else { + // If the archive already has an entry with the same name as addition.EntryName, delete it if (entryInArchive != null) { entryInArchive.Delete(); } // TODO: Add exception handling - _zipArchive.CreateEntryFromFile(sourceFileName: entry.FullPath, entryName: entryName, compressionLevel: _compressionLevel); + _zipArchive.CreateEntryFromFile(sourceFileName: addition.FullPath, entryName: entryName, compressionLevel: _compressionLevel); } - } string[] IArchive.GetEntries() @@ -81,7 +95,7 @@ private System.IO.Compression.ZipArchiveMode ConvertToZipArchiveMode(ArchiveMode { case ArchiveMode.Create: return System.IO.Compression.ZipArchiveMode.Create; case ArchiveMode.Update: return System.IO.Compression.ZipArchiveMode.Update; - case ArchiveMode.Read: return System.IO.Compression.ZipArchiveMode.Read; + case ArchiveMode.Extract: return System.IO.Compression.ZipArchiveMode.Read; default: return System.IO.Compression.ZipArchiveMode.Update; } } @@ -92,24 +106,14 @@ protected virtual void Dispose(bool disposing) { if (disposing) { - // TODO: dispose managed state (managed objects) _zipArchive.Dispose(); _archiveStream.Dispose(); } - // TODO: free unmanaged resources (unmanaged objects) and override finalizer - // TODO: set large fields to null disposedValue = true; } } - // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources - // ~ZipArchive() - // { - // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - // Dispose(disposing: false); - // } - public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method From e062f64fce325466dc5557ac5cb32788a2469d25 Mon Sep 17 00:00:00 2001 From: Abdullah Yousuf Date: Tue, 19 Jul 2022 12:28:29 -0700 Subject: [PATCH 18/42] added support for determining archive format automatically based on DestinationPath's extension --- src/CompressArchiveCommand.cs | 28 +++++++++++++++++++--------- src/Localized/Messages.Designer.cs | 9 +++++++++ src/Localized/Messages.resx | 3 +++ 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index ed1c496..106ae05 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -61,7 +61,8 @@ public class CompressArchiveCommand : PSCmdlet [ValidateNotNullOrEmpty] public System.IO.Compression.CompressionLevel CompressionLevel { get; set; } = System.IO.Compression.CompressionLevel.Optimal; - public ArchiveFormat Format { get; set; } = ArchiveFormat.zip; + [Parameter()] + public ArchiveFormat? Format { get; set; } = null; private List? _sourcePaths; @@ -80,23 +81,32 @@ protected override void BeginProcessing() // Validate DestinationPath ValidateDestinationPath(); - // We want to get the appropriate archive format based on the destination path or give a warning - + // Check if cmdlet is able to determine the format of the archive based on the extension of DestinationPath + bool ableToDetermineArchiveFormat = ArchiveFactory.TryGetArchiveFormatForPath(path: DestinationPath, archiveFormat: out var archiveFormat); // If the user did not specify which archive format to use, try to determine it automatically if (Format is null) { - // Try and get the suitable archive format based on DestinationPath - if (ArchiveFactory.TryGetArchiveFormatForPath(path: DestinationPath, archiveFormat: out var archiveFormat)) { + if (ableToDetermineArchiveFormat) + { Format = archiveFormat; - } - // If the archive format could not be determined, use zip by default and emit a warning - else + } else { + // If the archive format could not be determined, use zip by default and emit a warning var warningMsg = String.Format(Messages.ArchiveFormatCouldNotBeDeterminedWarning, DestinationPath); WriteWarning(warningMsg); Format = ArchiveFormat.zip; } } + // If the user did specify which archive format to use, emit a warning if DestinationPath does not match the chosen archive format + else + { + if (archiveFormat is null || archiveFormat.Value != Format.Value) + { + var warningMsg = String.Format(Messages.ArchiveExtensionDoesNotMatchArchiveFormatWarning, DestinationPath); + WriteWarning(warningMsg); + } + } + } protected override void ProcessRecord() @@ -155,7 +165,7 @@ protected override void EndProcessing() if (ShouldProcess(target: DestinationPath, action: "Create")) { // Create an archive -- this is where we will switch between different types of archives - archive = ArchiveFactory.GetArchive(format: Format, archivePath: DestinationPath, archiveMode: archiveMode, compressionLevel: CompressionLevel); + archive = ArchiveFactory.GetArchive(format: Format ?? ArchiveFormat.zip, archivePath: DestinationPath, archiveMode: archiveMode, compressionLevel: CompressionLevel); } // TODO: Update progress diff --git a/src/Localized/Messages.Designer.cs b/src/Localized/Messages.Designer.cs index b941fa9..6878153 100644 --- a/src/Localized/Messages.Designer.cs +++ b/src/Localized/Messages.Designer.cs @@ -78,6 +78,15 @@ internal static string ArchiveExistsMessage { } } + /// + /// Looks up a localized string similar to . + /// + internal static string ArchiveExtensionDoesNotMatchArchiveFormatWarning { + get { + return ResourceManager.GetString("ArchiveExtensionDoesNotMatchArchiveFormatWarning", resourceCulture); + } + } + /// /// Looks up a localized string similar to . /// diff --git a/src/Localized/Messages.resx b/src/Localized/Messages.resx index 49acbac..78469d6 100644 --- a/src/Localized/Messages.resx +++ b/src/Localized/Messages.resx @@ -123,6 +123,9 @@ + + + From 8fd551e2958118dbd7168c21a79c7545148af0dc Mon Sep 17 00:00:00 2001 From: Abdullah Yousuf Date: Tue, 19 Jul 2022 13:05:30 -0700 Subject: [PATCH 19/42] fixed a bug with archive format warning, added TarArchive file --- src/ArchiveFactory.cs | 2 +- src/CompressArchiveCommand.cs | 3 ++ src/Localized/Messages.Designer.cs | 9 +++++ src/Localized/Messages.resx | 3 ++ src/PathHelper.cs | 2 +- src/TarArchive.cs | 56 ++++++++++++++++++++++++++++++ 6 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 src/TarArchive.cs diff --git a/src/ArchiveFactory.cs b/src/ArchiveFactory.cs index f803f4a..7d22b1a 100644 --- a/src/ArchiveFactory.cs +++ b/src/ArchiveFactory.cs @@ -43,7 +43,7 @@ internal static bool TryGetArchiveFormatForPath(string path, out ArchiveFormat? { archiveFormat = ArchiveFormat.tgz; } - return archiveFormat is null; + return archiveFormat != null; } } } diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index 106ae05..d163cf0 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -96,6 +96,9 @@ protected override void BeginProcessing() WriteWarning(warningMsg); Format = ArchiveFormat.zip; } + // Write a verbose message saying that Format is not specified and a format was determined automatically + string verboseMessage = String.Format(Messages.ArchiveFormatDeterminedVerboseMessage, Format); + WriteVerbose(verboseMessage); } // If the user did specify which archive format to use, emit a warning if DestinationPath does not match the chosen archive format else diff --git a/src/Localized/Messages.Designer.cs b/src/Localized/Messages.Designer.cs index 6878153..f93539f 100644 --- a/src/Localized/Messages.Designer.cs +++ b/src/Localized/Messages.Designer.cs @@ -96,6 +96,15 @@ internal static string ArchiveFormatCouldNotBeDeterminedWarning { } } + /// + /// Looks up a localized string similar to . + /// + internal static string ArchiveFormatDeterminedVerboseMessage { + get { + return ResourceManager.GetString("ArchiveFormatDeterminedVerboseMessage", resourceCulture); + } + } + /// /// Looks up a localized string similar to . /// diff --git a/src/Localized/Messages.resx b/src/Localized/Messages.resx index 78469d6..b351b24 100644 --- a/src/Localized/Messages.resx +++ b/src/Localized/Messages.resx @@ -129,6 +129,9 @@ + + + diff --git a/src/PathHelper.cs b/src/PathHelper.cs index c1c8c4e..9bdaeaa 100644 --- a/src/PathHelper.cs +++ b/src/PathHelper.cs @@ -95,7 +95,7 @@ private void AddArchiveAdditionForUserEnteredNonLiteralPath(string path, List throw new NotImplementedException(); + + string IArchive.Path => throw new NotImplementedException(); + + void IArchive.AddFilesytemEntry(ArchiveAddition entry) + { + throw new NotImplementedException(); + } + + string[] IArchive.GetEntries() + { + throw new NotImplementedException(); + } + + void IArchive.Expand(string destinationPath) + { + throw new NotImplementedException(); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} From d872cf831cd19e02d211b157fc13d448232c4dee Mon Sep 17 00:00:00 2001 From: Abdullah Yousuf Date: Tue, 19 Jul 2022 19:32:06 -0700 Subject: [PATCH 20/42] renamed Action to WriteMode, fixed bug with compressing directories, added support for tar, added support for overwrite --- src/ArchiveFactory.cs | 1 + src/CompressArchiveCommand.cs | 57 +++++++++++++++++++------ src/ErrorMessages.cs | 10 +++-- src/Localized/Messages.Designer.cs | 35 +++++++++++++-- src/Localized/Messages.en-US.resx | 28 +++++++++++- src/Localized/Messages.resx | 13 +++++- src/Microsoft.PowerShell.Archive.csproj | 2 +- src/PathHelper.cs | 24 +++++------ src/Properties/launchSettings.json | 2 +- src/TarArchive.cs | 22 ++++++++-- src/{Action.cs => WriteMode.cs} | 2 +- 11 files changed, 154 insertions(+), 42 deletions(-) rename src/{Action.cs => WriteMode.cs} (87%) diff --git a/src/ArchiveFactory.cs b/src/ArchiveFactory.cs index 7d22b1a..beb8620 100644 --- a/src/ArchiveFactory.cs +++ b/src/ArchiveFactory.cs @@ -22,6 +22,7 @@ internal static IArchive GetArchive(ArchiveFormat format, string archivePath, Ar return format switch { ArchiveFormat.zip => new ZipArchive(archivePath, archiveMode, archiveFileStream, compressionLevel), + ArchiveFormat.tar => new TarArchive(archivePath, archiveMode, archiveFileStream), // TODO: Add archive types here // TODO: Add message to exception _ => throw new NotImplementedException() diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index d163cf0..1c6573d 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -52,7 +52,7 @@ public class CompressArchiveCommand : PSCmdlet public string? DestinationPath { get; set; } [Parameter()] - public Action Action { get; set; } = Action.Create; + public WriteMode WriteMode { get; set; } = WriteMode.Create; [Parameter()] public SwitchParameter PassThru { get; set; } = false; @@ -156,7 +156,7 @@ protected override void EndProcessing() // Get the ArchiveMode for the archive to be created or updated ArchiveMode archiveMode = ArchiveMode.Create; - if (Action == Action.Update) + if (WriteMode == WriteMode.Update) { archiveMode = ArchiveMode.Update; } @@ -167,6 +167,12 @@ protected override void EndProcessing() { if (ShouldProcess(target: DestinationPath, action: "Create")) { + // If the WriteMode is overwrite, delete the existing archive + if (WriteMode == WriteMode.Overwrite) + { + DeleteDestinationPathIfExists(); + } + // Create an archive -- this is where we will switch between different types of archives archive = ArchiveFactory.GetArchive(format: Format ?? ArchiveFormat.zip, archivePath: DestinationPath, archiveMode: archiveMode, compressionLevel: CompressionLevel); } @@ -179,11 +185,7 @@ protected override void EndProcessing() archive?.AddFilesytemEntry(entry); } } - } - catch - { - - } + } finally { archive?.Dispose(); @@ -210,12 +212,12 @@ private void ValidateDestinationPath() if (archiveAsFile.Exists) { // Throw an error if DestinationPath exists and the cmdlet is not in Update mode or Overwrite is not specified - if (Action == Action.Create) + if (WriteMode == WriteMode.Create) { errorCode = ErrorCode.ArchiveExists; } // Throw an error if the cmdlet is in Update mode but the archive is read only - if (Action == Action.Update && archiveAsFile.Attributes.HasFlag(FileAttributes.ReadOnly)) + if (WriteMode == WriteMode.Update && archiveAsFile.Attributes.HasFlag(FileAttributes.ReadOnly)) { errorCode = ErrorCode.ArchiveReadOnly; } @@ -224,17 +226,17 @@ private void ValidateDestinationPath() else if (archiveAsDirectory.Exists) { // Throw an error if DestinationPath exists and the cmdlet is not in Update mode or Overwrite is not specified - if (Action == Action.Create) + if (WriteMode == WriteMode.Create) { errorCode = ErrorCode.ArchiveExistsAsDirectory; } // Throw an error if the DestinationPath is a directory and the cmdlet is in Update mode - if (Action == Action.Update) + if (WriteMode == WriteMode.Update) { errorCode = ErrorCode.ArchiveExistsAsDirectory; } // Throw an error if the DestinationPath is a directory with at least item and the cmdlet is in Overwrite mode - if (Action == Action.Overwrite && archiveAsDirectory.GetFileSystemInfos().Length > 0) + if (WriteMode == WriteMode.Overwrite && archiveAsDirectory.GetFileSystemInfos().Length > 0) { errorCode = ErrorCode.ArchiveIsNonEmptyDirectory; } @@ -243,7 +245,7 @@ private void ValidateDestinationPath() else { // Throw an error if DestinationPath does not exist and cmdlet is in Update mode - if (Action == Action.Update) + if (WriteMode == WriteMode.Update) { errorCode = ErrorCode.ArchiveDoesNotExist; } @@ -256,5 +258,34 @@ private void ValidateDestinationPath() ThrowTerminatingError(errorRecord); } } + + private void DeleteDestinationPathIfExists() + { + try + { + if (System.IO.File.Exists(DestinationPath)) + { + System.IO.File.Delete(DestinationPath); + } + if (System.IO.Directory.Exists(DestinationPath)) + { + System.IO.Directory.Delete(DestinationPath); + } + } + // Throw a terminating error if an IOException occurs + catch (System.IO.IOException ioException) + { + var errorRecord = new ErrorRecord(ioException, errorId: ErrorCode.OverwriteDestinationPathFailed.ToString(), + errorCategory: ErrorCategory.InvalidOperation, targetObject: DestinationPath); + ThrowTerminatingError(errorRecord); + } + // Throw a terminating error if an UnauthorizedAccessException occurs + catch (System.UnauthorizedAccessException unauthorizedAccessException) + { + var errorRecord = new ErrorRecord(unauthorizedAccessException, errorId: ErrorCode.InsufficientPermissionsToAccessPath.ToString(), + errorCategory: ErrorCategory.PermissionDenied, targetObject: DestinationPath); + ThrowTerminatingError(errorRecord); + } + } } } diff --git a/src/ErrorMessages.cs b/src/ErrorMessages.cs index ed5779b..92d0e78 100644 --- a/src/ErrorMessages.cs +++ b/src/ErrorMessages.cs @@ -23,7 +23,11 @@ internal static string GetErrorMessage(ErrorCode errorCode) ErrorCode.ArchiveExists => Messages.ArchiveExistsMessage, ErrorCode.ArchiveExistsAsDirectory => Messages.ArchiveExistsAsDirectoryMessage, ErrorCode.ArchiveReadOnly => Messages.ArchiveIsReadOnlyMessage, - ErrorCode.PathResolvesToMultiplePaths => Messages.PathResolvesToMultiplePathsMessage, + ErrorCode.ArchiveDoesNotExist => Messages.ArchiveDoesNotExistMessage, + ErrorCode.SamePathAndDestinationPath => Messages.SamePathAndDestinationPathMessage, + ErrorCode.SameLiteralPathAndDestinationPath => Messages.SameLiteralPathAndDestinationPathMessage, + ErrorCode.InsufficientPermissionsToAccessPath => Messages.InsufficientPermssionsToAccessPathMessage, + ErrorCode.OverwriteDestinationPathFailed => Messages.OverwriteDestinationPathFailed, _ => throw new NotImplementedException("Error code has not been implemented") }; } @@ -45,8 +49,6 @@ internal enum ErrorCode ArchiveIsNonEmptyDirectory, // Used when Compress-Archive cmdlet is in Update mode but the archive is read-only ArchiveReadOnly, - // May be removed - PathResolvesToMultiplePaths, // Used when DestinationPath does not exist and the Compress-Archive cmdlet is in Update mode ArchiveDoesNotExist, // Used when Path and DestinationPath are the same @@ -55,5 +57,7 @@ internal enum ErrorCode SameLiteralPathAndDestinationPath, // Used when the user does not have sufficient permissions to access a path InsufficientPermissionsToAccessPath, + // Used when the cmdlet could not overwrite DestinationPath + OverwriteDestinationPathFailed } } diff --git a/src/Localized/Messages.Designer.cs b/src/Localized/Messages.Designer.cs index f93539f..e095324 100644 --- a/src/Localized/Messages.Designer.cs +++ b/src/Localized/Messages.Designer.cs @@ -60,6 +60,15 @@ internal Messages() { } } + /// + /// Looks up a localized string similar to . + /// + internal static string ArchiveDoesNotExistMessage { + get { + return ResourceManager.GetString("ArchiveDoesNotExistMessage", resourceCulture); + } + } + /// /// Looks up a localized string similar to . /// @@ -126,9 +135,9 @@ internal static string DuplicatePathsMessage { /// /// Looks up a localized string similar to . /// - internal static string GetResolvedPathFromPSPathProviderReturnedNullMessage { + internal static string InsufficientPermssionsToAccessPathMessage { get { - return ResourceManager.GetString("GetResolvedPathFromPSPathProviderReturnedNullMessage", resourceCulture); + return ResourceManager.GetString("InsufficientPermssionsToAccessPathMessage", resourceCulture); } } @@ -150,6 +159,15 @@ internal static string NoItemsToAddWarning { } } + /// + /// Looks up a localized string similar to . + /// + internal static string OverwriteDestinationPathFailed { + get { + return ResourceManager.GetString("OverwriteDestinationPathFailed", resourceCulture); + } + } + /// /// Looks up a localized string similar to . /// @@ -162,9 +180,18 @@ internal static string PathNotFoundMessage { /// /// Looks up a localized string similar to . /// - internal static string PathResolvesToMultiplePathsMessage { + internal static string SameLiteralPathAndDestinationPathMessage { + get { + return ResourceManager.GetString("SameLiteralPathAndDestinationPathMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to . + /// + internal static string SamePathAndDestinationPathMessage { get { - return ResourceManager.GetString("PathResolvesToMultiplePathsMessage", resourceCulture); + return ResourceManager.GetString("SamePathAndDestinationPathMessage", resourceCulture); } } } diff --git a/src/Localized/Messages.en-US.resx b/src/Localized/Messages.en-US.resx index f544e04..058a1fb 100644 --- a/src/Localized/Messages.en-US.resx +++ b/src/Localized/Messages.en-US.resx @@ -117,25 +117,49 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + The archive {0} does not exist. + The destination path {0} is a directory. The destination path {0} already exists. + + The archive {0} does not have an extension or an extension that matches the chosen archive format. + + + The format of the archive {0} could not determined by its extension. The zip archive is chosen by default. + + + The -Format was not specified, so the archive format was determined to be {0} based on its extension. + The archive at {0} is read-only. The path(s) {0} have been specified more than once. + + There are insufficient permissions to access the path {0}. + The path(s) {0} are invalid. + + There are no items to add to the archive. + + + Could not overwrite the destination path. + The path {0} could not be found. - - The path {0} resolves to multiple possible paths. + + A path {0} supplied to -LiteralPath is the same as the path supplied to -DestinationPath. + + + A path {0} supplied to -Path is the same as the path supplied to -DestinationPath. \ No newline at end of file diff --git a/src/Localized/Messages.resx b/src/Localized/Messages.resx index b351b24..b20fe4f 100644 --- a/src/Localized/Messages.resx +++ b/src/Localized/Messages.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + @@ -138,7 +141,7 @@ - + @@ -147,10 +150,16 @@ + + + - + + + + \ No newline at end of file diff --git a/src/Microsoft.PowerShell.Archive.csproj b/src/Microsoft.PowerShell.Archive.csproj index 82a956d..618c34b 100644 --- a/src/Microsoft.PowerShell.Archive.csproj +++ b/src/Microsoft.PowerShell.Archive.csproj @@ -1,7 +1,7 @@ - netstandard2.1 + net7.0 enable en-US diff --git a/src/PathHelper.cs b/src/PathHelper.cs index 9bdaeaa..42f00de 100644 --- a/src/PathHelper.cs +++ b/src/PathHelper.cs @@ -9,12 +9,12 @@ namespace Microsoft.PowerShell.Archive { - + // TODO: Add exception handling internal class PathHelper { private PSCmdlet _cmdlet; - private const string FilesystemProviderName = "FileSystem"; + private const string FileSystemProviderName = "FileSystem"; internal PathHelper(PSCmdlet cmdlet) { @@ -31,13 +31,12 @@ internal List GetArchiveAdditionsForPath(string[] paths, bool l { List additions = new List(); - //Used to keep track of non-filesystem paths + // Used to keep track of non-filesystem paths HashSet nonfilesystemPaths = new HashSet(); foreach (var path in paths) { // Based on the value of literalPath, call the appropriate method - if (literalPath) { AddArchiveAdditionForUserEnteredLiteralPath(path: path, archiveAdditions: additions, nonfilesystemPaths: nonfilesystemPaths); @@ -81,7 +80,7 @@ private void AddArchiveAdditionForUserEnteredNonLiteralPath(string path, List additions, // Add an entry for each child path var entryName = GetEntryName(path: childPath.FullName, shouldPreservePathStructure: shouldPreservePathStructure); - additions.Add(new ArchiveAddition(entryName: entryName, fullPath: childPath.Name, type: type)); + additions.Add(new ArchiveAddition(entryName: entryName, fullPath: childPath.FullName, type: type)); } } - // Throw a terminating error if a securityException occurs - catch (System.Security.SecurityException) + // Throw a non-terminating error if a securityException occurs + catch (System.Security.SecurityException securityException) { - var errorRecord = ErrorMessages.GetErrorRecord(errorCode: ErrorCode.InsufficientPermissionsToAccessPath, errorItem: path); - _cmdlet.ThrowTerminatingError(errorRecord); + var errorId = ErrorCode.InsufficientPermissionsToAccessPath.ToString(); + var errorRecord = new ErrorRecord(securityException, errorId: errorId, ErrorCategory.SecurityError, targetObject: path); + _cmdlet.WriteError(errorRecord); } // Throw a terminating error if a directoryNotFoundException occurs catch (System.IO.DirectoryNotFoundException) @@ -245,7 +245,7 @@ internal string ResolveToSingleFullyQualifiedPath(string path) string fullyQualifiedPath = _cmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath(path, out var providerInfo, out var psDriveInfo); // If the path is not from the filesystem, throw an error - if (providerInfo.Name != FilesystemProviderName) + if (providerInfo.Name != FileSystemProviderName) { var errorRecord = ErrorMessages.GetErrorRecord(errorCode: ErrorCode.InvalidPath, errorItem: path); _cmdlet.ThrowTerminatingError(errorRecord); diff --git a/src/Properties/launchSettings.json b/src/Properties/launchSettings.json index 4f406c4..7fa785b 100644 --- a/src/Properties/launchSettings.json +++ b/src/Properties/launchSettings.json @@ -5,7 +5,7 @@ }, "PowerShell 7": { "commandName": "Executable", - "executablePath": "pwsh", + "executablePath": "C:\\Users\\t-ayousuf\\Code\\PowerShell\\src\\powershell-win-core\\bin\\Debug\\net7.0\\win7-x64\\pwsh.exe", "commandLineArgs": "-NoExit -Command \"& Import-Module .\\Microsoft.PowerShell.Archive.dll" } } diff --git a/src/TarArchive.cs b/src/TarArchive.cs index 9bebfea..a0d1fc4 100644 --- a/src/TarArchive.cs +++ b/src/TarArchive.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Formats.Tar; +using System.IO; using System.Text; namespace Microsoft.PowerShell.Archive @@ -12,13 +14,25 @@ internal class TarArchive : IArchive private string _path; - ArchiveMode IArchive.Mode => throw new NotImplementedException(); + private TarWriter _tarWriter; - string IArchive.Path => throw new NotImplementedException(); + private FileStream _fileStream; + + ArchiveMode IArchive.Mode => _mode; + + string IArchive.Path => _path; + + public TarArchive(string path, ArchiveMode mode, FileStream fileStream) + { + _mode = mode; + _path = path; + _tarWriter = new TarWriter(archiveStream: fileStream, archiveFormat: TarFormat.Pax, leaveOpen: false); + _fileStream = fileStream; + } void IArchive.AddFilesytemEntry(ArchiveAddition entry) { - throw new NotImplementedException(); + _tarWriter.WriteEntry(fileName: entry.FullPath, entryName: entry.EntryName); } string[] IArchive.GetEntries() @@ -38,6 +52,8 @@ protected virtual void Dispose(bool disposing) if (disposing) { // TODO: dispose managed state (managed objects) + _tarWriter.Dispose(); + _fileStream.Dispose(); } // TODO: free unmanaged resources (unmanaged objects) and override finalizer diff --git a/src/Action.cs b/src/WriteMode.cs similarity index 87% rename from src/Action.cs rename to src/WriteMode.cs index f21c881..88566b1 100644 --- a/src/Action.cs +++ b/src/WriteMode.cs @@ -4,7 +4,7 @@ namespace Microsoft.PowerShell.Archive { - public enum Action + public enum WriteMode { Create, Update, From bc4987121d6377f529e1ab10e50f62893edddfe4 Mon Sep 17 00:00:00 2001 From: Abdullah Yousuf Date: Wed, 20 Jul 2022 13:17:56 -0700 Subject: [PATCH 21/42] fixed bug where the directory structure of directories was not being preserved, fixed bug where error and warning messages were not being shown --- src/CompressArchiveCommand.cs | 1 + src/Localized/Messages.Designer.cs | 30 +++--- src/Localized/Messages.en-US.resx | 165 ----------------------------- src/Localized/Messages.resx | 30 +++--- src/PathHelper.cs | 38 ++++++- 5 files changed, 67 insertions(+), 197 deletions(-) delete mode 100644 src/Localized/Messages.en-US.resx diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index 1c6573d..e40d62b 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -72,6 +72,7 @@ public CompressArchiveCommand() { _sourcePaths = new List(); _pathHelper = new PathHelper(this); + Messages.Culture = new System.Globalization.CultureInfo("en-US"); } protected override void BeginProcessing() diff --git a/src/Localized/Messages.Designer.cs b/src/Localized/Messages.Designer.cs index e095324..3946e25 100644 --- a/src/Localized/Messages.Designer.cs +++ b/src/Localized/Messages.Designer.cs @@ -61,7 +61,7 @@ internal Messages() { } /// - /// Looks up a localized string similar to . + /// Looks up a localized string similar to The archive {0} does not exist.. /// internal static string ArchiveDoesNotExistMessage { get { @@ -70,7 +70,7 @@ internal static string ArchiveDoesNotExistMessage { } /// - /// Looks up a localized string similar to . + /// Looks up a localized string similar to The destination path {0} is a directory.. /// internal static string ArchiveExistsAsDirectoryMessage { get { @@ -79,7 +79,7 @@ internal static string ArchiveExistsAsDirectoryMessage { } /// - /// Looks up a localized string similar to . + /// Looks up a localized string similar to The destination path {0} already exists.. /// internal static string ArchiveExistsMessage { get { @@ -88,7 +88,7 @@ internal static string ArchiveExistsMessage { } /// - /// Looks up a localized string similar to . + /// Looks up a localized string similar to The archive {0} does not have an extension or an extension that matches the chosen archive format.. /// internal static string ArchiveExtensionDoesNotMatchArchiveFormatWarning { get { @@ -97,7 +97,7 @@ internal static string ArchiveExtensionDoesNotMatchArchiveFormatWarning { } /// - /// Looks up a localized string similar to . + /// Looks up a localized string similar to The format of the archive {0} could not determined by its extension. The zip format is chosen by default.. /// internal static string ArchiveFormatCouldNotBeDeterminedWarning { get { @@ -106,7 +106,7 @@ internal static string ArchiveFormatCouldNotBeDeterminedWarning { } /// - /// Looks up a localized string similar to . + /// Looks up a localized string similar to The -Format was not specified, so the archive format was determined to be {0} based on its extension.. /// internal static string ArchiveFormatDeterminedVerboseMessage { get { @@ -115,7 +115,7 @@ internal static string ArchiveFormatDeterminedVerboseMessage { } /// - /// Looks up a localized string similar to . + /// Looks up a localized string similar to The archive at {0} is read-only.. /// internal static string ArchiveIsReadOnlyMessage { get { @@ -124,7 +124,7 @@ internal static string ArchiveIsReadOnlyMessage { } /// - /// Looks up a localized string similar to . + /// Looks up a localized string similar to The path(s) {0} have been specified more than once.. /// internal static string DuplicatePathsMessage { get { @@ -133,7 +133,7 @@ internal static string DuplicatePathsMessage { } /// - /// Looks up a localized string similar to . + /// Looks up a localized string similar to There are insufficient permissions to access the path {0}.. /// internal static string InsufficientPermssionsToAccessPathMessage { get { @@ -142,7 +142,7 @@ internal static string InsufficientPermssionsToAccessPathMessage { } /// - /// Looks up a localized string similar to . + /// Looks up a localized string similar to The path(s) {0} are invalid.. /// internal static string InvalidPathMessage { get { @@ -151,7 +151,7 @@ internal static string InvalidPathMessage { } /// - /// Looks up a localized string similar to . + /// Looks up a localized string similar to There are no items to add to the archive.. /// internal static string NoItemsToAddWarning { get { @@ -160,7 +160,7 @@ internal static string NoItemsToAddWarning { } /// - /// Looks up a localized string similar to . + /// Looks up a localized string similar to Could not overwrite the destination path.. /// internal static string OverwriteDestinationPathFailed { get { @@ -169,7 +169,7 @@ internal static string OverwriteDestinationPathFailed { } /// - /// Looks up a localized string similar to . + /// Looks up a localized string similar to The path {0} could not be found.. /// internal static string PathNotFoundMessage { get { @@ -178,7 +178,7 @@ internal static string PathNotFoundMessage { } /// - /// Looks up a localized string similar to . + /// Looks up a localized string similar to A path {0} supplied to -LiteralPath is the same as the path supplied to -DestinationPath.. /// internal static string SameLiteralPathAndDestinationPathMessage { get { @@ -187,7 +187,7 @@ internal static string SameLiteralPathAndDestinationPathMessage { } /// - /// Looks up a localized string similar to . + /// Looks up a localized string similar to A path {0} supplied to -Path is the same as the path supplied to -DestinationPath.. /// internal static string SamePathAndDestinationPathMessage { get { diff --git a/src/Localized/Messages.en-US.resx b/src/Localized/Messages.en-US.resx deleted file mode 100644 index 058a1fb..0000000 --- a/src/Localized/Messages.en-US.resx +++ /dev/null @@ -1,165 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - The archive {0} does not exist. - - - The destination path {0} is a directory. - - - The destination path {0} already exists. - - - The archive {0} does not have an extension or an extension that matches the chosen archive format. - - - The format of the archive {0} could not determined by its extension. The zip archive is chosen by default. - - - The -Format was not specified, so the archive format was determined to be {0} based on its extension. - - - The archive at {0} is read-only. - - - The path(s) {0} have been specified more than once. - - - There are insufficient permissions to access the path {0}. - - - The path(s) {0} are invalid. - - - There are no items to add to the archive. - - - Could not overwrite the destination path. - - - The path {0} could not be found. - - - A path {0} supplied to -LiteralPath is the same as the path supplied to -DestinationPath. - - - A path {0} supplied to -Path is the same as the path supplied to -DestinationPath. - - \ No newline at end of file diff --git a/src/Localized/Messages.resx b/src/Localized/Messages.resx index b20fe4f..a7a9738 100644 --- a/src/Localized/Messages.resx +++ b/src/Localized/Messages.resx @@ -118,48 +118,48 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + The archive {0} does not exist. - + The destination path {0} is a directory. - + The destination path {0} already exists. - + The archive {0} does not have an extension or an extension that matches the chosen archive format. - + The format of the archive {0} could not determined by its extension. The zip format is chosen by default. - + The -Format was not specified, so the archive format was determined to be {0} based on its extension. - + The archive at {0} is read-only. - + The path(s) {0} have been specified more than once. - + There are insufficient permissions to access the path {0}. - + The path(s) {0} are invalid. - + There are no items to add to the archive. - + Could not overwrite the destination path. - + The path {0} could not be found. - + A path {0} supplied to -LiteralPath is the same as the path supplied to -DestinationPath. - + A path {0} supplied to -Path is the same as the path supplied to -DestinationPath. \ No newline at end of file diff --git a/src/PathHelper.cs b/src/PathHelper.cs index 42f00de..3e1a73a 100644 --- a/src/PathHelper.cs +++ b/src/PathHelper.cs @@ -163,6 +163,8 @@ private void AddDescendentEntries(string path, List additions, try { var directoryInfo = new System.IO.DirectoryInfo(path); + // pathPrefix is used to construct the entry names of the descendents of the directory + var pathPrefix = GetPrefixForPath(directoryInfo: directoryInfo); foreach (var childPath in directoryInfo.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)) { // childPath can either be a file or directory, and nothing else @@ -173,7 +175,7 @@ private void AddDescendentEntries(string path, List additions, } // Add an entry for each child path - var entryName = GetEntryName(path: childPath.FullName, shouldPreservePathStructure: shouldPreservePathStructure); + var entryName = GetEntryName(path: childPath.FullName, prefix: pathPrefix); additions.Add(new ArchiveAddition(entryName: entryName, fullPath: childPath.FullName, type: type)); } } @@ -219,6 +221,38 @@ private string GetEntryName(string path, bool shouldPreservePathStructure) } } + private string GetEntryName(string path, string prefix) + { + if (prefix == String.Empty) return path; + + //If the path does not start with the prefix, throw an exception + if (!path.StartsWith(prefix)) + { + throw new ArgumentException($"{path} does not begin with {prefix}"); + } + + if (path.Length <= prefix.Length) throw new ArgumentException($"The length of {path} is shorter than or equal to the length of {prefix}"); + + string entryName = path.Substring(prefix.Length); + + return entryName; + } + + private string GetPrefixForPath(System.IO.DirectoryInfo directoryInfo) + { + // Get the parent directory of the path + if (directoryInfo.Parent is null) + { + return String.Empty; + } + var prefix = directoryInfo.Parent.FullName; + if (!prefix.EndsWith(System.IO.Path.DirectorySeparatorChar)) + { + prefix += System.IO.Path.DirectorySeparatorChar; + } + return prefix; + } + /// /// Get the duplicate fully qualified paths from a list of ArchiveAdditions /// @@ -261,7 +295,7 @@ internal string ResolveToSingleFullyQualifiedPath(string path) /// private bool CanPreservePathStructure(string path) { - return System.IO.Path.IsPathRooted(path); + return !System.IO.Path.IsPathRooted(path); } /// From 6ee7ccb6c90fca695698aec459f4b41d61575e36 Mon Sep 17 00:00:00 2001 From: Abdullah Yousuf Date: Wed, 20 Jul 2022 17:34:39 -0700 Subject: [PATCH 22/42] addded exception handling to PathHelper class --- src/ArchiveFactory.cs | 5 ++ src/CompressArchiveCommand.cs | 43 ++++++++++- src/Localized/Messages.Designer.cs | 9 +++ src/Localized/Messages.resx | 3 + src/PathHelper.cs | 120 +++++++++++++++++++++++------ 5 files changed, 154 insertions(+), 26 deletions(-) diff --git a/src/ArchiveFactory.cs b/src/ArchiveFactory.cs index beb8620..46a4eb1 100644 --- a/src/ArchiveFactory.cs +++ b/src/ArchiveFactory.cs @@ -19,6 +19,11 @@ internal static IArchive GetArchive(ArchiveFormat format, string archivePath, Ar _ => throw new NotImplementedException() }; + if (format == ArchiveFormat.tar) + { + archiveFileStream.Position = archiveFileStream.Length; + } + return format switch { ArchiveFormat.zip => new ZipArchive(archivePath, archiveMode, archiveFileStream, compressionLevel), diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index e40d62b..6386df7 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -68,11 +68,14 @@ public class CompressArchiveCommand : PSCmdlet private PathHelper _pathHelper; + private bool _didCreateNewArchive; + public CompressArchiveCommand() { _sourcePaths = new List(); _pathHelper = new PathHelper(this); Messages.Culture = new System.Globalization.CultureInfo("en-US"); + _didCreateNewArchive = false; } protected override void BeginProcessing() @@ -176,26 +179,64 @@ protected override void EndProcessing() // Create an archive -- this is where we will switch between different types of archives archive = ArchiveFactory.GetArchive(format: Format ?? ArchiveFormat.zip, archivePath: DestinationPath, archiveMode: archiveMode, compressionLevel: CompressionLevel); + _didCreateNewArchive = archiveMode == ArchiveMode.Update; } // TODO: Update progress + long numberOfAdditions = archiveAddtions.Count; + long numberOfAddedItems = 0; + var progressRecord = new ProgressRecord(activityId: 1, activity: "Compress-Archive", "0% complete"); + WriteProgress(progressRecord); foreach (ArchiveAddition entry in archiveAddtions) { if (ShouldProcess(target: entry.FullPath, action: "Add")) { archive?.AddFilesytemEntry(entry); + // Keep track of number of items added to the archive and use that to update progress + numberOfAddedItems++; + var percentComplete = numberOfAddedItems / (float)numberOfAdditions * 100f; + progressRecord.StatusDescription = $"{percentComplete:0.0}% complete"; + WriteProgress(progressRecord); + + // Write a verbose message saying this item was added to the archive + var addedItemMessage = String.Format(Messages.AddedItemToArchiveVerboseMessage, entry.FullPath); + WriteVerbose(addedItemMessage); + } else + { + numberOfAdditions--; } } + + // If there were no items to add, show progress as 100% + if (numberOfAdditions == 0) + { + progressRecord.StatusDescription = "100% complete"; + WriteProgress(progressRecord); + } } finally { archive?.Dispose(); } + + // If -PassThru is specified, write a System.IO.FileInfo object + if (PassThru) + { + var archiveInfo = new System.IO.FileInfo(DestinationPath); + WriteObject(archiveInfo); + } } protected override void StopProcessing() { - base.StopProcessing(); + // If a new output archive was created, delete it (this does not delete an archive if -WriteMode Update is specified) + if (_didCreateNewArchive) + { + if (System.IO.File.Exists(DestinationPath)) + { + System.IO.File.Delete(DestinationPath); + } + } } /// diff --git a/src/Localized/Messages.Designer.cs b/src/Localized/Messages.Designer.cs index 3946e25..a62813d 100644 --- a/src/Localized/Messages.Designer.cs +++ b/src/Localized/Messages.Designer.cs @@ -60,6 +60,15 @@ internal Messages() { } } + /// + /// Looks up a localized string similar to {0} was added to the archive.. + /// + internal static string AddedItemToArchiveVerboseMessage { + get { + return ResourceManager.GetString("AddedItemToArchiveVerboseMessage", resourceCulture); + } + } + /// /// Looks up a localized string similar to The archive {0} does not exist.. /// diff --git a/src/Localized/Messages.resx b/src/Localized/Messages.resx index a7a9738..d5b8a2f 100644 --- a/src/Localized/Messages.resx +++ b/src/Localized/Messages.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + {0} was added to the archive. + The archive {0} does not exist. diff --git a/src/PathHelper.cs b/src/PathHelper.cs index 3e1a73a..49d8d30 100644 --- a/src/PathHelper.cs +++ b/src/PathHelper.cs @@ -76,25 +76,63 @@ internal List GetArchiveAdditionsForPath(string[] paths, bool l /// private void AddArchiveAdditionForUserEnteredNonLiteralPath(string path, List archiveAdditions, HashSet nonfilesystemPaths) { - // Resolve the path -- I don't think we need to handle exceptions here as no special behavior occurs when an exception occurs - var resolvedPaths = _cmdlet.SessionState.Path.GetResolvedProviderPathFromPSPath(path: path, provider: out var providerInfo); + // Keep the exception at the top, then when an error occurs, use the exception to create an ErrorRecord + Exception? exception = null; + try + { + // Resolve the path -- I don't think we need to handle exceptions here as no special behavior occurs when an exception occurs + var resolvedPaths = _cmdlet.SessionState.Path.GetResolvedProviderPathFromPSPath(path: path, provider: out var providerInfo); + + // Check if the path if from the filesystem + if (providerInfo?.Name != FileSystemProviderName) + { + // If not, add the path to the set of non-filesystem paths. We will throw an error later so we can show the user all invalid paths at once + nonfilesystemPaths.Add(path); + return; + } + + // Check if the cmdlet can preserve paths based on path variable + bool shouldPreservePathStructure = CanPreservePathStructure(path); - // Check if the path if from the filesystem - if (providerInfo?.Name != FileSystemProviderName) + // Go through each resolved path and add an ArchiveAddition for it to additions + for (int i = 0; i < resolvedPaths.Count; i++) + { + var resolvedPath = resolvedPaths[i]; + AddAdditionForFullyQualifiedPath(path: resolvedPath, additions: archiveAdditions, shouldPreservePathStructure: shouldPreservePathStructure); + } + } + catch (System.Management.Automation.ProviderNotFoundException providerNotFoundException) + { + exception = providerNotFoundException; + } + catch (System.Management.Automation.DriveNotFoundException driveNotFoundException) { - // If not, add the path to the set of non-filesystem paths. We will throw an error later so we can show the user all invalid paths at once - nonfilesystemPaths.Add(path); - return; + exception = driveNotFoundException; } + catch (System.Management.Automation.ProviderInvocationException providerInvocationException) + { + exception = providerInvocationException; + } + catch (System.Management.Automation.PSNotSupportedException notSupportedException) + { + exception = notSupportedException; + } + catch (System.Management.Automation.PSInvalidOperationException invalidOperationException) + { + exception = invalidOperationException; + } + // Throw a terminating error if the path could not be found + catch (System.Management.Automation.ItemNotFoundException notFoundException) + { - // Check if the cmdlet can preserve paths based on path variable - bool shouldPreservePathStructure = CanPreservePathStructure(path); + } - // Go through each resolved path and add an ArchiveAddition for it to additions - for (int i = 0; i < resolvedPaths.Count; i++) + // If an exception was caught, write a non-terminating error + if (exception != null) { - var resolvedPath = resolvedPaths[i]; - AddAdditionForFullyQualifiedPath(path: resolvedPath, additions: archiveAdditions, shouldPreservePathStructure: shouldPreservePathStructure); + var errorRecord = new ErrorRecord(exception: exception, errorId: ErrorCode.InvalidPath.ToString(), errorCategory: ErrorCategory.InvalidArgument, + targetObject: path); + _cmdlet.WriteError(errorRecord); } } @@ -106,22 +144,54 @@ private void AddArchiveAdditionForUserEnteredNonLiteralPath(string path, List private void AddArchiveAdditionForUserEnteredLiteralPath(string path, List archiveAdditions, HashSet nonfilesystemPaths) { - // Resolve the path -- gets the fully qualified path - // I don't think we need to handle exceptions for the call below as the cmdlet does not have any special behaviors when the call below throws an exception - string fullyQualifiedPath = _cmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath(path, out var providerInfo, out var psDriveInfo); + // Keep the exception at the top, then when an error occurs, use the exception to create an ErrorRecord + Exception? exception = null; + try + { + // Resolve the path -- gets the fully qualified path + string fullyQualifiedPath = _cmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath(path, out var providerInfo, out var psDriveInfo); - // Check if the path is from the filesystem - if (providerInfo.Name != FileSystemProviderName) + // Check if the path is from the filesystem + if (providerInfo.Name != FileSystemProviderName) + { + nonfilesystemPaths.Add(path); + return; + } + + // Check if we can preserve the path structure -- this is based on the original path the user entered (not fully qualified) + bool canPreservePathStructure = CanPreservePathStructure(path: path); + + // Add an ArchiveAddition for the path to the list of additions + AddAdditionForFullyQualifiedPath(path: fullyQualifiedPath, additions: archiveAdditions, shouldPreservePathStructure: canPreservePathStructure); + } + catch (System.Management.Automation.ProviderNotFoundException providerNotFoundException) + { + exception = providerNotFoundException; + } + catch (System.Management.Automation.DriveNotFoundException driveNotFoundException) { - nonfilesystemPaths.Add(path); - return; + exception = driveNotFoundException; + } + catch (System.Management.Automation.ProviderInvocationException providerInvocationException) + { + exception = providerInvocationException; + } + catch (System.Management.Automation.PSNotSupportedException notSupportedException) + { + exception = notSupportedException; + } + catch (System.Management.Automation.PSInvalidOperationException invalidOperationException) + { + exception = invalidOperationException; } - // Check if we can preserve the path structure -- this is based on the original path the user entered (not fully qualified) - bool canPreservePathStructure = CanPreservePathStructure(path: path); - - // Add an ArchiveAddition for the path to the list of additions - AddAdditionForFullyQualifiedPath(path: fullyQualifiedPath, additions: archiveAdditions, shouldPreservePathStructure: canPreservePathStructure); + // If an exception was caught, write a non-terminating error + if (exception != null) + { + var errorRecord = new ErrorRecord(exception: exception, errorId: ErrorCode.InvalidPath.ToString(), errorCategory: ErrorCategory.InvalidArgument, + targetObject: path); + _cmdlet.WriteError(errorRecord); + } } /// From 3fc29de5745ad37e75d8eaf57e0f1a9421fb606a Mon Sep 17 00:00:00 2001 From: Abdullah Yousuf Date: Thu, 21 Jul 2022 11:11:21 -0700 Subject: [PATCH 23/42] removed files from git, removed tar support for preview release, added psd1 file --- .gitignore | 6 ++ .vscode/launch.json | 14 --- src/ArchiveFactory.cs | 11 +- src/ArchiveFormat.cs | 3 +- src/CompressArchiveCommand.cs | 62 ++++++----- src/Microsoft.PowerShell.Archive.csproj | 10 ++ src/Microsoft.PowerShell.Archive.psd1 | 132 ++++++++++++++++++++++++ src/Properties/launchSettings.json | 12 --- 8 files changed, 187 insertions(+), 63 deletions(-) delete mode 100644 .vscode/launch.json create mode 100644 src/Microsoft.PowerShell.Archive.psd1 delete mode 100644 src/Properties/launchSettings.json diff --git a/.gitignore b/.gitignore index 9491a2f..2b3834d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,12 @@ ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +# VSCode +.vscode + +# Visual Studio Launch Settings +src/Properties + # User-specific files *.rsuser *.suo diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index b6c7b60..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "PowerShell: Attach to PowerShell Host Process", - "type": "PowerShell", - "request": "attach", - "runspaceId": 1 - } - ] -} \ No newline at end of file diff --git a/src/ArchiveFactory.cs b/src/ArchiveFactory.cs index 46a4eb1..33fb5e4 100644 --- a/src/ArchiveFactory.cs +++ b/src/ArchiveFactory.cs @@ -19,15 +19,10 @@ internal static IArchive GetArchive(ArchiveFormat format, string archivePath, Ar _ => throw new NotImplementedException() }; - if (format == ArchiveFormat.tar) - { - archiveFileStream.Position = archiveFileStream.Length; - } - return format switch { ArchiveFormat.zip => new ZipArchive(archivePath, archiveMode, archiveFileStream, compressionLevel), - ArchiveFormat.tar => new TarArchive(archivePath, archiveMode, archiveFileStream), + //ArchiveFormat.tar => new TarArchive(archivePath, archiveMode, archiveFileStream), // TODO: Add archive types here // TODO: Add message to exception _ => throw new NotImplementedException() @@ -41,14 +36,14 @@ internal static bool TryGetArchiveFormatForPath(string path, out ArchiveFormat? { archiveFormat = ArchiveFormat.zip; } - if (path.EndsWith(".tar")) + /*if (path.EndsWith(".tar")) { archiveFormat = ArchiveFormat.tar; } if (path.EndsWith(".tar.gz") || path.EndsWith(".tgz")) { archiveFormat = ArchiveFormat.tgz; - } + }*/ return archiveFormat != null; } } diff --git a/src/ArchiveFormat.cs b/src/ArchiveFormat.cs index 783c7b5..245b398 100644 --- a/src/ArchiveFormat.cs +++ b/src/ArchiveFormat.cs @@ -7,7 +7,8 @@ namespace Microsoft.PowerShell.Archive public enum ArchiveFormat { zip, + /* Removing these formats for preview relase tar, - tgz + tgz*/ } } diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index 6386df7..206d04f 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -85,34 +85,7 @@ protected override void BeginProcessing() // Validate DestinationPath ValidateDestinationPath(); - // Check if cmdlet is able to determine the format of the archive based on the extension of DestinationPath - bool ableToDetermineArchiveFormat = ArchiveFactory.TryGetArchiveFormatForPath(path: DestinationPath, archiveFormat: out var archiveFormat); - // If the user did not specify which archive format to use, try to determine it automatically - if (Format is null) - { - if (ableToDetermineArchiveFormat) - { - Format = archiveFormat; - } else - { - // If the archive format could not be determined, use zip by default and emit a warning - var warningMsg = String.Format(Messages.ArchiveFormatCouldNotBeDeterminedWarning, DestinationPath); - WriteWarning(warningMsg); - Format = ArchiveFormat.zip; - } - // Write a verbose message saying that Format is not specified and a format was determined automatically - string verboseMessage = String.Format(Messages.ArchiveFormatDeterminedVerboseMessage, Format); - WriteVerbose(verboseMessage); - } - // If the user did specify which archive format to use, emit a warning if DestinationPath does not match the chosen archive format - else - { - if (archiveFormat is null || archiveFormat.Value != Format.Value) - { - var warningMsg = String.Format(Messages.ArchiveExtensionDoesNotMatchArchiveFormatWarning, DestinationPath); - WriteWarning(warningMsg); - } - } + } @@ -329,5 +302,38 @@ private void DeleteDestinationPathIfExists() ThrowTerminatingError(errorRecord); } } + + private void DetermineArchiveFormat() + { + // Check if cmdlet is able to determine the format of the archive based on the extension of DestinationPath + bool ableToDetermineArchiveFormat = ArchiveFactory.TryGetArchiveFormatForPath(path: DestinationPath, archiveFormat: out var archiveFormat); + // If the user did not specify which archive format to use, try to determine it automatically + if (Format is null) + { + if (ableToDetermineArchiveFormat) + { + Format = archiveFormat; + } + else + { + // If the archive format could not be determined, use zip by default and emit a warning + var warningMsg = String.Format(Messages.ArchiveFormatCouldNotBeDeterminedWarning, DestinationPath); + WriteWarning(warningMsg); + Format = ArchiveFormat.zip; + } + // Write a verbose message saying that Format is not specified and a format was determined automatically + string verboseMessage = String.Format(Messages.ArchiveFormatDeterminedVerboseMessage, Format); + WriteVerbose(verboseMessage); + } + // If the user did specify which archive format to use, emit a warning if DestinationPath does not match the chosen archive format + else + { + if (archiveFormat is null || archiveFormat.Value != Format.Value) + { + var warningMsg = String.Format(Messages.ArchiveExtensionDoesNotMatchArchiveFormatWarning, DestinationPath); + WriteWarning(warningMsg); + } + } + } } } diff --git a/src/Microsoft.PowerShell.Archive.csproj b/src/Microsoft.PowerShell.Archive.csproj index 618c34b..69bbcca 100644 --- a/src/Microsoft.PowerShell.Archive.csproj +++ b/src/Microsoft.PowerShell.Archive.csproj @@ -6,6 +6,16 @@ en-US + + + + + + + PreserveNewest + + + diff --git a/src/Microsoft.PowerShell.Archive.psd1 b/src/Microsoft.PowerShell.Archive.psd1 new file mode 100644 index 0000000..7aa833e --- /dev/null +++ b/src/Microsoft.PowerShell.Archive.psd1 @@ -0,0 +1,132 @@ +# +# Module manifest for module '' +# +# Generated by: Microsoft +# +# Generated on: 6/17/2022 +# + +@{ + +# Script module or binary module file associated with this manifest. +RootModule = '' + +# Version number of this module. +ModuleVersion = '2.0.0' + +# Supported PSEditions +# CompatiblePSEditions = @() + +# ID used to uniquely identify this module +GUID = '06a335eb-dd10-4d25-b753-4f6a80163516' + +# Author of this module +Author = 'Microsoft' + +# Company or vendor of this module +CompanyName = 'Microsoft' + +# Copyright statement for this module +Copyright = '(c) Microsoft. All rights reserved.' + +# Description of the functionality provided by this module +Description = 'The module allows creating and expanding archives.' + +# Minimum version of the PowerShell engine required by this module +PowerShellVersion = '7.3.0' + +# Name of the PowerShell host required by this module +# PowerShellHostName = '' + +# Minimum version of the PowerShell host required by this module +# PowerShellHostVersion = '' + +# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# DotNetFrameworkVersion = '' + +# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# ClrVersion = '' + +# Processor architecture (None, X86, Amd64) required by this module +# ProcessorArchitecture = '' + +# Modules that must be imported into the global environment prior to importing this module +# RequiredModules = @() + +# Assemblies that must be loaded prior to importing this module +# RequiredAssemblies = @() + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +# ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +# TypesToProcess = @() + +# Format files (.ps1xml) to be loaded when importing this module +# FormatsToProcess = @() + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +NestedModules = @('Microsoft.PowerShell.Archive.dll') + +# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. +FunctionsToExport = '*' + +# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. +CmdletsToExport = @('Compress-Archive', 'Expand-Archive', 'Expand-Archive2') + +# Variables to export from this module +VariablesToExport = '*' + +# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. +AliasesToExport = '*' + +# DSC resources to export from this module +# DscResourcesToExport = @() + +# List of all modules packaged with this module +# ModuleList = @() + +# List of all files packaged with this module +# FileList = @() + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('Archive', 'Zip', 'Compress') + + # A URL to the license for this module. + # LicenseUri = '' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/PowerShell/Microsoft.PowerShell.Archive' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + # Prerelease string of this module + # Prerelease = 'This module is a prerelease version.' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + +} # End of PrivateData hashtable + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +# DefaultCommandPrefix = '' + +} + diff --git a/src/Properties/launchSettings.json b/src/Properties/launchSettings.json deleted file mode 100644 index 7fa785b..0000000 --- a/src/Properties/launchSettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "profiles": { - "Microsoft.PowerShell.Archive": { - "commandName": "Project" - }, - "PowerShell 7": { - "commandName": "Executable", - "executablePath": "C:\\Users\\t-ayousuf\\Code\\PowerShell\\src\\powershell-win-core\\bin\\Debug\\net7.0\\win7-x64\\pwsh.exe", - "commandLineArgs": "-NoExit -Command \"& Import-Module .\\Microsoft.PowerShell.Archive.dll" - } - } -} \ No newline at end of file From bf53c3399dd13f0eb6ee43d42c70f102353b50d2 Mon Sep 17 00:00:00 2001 From: Abdullah Yousuf Date: Thu, 21 Jul 2022 14:01:29 -0700 Subject: [PATCH 24/42] updated build script, updated exception handling in PathHelper --- .../Microsoft.PowerShell.Archive.psd1 | 15 - .../Microsoft.PowerShell.Archive.psm1 | 1312 ----------------- .../en-US/ArchiveResources.psd1 | 26 - SimpleBuild.ps1 | 23 +- src/PathHelper.cs | 49 +- 5 files changed, 41 insertions(+), 1384 deletions(-) delete mode 100644 Microsoft.PowerShell.Archive/Microsoft.PowerShell.Archive.psd1 delete mode 100644 Microsoft.PowerShell.Archive/Microsoft.PowerShell.Archive.psm1 delete mode 100644 Microsoft.PowerShell.Archive/en-US/ArchiveResources.psd1 diff --git a/Microsoft.PowerShell.Archive/Microsoft.PowerShell.Archive.psd1 b/Microsoft.PowerShell.Archive/Microsoft.PowerShell.Archive.psd1 deleted file mode 100644 index 3260008..0000000 --- a/Microsoft.PowerShell.Archive/Microsoft.PowerShell.Archive.psd1 +++ /dev/null @@ -1,15 +0,0 @@ -@{ -GUID="eb74e8da-9ae2-482a-a648-e96550fb8733" -Author="Microsoft Corporation" -CompanyName="Microsoft Corporation" -Copyright="© Microsoft Corporation. All rights reserved." -Description='PowerShell module for working with ZIP archives.' -ModuleVersion="1.2.5" -PowerShellVersion="3.0" -FunctionsToExport = @('Compress-Archive', 'Expand-Archive') -DotNetFrameworkVersion = 4.5 -CmdletsToExport = @() -AliasesToExport = @() -NestedModules="Microsoft.PowerShell.Archive.psm1" -HelpInfoURI = 'https://go.microsoft.com/fwlink/?LinkId=2113631' -} diff --git a/Microsoft.PowerShell.Archive/Microsoft.PowerShell.Archive.psm1 b/Microsoft.PowerShell.Archive/Microsoft.PowerShell.Archive.psm1 deleted file mode 100644 index e7dd78d..0000000 --- a/Microsoft.PowerShell.Archive/Microsoft.PowerShell.Archive.psm1 +++ /dev/null @@ -1,1312 +0,0 @@ -data LocalizedData -{ - # culture="en-US" - ConvertFrom-StringData @' - PathNotFoundError=The path '{0}' either does not exist or is not a valid file system path. - ExpandArchiveInValidDestinationPath=The path '{0}' is not a valid file system directory path. - InvalidZipFileExtensionError={0} is not a supported archive file format. {1} is the only supported archive file format. - ArchiveFileIsReadOnly=The attributes of the archive file {0} is set to 'ReadOnly' hence it cannot be updated. If you intend to update the existing archive file, remove the 'ReadOnly' attribute on the archive file else use -Force parameter to override and create a new archive file. - ZipFileExistError=The archive file {0} already exists. Use the -Update parameter to update the existing archive file or use the -Force parameter to overwrite the existing archive file. - DuplicatePathFoundError=The input to {0} parameter contains a duplicate path '{1}'. Provide a unique set of paths as input to {2} parameter. - ArchiveFileIsEmpty=The archive file {0} is empty. - CompressProgressBarText=The archive file '{0}' creation is in progress... - ExpandProgressBarText=The archive file '{0}' expansion is in progress... - AppendArchiveFileExtensionMessage=The archive file path '{0}' supplied to the DestinationPath parameter does not include .zip extension. Hence .zip is appended to the supplied DestinationPath path and the archive file would be created at '{1}'. - AddItemtoArchiveFile=Adding '{0}'. - BadArchiveEntry=Can not process invalid archive entry '{0}'. - CreateFileAtExpandedPath=Created '{0}'. - InvalidArchiveFilePathError=The archive file path '{0}' specified as input to the {1} parameter is resolving to multiple file system paths. Provide a unique path to the {2} parameter where the archive file has to be created. - InvalidExpandedDirPathError=The directory path '{0}' specified as input to the DestinationPath parameter is resolving to multiple file system paths. Provide a unique path to the Destination parameter where the archive file contents have to be expanded. - FileExistsError=Failed to create file '{0}' while expanding the archive file '{1}' contents as the file '{2}' already exists. Use the -Force parameter if you want to overwrite the existing directory '{3}' contents when expanding the archive file. - DeleteArchiveFile=The partially created archive file '{0}' is deleted as it is not usable. - InvalidDestinationPath=The destination path '{0}' does not contain a valid archive file name. - PreparingToCompressVerboseMessage=Preparing to compress... - PreparingToExpandVerboseMessage=Preparing to expand... - ItemDoesNotAppearToBeAValidZipArchive=File '{0}' does not appear to be a valid zip archive. -'@ -} - -Import-LocalizedData LocalizedData -filename ArchiveResources -ErrorAction Ignore - -$zipFileExtension = ".zip" - -<############################################################################################ -# The Compress-Archive cmdlet can be used to zip/compress one or more files/directories. -############################################################################################> -function Compress-Archive -{ - [CmdletBinding( - DefaultParameterSetName="Path", - SupportsShouldProcess=$true, - HelpUri="https://go.microsoft.com/fwlink/?linkid=2096473")] - [OutputType([System.IO.File])] - param - ( - [parameter (mandatory=$true, Position=0, ParameterSetName="Path", ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] - [parameter (mandatory=$true, Position=0, ParameterSetName="PathWithForce", ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] - [parameter (mandatory=$true, Position=0, ParameterSetName="PathWithUpdate", ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] - [ValidateNotNullOrEmpty()] - [string[]] $Path, - - [parameter (mandatory=$true, ParameterSetName="LiteralPath", ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$true)] - [parameter (mandatory=$true, ParameterSetName="LiteralPathWithForce", ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$true)] - [parameter (mandatory=$true, ParameterSetName="LiteralPathWithUpdate", ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$true)] - [ValidateNotNullOrEmpty()] - [Alias("PSPath")] - [string[]] $LiteralPath, - - [parameter (mandatory=$true, - Position=1, - ValueFromPipeline=$false, - ValueFromPipelineByPropertyName=$false)] - [ValidateNotNullOrEmpty()] - [string] $DestinationPath, - - [parameter ( - mandatory=$false, - ValueFromPipeline=$false, - ValueFromPipelineByPropertyName=$false)] - [ValidateSet("Optimal","NoCompression","Fastest")] - [string] - $CompressionLevel = "Optimal", - - [parameter(mandatory=$true, ParameterSetName="PathWithUpdate", ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false)] - [parameter(mandatory=$true, ParameterSetName="LiteralPathWithUpdate", ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false)] - [switch] - $Update = $false, - - [parameter(mandatory=$true, ParameterSetName="PathWithForce", ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false)] - [parameter(mandatory=$true, ParameterSetName="LiteralPathWithForce", ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false)] - [switch] - $Force = $false, - - [switch] - $PassThru = $false - ) - - BEGIN - { - # Ensure the destination path is in a non-PS-specific format - $DestinationPath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($DestinationPath) - - $inputPaths = @() - $destinationParentDir = [system.IO.Path]::GetDirectoryName($DestinationPath) - if($null -eq $destinationParentDir) - { - $errorMessage = ($LocalizedData.InvalidDestinationPath -f $DestinationPath) - ThrowTerminatingErrorHelper "InvalidArchiveFilePath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath - } - - if($destinationParentDir -eq [string]::Empty) - { - $destinationParentDir = '.' - } - - $archiveFileName = [system.IO.Path]::GetFileName($DestinationPath) - $destinationParentDir = GetResolvedPathHelper $destinationParentDir $false $PSCmdlet - - if($destinationParentDir.Count -gt 1) - { - $errorMessage = ($LocalizedData.InvalidArchiveFilePathError -f $DestinationPath, "DestinationPath", "DestinationPath") - ThrowTerminatingErrorHelper "InvalidArchiveFilePath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath - } - - IsValidFileSystemPath $destinationParentDir | Out-Null - $DestinationPath = Join-Path -Path $destinationParentDir -ChildPath $archiveFileName - - # GetExtension API does not validate for the actual existence of the path. - $extension = [system.IO.Path]::GetExtension($DestinationPath) - - # If user does not specify an extension, we append the .zip extension automatically. - If($extension -eq [string]::Empty) - { - $DestinationPathWithOutExtension = $DestinationPath - $DestinationPath = $DestinationPathWithOutExtension + $zipFileExtension - $appendArchiveFileExtensionMessage = ($LocalizedData.AppendArchiveFileExtensionMessage -f $DestinationPathWithOutExtension, $DestinationPath) - Write-Verbose $appendArchiveFileExtensionMessage - } - - $archiveFileExist = Test-Path -LiteralPath $DestinationPath -PathType Leaf - - if($archiveFileExist -and ($Update -eq $false -and $Force -eq $false)) - { - $errorMessage = ($LocalizedData.ZipFileExistError -f $DestinationPath) - ThrowTerminatingErrorHelper "ArchiveFileExists" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath - } - - # If archive file already exists and if -Update is specified, then we check to see - # if we have write access permission to update the existing archive file. - if($archiveFileExist -and $Update -eq $true) - { - $item = Get-Item -Path $DestinationPath - if($item.Attributes.ToString().Contains("ReadOnly")) - { - $errorMessage = ($LocalizedData.ArchiveFileIsReadOnly -f $DestinationPath) - ThrowTerminatingErrorHelper "ArchiveFileIsReadOnly" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidOperation) $DestinationPath - } - } - - $isWhatIf = $psboundparameters.ContainsKey("WhatIf") - if(!$isWhatIf) - { - $preparingToCompressVerboseMessage = ($LocalizedData.PreparingToCompressVerboseMessage) - Write-Verbose $preparingToCompressVerboseMessage - - $progressBarStatus = ($LocalizedData.CompressProgressBarText -f $DestinationPath) - ProgressBarHelper "Compress-Archive" $progressBarStatus 0 100 100 1 - } - } - PROCESS - { - if($PsCmdlet.ParameterSetName -eq "Path" -or - $PsCmdlet.ParameterSetName -eq "PathWithForce" -or - $PsCmdlet.ParameterSetName -eq "PathWithUpdate") - { - $inputPaths += $Path - } - - if($PsCmdlet.ParameterSetName -eq "LiteralPath" -or - $PsCmdlet.ParameterSetName -eq "LiteralPathWithForce" -or - $PsCmdlet.ParameterSetName -eq "LiteralPathWithUpdate") - { - $inputPaths += $LiteralPath - } - } - END - { - # If archive file already exists and if -Force is specified, we delete the - # existing archive file and create a brand new one. - if(($PsCmdlet.ParameterSetName -eq "PathWithForce" -or - $PsCmdlet.ParameterSetName -eq "LiteralPathWithForce") -and $archiveFileExist) - { - Remove-Item -Path $DestinationPath -Force -ErrorAction Stop - } - - # Validate Source Path depending on parameter set being used. - # The specified source path contains one or more files or directories that needs - # to be compressed. - $isLiteralPathUsed = $false - if($PsCmdlet.ParameterSetName -eq "LiteralPath" -or - $PsCmdlet.ParameterSetName -eq "LiteralPathWithForce" -or - $PsCmdlet.ParameterSetName -eq "LiteralPathWithUpdate") - { - $isLiteralPathUsed = $true - } - - ValidateDuplicateFileSystemPath $PsCmdlet.ParameterSetName $inputPaths - $resolvedPaths = GetResolvedPathHelper $inputPaths $isLiteralPathUsed $PSCmdlet - IsValidFileSystemPath $resolvedPaths | Out-Null - - $sourcePath = $resolvedPaths; - - # CSVHelper: This is a helper function used to append comma after each path specified by - # the $sourcePath array. The comma separated paths are displayed in the -WhatIf message. - $sourcePathInCsvFormat = CSVHelper $sourcePath - if($pscmdlet.ShouldProcess($sourcePathInCsvFormat)) - { - try - { - # StopProcessing is not available in Script cmdlets. However the pipeline execution - # is terminated when ever 'CTRL + C' is entered by user to terminate the cmdlet execution. - # The finally block is executed whenever pipeline is terminated. - # $isArchiveFileProcessingComplete variable is used to track if 'CTRL + C' is entered by the - # user. - $isArchiveFileProcessingComplete = $false - - $numberOfItemsArchived = CompressArchiveHelper $sourcePath $DestinationPath $CompressionLevel $Update - - $isArchiveFileProcessingComplete = $true - } - finally - { - # The $isArchiveFileProcessingComplete would be set to $false if user has typed 'CTRL + C' to - # terminate the cmdlet execution or if an unhandled exception is thrown. - # $numberOfItemsArchived contains the count of number of files or directories add to the archive file. - # If the newly created archive file is empty then we delete it as it's not usable. - if(($isArchiveFileProcessingComplete -eq $false) -or - ($numberOfItemsArchived -eq 0)) - { - $DeleteArchiveFileMessage = ($LocalizedData.DeleteArchiveFile -f $DestinationPath) - Write-Verbose $DeleteArchiveFileMessage - - # delete the partial archive file created. - if (Test-Path $DestinationPath) { - Remove-Item -LiteralPath $DestinationPath -Force -Recurse -ErrorAction SilentlyContinue - } - } - elseif ($PassThru) - { - Get-Item -LiteralPath $DestinationPath - } - } - } - } -} - -<############################################################################################ -# The Expand-Archive cmdlet can be used to expand/extract an zip file. -############################################################################################> -function Expand-Archive -{ - [CmdletBinding( - DefaultParameterSetName="Path", - SupportsShouldProcess=$true, - HelpUri="https://go.microsoft.com/fwlink/?linkid=2096769")] - [OutputType([System.IO.FileSystemInfo])] - param - ( - [parameter ( - mandatory=$true, - Position=0, - ParameterSetName="Path", - ValueFromPipeline=$true, - ValueFromPipelineByPropertyName=$true)] - [ValidateNotNullOrEmpty()] - [string] $Path, - - [parameter ( - mandatory=$true, - ParameterSetName="LiteralPath", - ValueFromPipelineByPropertyName=$true)] - [ValidateNotNullOrEmpty()] - [Alias("PSPath")] - [string] $LiteralPath, - - [parameter (mandatory=$false, - Position=1, - ValueFromPipeline=$false, - ValueFromPipelineByPropertyName=$false)] - [ValidateNotNullOrEmpty()] - [string] $DestinationPath, - - [parameter (mandatory=$false, - ValueFromPipeline=$false, - ValueFromPipelineByPropertyName=$false)] - [switch] $Force, - - [switch] - $PassThru = $false - ) - - BEGIN - { - $isVerbose = $psboundparameters.ContainsKey("Verbose") - $isConfirm = $psboundparameters.ContainsKey("Confirm") - - $isDestinationPathProvided = $true - if($DestinationPath -eq [string]::Empty) - { - $resolvedDestinationPath = (Get-Location).ProviderPath - $isDestinationPathProvided = $false - } - else - { - $destinationPathExists = Test-Path -Path $DestinationPath -PathType Container - if($destinationPathExists) - { - $resolvedDestinationPath = GetResolvedPathHelper $DestinationPath $false $PSCmdlet - if($resolvedDestinationPath.Count -gt 1) - { - $errorMessage = ($LocalizedData.InvalidExpandedDirPathError -f $DestinationPath) - ThrowTerminatingErrorHelper "InvalidDestinationPath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath - } - - # At this point we are sure that the provided path resolves to a valid single path. - # Calling Resolve-Path again to get the underlying provider name. - $suppliedDestinationPath = Resolve-Path -Path $DestinationPath - if($suppliedDestinationPath.Provider.Name-ne "FileSystem") - { - $errorMessage = ($LocalizedData.ExpandArchiveInValidDestinationPath -f $DestinationPath) - ThrowTerminatingErrorHelper "InvalidDirectoryPath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath - } - } - else - { - $createdItem = New-Item -Path $DestinationPath -ItemType Directory -Confirm:$isConfirm -Verbose:$isVerbose -ErrorAction Stop - if($createdItem -ne $null -and $createdItem.PSProvider.Name -ne "FileSystem") - { - Remove-Item "$DestinationPath" -Force -Recurse -ErrorAction SilentlyContinue - $errorMessage = ($LocalizedData.ExpandArchiveInValidDestinationPath -f $DestinationPath) - ThrowTerminatingErrorHelper "InvalidDirectoryPath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath - } - - $resolvedDestinationPath = GetResolvedPathHelper $DestinationPath $true $PSCmdlet - } - } - - $isWhatIf = $psboundparameters.ContainsKey("WhatIf") - if(!$isWhatIf) - { - $preparingToExpandVerboseMessage = ($LocalizedData.PreparingToExpandVerboseMessage) - Write-Verbose $preparingToExpandVerboseMessage - - $progressBarStatus = ($LocalizedData.ExpandProgressBarText -f $DestinationPath) - ProgressBarHelper "Expand-Archive" $progressBarStatus 0 100 100 1 - } - } - PROCESS - { - switch($PsCmdlet.ParameterSetName) - { - "Path" - { - $resolvedSourcePaths = GetResolvedPathHelper $Path $false $PSCmdlet - - if($resolvedSourcePaths.Count -gt 1) - { - $errorMessage = ($LocalizedData.InvalidArchiveFilePathError -f $Path, $PsCmdlet.ParameterSetName, $PsCmdlet.ParameterSetName) - ThrowTerminatingErrorHelper "InvalidArchiveFilePath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $Path - } - } - "LiteralPath" - { - $resolvedSourcePaths = GetResolvedPathHelper $LiteralPath $true $PSCmdlet - - if($resolvedSourcePaths.Count -gt 1) - { - $errorMessage = ($LocalizedData.InvalidArchiveFilePathError -f $LiteralPath, $PsCmdlet.ParameterSetName, $PsCmdlet.ParameterSetName) - ThrowTerminatingErrorHelper "InvalidArchiveFilePath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $LiteralPath - } - } - } - - ValidateArchivePathHelper $resolvedSourcePaths - - if($pscmdlet.ShouldProcess($resolvedSourcePaths)) - { - $expandedItems = @() - - try - { - # StopProcessing is not available in Script cmdlets. However the pipeline execution - # is terminated when ever 'CTRL + C' is entered by user to terminate the cmdlet execution. - # The finally block is executed whenever pipeline is terminated. - # $isArchiveFileProcessingComplete variable is used to track if 'CTRL + C' is entered by the - # user. - $isArchiveFileProcessingComplete = $false - - # The User has not provided a destination path, hence we use '$pwd\ArchiveFileName' as the directory where the - # archive file contents would be expanded. If the path '$pwd\ArchiveFileName' already exists then we use the - # Windows default mechanism of appending a counter value at the end of the directory name where the contents - # would be expanded. - if(!$isDestinationPathProvided) - { - $archiveFile = New-Object System.IO.FileInfo $resolvedSourcePaths - $resolvedDestinationPath = Join-Path -Path $resolvedDestinationPath -ChildPath $archiveFile.BaseName - $destinationPathExists = Test-Path -LiteralPath $resolvedDestinationPath -PathType Container - - if(!$destinationPathExists) - { - New-Item -Path $resolvedDestinationPath -ItemType Directory -Confirm:$isConfirm -Verbose:$isVerbose -ErrorAction Stop | Out-Null - } - } - - ExpandArchiveHelper $resolvedSourcePaths $resolvedDestinationPath ([ref]$expandedItems) $Force $isVerbose $isConfirm - - $isArchiveFileProcessingComplete = $true - } - finally - { - # The $isArchiveFileProcessingComplete would be set to $false if user has typed 'CTRL + C' to - # terminate the cmdlet execution or if an unhandled exception is thrown. - if($isArchiveFileProcessingComplete -eq $false) - { - if($expandedItems.Count -gt 0) - { - # delete the expanded file/directory as the archive - # file was not completely expanded. - $expandedItems | % { Remove-Item "$_" -Force -Recurse } - } - } - elseif ($PassThru -and $expandedItems.Count -gt 0) - { - # Return the expanded items, being careful to remove trailing directory separators from - # any folder paths for consistency - $trailingDirSeparators = '\' + [System.IO.Path]::DirectorySeparatorChar + '+$' - Get-Item -LiteralPath ($expandedItems -replace $trailingDirSeparators) - } - } - } - } -} - -<############################################################################################ -# GetResolvedPathHelper: This is a helper function used to resolve the user specified Path. -# The path can either be absolute or relative path. -############################################################################################> -function GetResolvedPathHelper -{ - param - ( - [string[]] $path, - [boolean] $isLiteralPath, - [System.Management.Automation.PSCmdlet] - $callerPSCmdlet - ) - - $resolvedPaths =@() - - # null and empty check are are already done on Path parameter at the cmdlet layer. - foreach($currentPath in $path) - { - try - { - if($isLiteralPath) - { - $currentResolvedPaths = Resolve-Path -LiteralPath $currentPath -ErrorAction Stop - } - else - { - $currentResolvedPaths = Resolve-Path -Path $currentPath -ErrorAction Stop - } - } - catch - { - $errorMessage = ($LocalizedData.PathNotFoundError -f $currentPath) - $exception = New-Object System.InvalidOperationException $errorMessage, $_.Exception - $errorRecord = CreateErrorRecordHelper "ArchiveCmdletPathNotFound" $null ([System.Management.Automation.ErrorCategory]::InvalidArgument) $exception $currentPath - $callerPSCmdlet.ThrowTerminatingError($errorRecord) - } - - foreach($currentResolvedPath in $currentResolvedPaths) - { - $resolvedPaths += $currentResolvedPath.ProviderPath - } - } - - $resolvedPaths -} - -function Add-CompressionAssemblies { - Add-Type -AssemblyName System.IO.Compression - if ($psedition -eq "Core") - { - Add-Type -AssemblyName System.IO.Compression.ZipFile - } - else - { - Add-Type -AssemblyName System.IO.Compression.FileSystem - } -} - -function IsValidFileSystemPath -{ - param - ( - [string[]] $path - ) - - $result = $true; - - # null and empty check are are already done on Path parameter at the cmdlet layer. - foreach($currentPath in $path) - { - if(!([System.IO.File]::Exists($currentPath) -or [System.IO.Directory]::Exists($currentPath))) - { - $errorMessage = ($LocalizedData.PathNotFoundError -f $currentPath) - ThrowTerminatingErrorHelper "PathNotFound" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $currentPath - } - } - - return $result; -} - - -function ValidateDuplicateFileSystemPath -{ - param - ( - [string] $inputParameter, - [string[]] $path - ) - - $uniqueInputPaths = @() - - # null and empty check are are already done on Path parameter at the cmdlet layer. - foreach($currentPath in $path) - { - $currentInputPath = $currentPath.ToUpper() - if($uniqueInputPaths.Contains($currentInputPath)) - { - $errorMessage = ($LocalizedData.DuplicatePathFoundError -f $inputParameter, $currentPath, $inputParameter) - ThrowTerminatingErrorHelper "DuplicatePathFound" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $currentPath - } - else - { - $uniqueInputPaths += $currentInputPath - } - } -} - -function CompressionLevelMapper -{ - param - ( - [string] $compressionLevel - ) - - $compressionLevelFormat = [System.IO.Compression.CompressionLevel]::Optimal - - # CompressionLevel format is already validated at the cmdlet layer. - switch($compressionLevel.ToString()) - { - "Fastest" - { - $compressionLevelFormat = [System.IO.Compression.CompressionLevel]::Fastest - } - "NoCompression" - { - $compressionLevelFormat = [System.IO.Compression.CompressionLevel]::NoCompression - } - } - - return $compressionLevelFormat -} - -function CompressArchiveHelper -{ - param - ( - [string[]] $sourcePath, - [string] $destinationPath, - [string] $compressionLevel, - [bool] $isUpdateMode - ) - - $numberOfItemsArchived = 0 - $sourceFilePaths = @() - $sourceDirPaths = @() - - foreach($currentPath in $sourcePath) - { - $result = Test-Path -LiteralPath $currentPath -Type Leaf - if($result -eq $true) - { - $sourceFilePaths += $currentPath - } - else - { - $sourceDirPaths += $currentPath - } - } - - # The Source Path contains one or more directory (this directory can have files under it) and no files to be compressed. - if($sourceFilePaths.Count -eq 0 -and $sourceDirPaths.Count -gt 0) - { - $currentSegmentWeight = 100/[double]$sourceDirPaths.Count - $previousSegmentWeight = 0 - foreach($currentSourceDirPath in $sourceDirPaths) - { - $count = CompressSingleDirHelper $currentSourceDirPath $destinationPath $compressionLevel $true $isUpdateMode $previousSegmentWeight $currentSegmentWeight - $numberOfItemsArchived += $count - $previousSegmentWeight += $currentSegmentWeight - } - } - - # The Source Path contains only files to be compressed. - elseIf($sourceFilePaths.Count -gt 0 -and $sourceDirPaths.Count -eq 0) - { - # $previousSegmentWeight is equal to 0 as there are no prior segments. - # $currentSegmentWeight is set to 100 as all files have equal weightage. - $previousSegmentWeight = 0 - $currentSegmentWeight = 100 - - $numberOfItemsArchived = CompressFilesHelper $sourceFilePaths $destinationPath $compressionLevel $isUpdateMode $previousSegmentWeight $currentSegmentWeight - } - # The Source Path contains one or more files and one or more directories (this directory can have files under it) to be compressed. - elseif($sourceFilePaths.Count -gt 0 -and $sourceDirPaths.Count -gt 0) - { - # each directory is considered as an individual segments & all the individual files are clubed in to a separate segment. - $currentSegmentWeight = 100/[double]($sourceDirPaths.Count +1) - $previousSegmentWeight = 0 - - foreach($currentSourceDirPath in $sourceDirPaths) - { - $count = CompressSingleDirHelper $currentSourceDirPath $destinationPath $compressionLevel $true $isUpdateMode $previousSegmentWeight $currentSegmentWeight - $numberOfItemsArchived += $count - $previousSegmentWeight += $currentSegmentWeight - } - - $count = CompressFilesHelper $sourceFilePaths $destinationPath $compressionLevel $isUpdateMode $previousSegmentWeight $currentSegmentWeight - $numberOfItemsArchived += $count - } - - return $numberOfItemsArchived -} - -function CompressFilesHelper -{ - param - ( - [string[]] $sourceFilePaths, - [string] $destinationPath, - [string] $compressionLevel, - [bool] $isUpdateMode, - [double] $previousSegmentWeight, - [double] $currentSegmentWeight - ) - - $numberOfItemsArchived = ZipArchiveHelper $sourceFilePaths $destinationPath $compressionLevel $isUpdateMode $null $previousSegmentWeight $currentSegmentWeight - - return $numberOfItemsArchived -} - -function CompressSingleDirHelper -{ - param - ( - [string] $sourceDirPath, - [string] $destinationPath, - [string] $compressionLevel, - [bool] $useParentDirAsRoot, - [bool] $isUpdateMode, - [double] $previousSegmentWeight, - [double] $currentSegmentWeight - ) - - [System.Collections.Generic.List[System.String]]$subDirFiles = @() - - if($useParentDirAsRoot) - { - $sourceDirInfo = New-Object -TypeName System.IO.DirectoryInfo -ArgumentList $sourceDirPath - $sourceDirFullName = $sourceDirInfo.Parent.FullName - - # If the directory is present at the drive level the DirectoryInfo.Parent include directory separator. example: C:\ - # On the other hand if the directory exists at a deper level then DirectoryInfo.Parent - # has just the path (without an ending directory separator). example C:\source - if($sourceDirFullName.Length -eq 3) - { - $modifiedSourceDirFullName = $sourceDirFullName - } - else - { - $modifiedSourceDirFullName = $sourceDirFullName + [System.IO.Path]::DirectorySeparatorChar - } - } - else - { - $sourceDirFullName = $sourceDirPath - $modifiedSourceDirFullName = $sourceDirFullName + [System.IO.Path]::DirectorySeparatorChar - } - - $dirContents = Get-ChildItem -LiteralPath $sourceDirPath -Recurse - foreach($currentContent in $dirContents) - { - $isContainer = $currentContent -is [System.IO.DirectoryInfo] - if(!$isContainer) - { - $subDirFiles.Add($currentContent.FullName) - } - else - { - # The currentContent points to a directory. - # We need to check if the directory is an empty directory, if so such a - # directory has to be explicitly added to the archive file. - # if there are no files in the directory the GetFiles() API returns an empty array. - $files = $currentContent.GetFiles() - if($files.Count -eq 0) - { - $subDirFiles.Add($currentContent.FullName + [System.IO.Path]::DirectorySeparatorChar) - } - } - } - - $numberOfItemsArchived = ZipArchiveHelper $subDirFiles.ToArray() $destinationPath $compressionLevel $isUpdateMode $modifiedSourceDirFullName $previousSegmentWeight $currentSegmentWeight - - return $numberOfItemsArchived -} - -function ZipArchiveHelper -{ - param - ( - [System.Collections.Generic.List[System.String]] $sourcePaths, - [string] $destinationPath, - [string] $compressionLevel, - [bool] $isUpdateMode, - [string] $modifiedSourceDirFullName, - [double] $previousSegmentWeight, - [double] $currentSegmentWeight - ) - - $numberOfItemsArchived = 0 - $fileMode = [System.IO.FileMode]::Create - $result = Test-Path -LiteralPath $DestinationPath -Type Leaf - if($result -eq $true) - { - $fileMode = [System.IO.FileMode]::Open - } - - Add-CompressionAssemblies - - try - { - # At this point we are sure that the archive file has write access. - $archiveFileStreamArgs = @($destinationPath, $fileMode) - $archiveFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList $archiveFileStreamArgs - - $zipArchiveArgs = @($archiveFileStream, [System.IO.Compression.ZipArchiveMode]::Update, $false) - $zipArchive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $zipArchiveArgs - - $currentEntryCount = 0 - $progressBarStatus = ($LocalizedData.CompressProgressBarText -f $destinationPath) - $bufferSize = 4kb - $buffer = New-Object Byte[] $bufferSize - - foreach($currentFilePath in $sourcePaths) - { - if($modifiedSourceDirFullName -ne $null -and $modifiedSourceDirFullName.Length -gt 0) - { - $index = $currentFilePath.IndexOf($modifiedSourceDirFullName, [System.StringComparison]::OrdinalIgnoreCase) - $currentFilePathSubString = $currentFilePath.Substring($index, $modifiedSourceDirFullName.Length) - $relativeFilePath = $currentFilePath.Replace($currentFilePathSubString, "").Trim() - } - else - { - $relativeFilePath = [System.IO.Path]::GetFileName($currentFilePath) - } - - # Update mode is selected. - # Check to see if archive file already contains one or more zip files in it. - if($isUpdateMode -eq $true -and $zipArchive.Entries.Count -gt 0) - { - $entryToBeUpdated = $null - - # Check if the file already exists in the archive file. - # If so replace it with new file from the input source. - # If the file does not exist in the archive file then default to - # create mode and create the entry in the archive file. - - foreach($currentArchiveEntry in $zipArchive.Entries) - { - if(ArchivePathCompareHelper $currentArchiveEntry.FullName $relativeFilePath) - { - $entryToBeUpdated = $currentArchiveEntry - break - } - } - - if($entryToBeUpdated -ne $null) - { - $addItemtoArchiveFileMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentFilePath) - $entryToBeUpdated.Delete() - } - } - - $compression = CompressionLevelMapper $compressionLevel - - # If a directory needs to be added to an archive file, - # by convention the .Net API's expect the path of the directory - # to end with directory separator to detect the path as an directory. - if(!$relativeFilePath.EndsWith([System.IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) - { - try - { - try - { - $currentFileStream = [System.IO.File]::Open($currentFilePath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::Read) - } - catch - { - # Failed to access the file. Write a non terminating error to the pipeline - # and move on with the remaining files. - $exception = $_.Exception - if($null -ne $_.Exception -and - $null -ne $_.Exception.InnerException) - { - $exception = $_.Exception.InnerException - } - $errorRecord = CreateErrorRecordHelper "CompressArchiveUnauthorizedAccessError" $null ([System.Management.Automation.ErrorCategory]::PermissionDenied) $exception $currentFilePath - Write-Error -ErrorRecord $errorRecord - } - - if($null -ne $currentFileStream) - { - $srcStream = New-Object System.IO.BinaryReader $currentFileStream - - $entryPath = DirectorySeparatorNormalizeHelper $relativeFilePath - $currentArchiveEntry = $zipArchive.CreateEntry($entryPath, $compression) - - # Updating the File Creation time so that the same timestamp would be retained after expanding the compressed file. - # At this point we are sure that Get-ChildItem would succeed. - $lastWriteTime = (Get-Item -LiteralPath $currentFilePath).LastWriteTime - if ($lastWriteTime.Year -lt 1980) - { - Write-Warning "'$currentFilePath' has LastWriteTime earlier than 1980. Compress-Archive will store any files with LastWriteTime values earlier than 1980 as 1/1/1980 00:00." - $lastWriteTime = [DateTime]::Parse('1980-01-01T00:00:00') - } - - $currentArchiveEntry.LastWriteTime = $lastWriteTime - - $destStream = New-Object System.IO.BinaryWriter $currentArchiveEntry.Open() - - while($numberOfBytesRead = $srcStream.Read($buffer, 0, $bufferSize)) - { - $destStream.Write($buffer, 0, $numberOfBytesRead) - $destStream.Flush() - } - - $numberOfItemsArchived += 1 - $addItemtoArchiveFileMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentFilePath) - } - } - finally - { - If($null -ne $currentFileStream) - { - $currentFileStream.Dispose() - } - If($null -ne $srcStream) - { - $srcStream.Dispose() - } - If($null -ne $destStream) - { - $destStream.Dispose() - } - } - } - else - { - $entryPath = DirectorySeparatorNormalizeHelper $relativeFilePath - $currentArchiveEntry = $zipArchive.CreateEntry($entryPath, $compression) - $numberOfItemsArchived += 1 - $addItemtoArchiveFileMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentFilePath) - } - - if($null -ne $addItemtoArchiveFileMessage) - { - Write-Verbose $addItemtoArchiveFileMessage - } - - $currentEntryCount += 1 - ProgressBarHelper "Compress-Archive" $progressBarStatus $previousSegmentWeight $currentSegmentWeight $sourcePaths.Count $currentEntryCount - } - } - finally - { - If($null -ne $zipArchive) - { - $zipArchive.Dispose() - } - - If($null -ne $archiveFileStream) - { - $archiveFileStream.Dispose() - } - - # Complete writing progress. - Write-Progress -Activity "Compress-Archive" -Completed - } - - return $numberOfItemsArchived -} - -<############################################################################################ -# ValidateArchivePathHelper: This is a helper function used to validate the archive file -# path & its file format. The only supported archive file format is .zip -############################################################################################> -function ValidateArchivePathHelper -{ - param - ( - [string] $archiveFile - ) - - if(-not [System.IO.File]::Exists($archiveFile)) - { - $errorMessage = ($LocalizedData.PathNotFoundError -f $archiveFile) - ThrowTerminatingErrorHelper "PathNotFound" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $archiveFile - } -} - -<############################################################################################ -# ExpandArchiveHelper: This is a helper function used to expand the archive file contents -# to the specified directory. -############################################################################################> -function ExpandArchiveHelper -{ - param - ( - [string] $archiveFile, - [string] $expandedDir, - [ref] $expandedItems, - [boolean] $force, - [boolean] $isVerbose, - [boolean] $isConfirm - ) - - Add-CompressionAssemblies - - try - { - # The existence of archive file has already been validated by ValidateArchivePathHelper - # before calling this helper function. - $archiveFileStreamArgs = @($archiveFile, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) - $archiveFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList $archiveFileStreamArgs - - $zipArchiveArgs = @($archiveFileStream, [System.IO.Compression.ZipArchiveMode]::Read, $false) - try - { - $zipArchive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $zipArchiveArgs - } - catch [System.IO.InvalidDataException] - { - # Failed to open the file for reading as a zip archive. Wrap the exception - # and re-throw it indicating it does not appear to be a valid zip file. - $exception = $_.Exception - if($null -ne $_.Exception -and - $null -ne $_.Exception.InnerException) - { - $exception = $_.Exception.InnerException - } - # Load the WindowsBase.dll assembly to get access to the System.IO.FileFormatException class - [System.Reflection.Assembly]::Load('WindowsBase,Version=4.0.0.0,Culture=neutral,PublicKeyToken=31bf3856ad364e35') - $invalidFileFormatException = New-Object -TypeName System.IO.FileFormatException -ArgumentList @( - ($LocalizedData.ItemDoesNotAppearToBeAValidZipArchive -f $archiveFile) - $exception - ) - throw $invalidFileFormatException - } - - if($zipArchive.Entries.Count -eq 0) - { - $archiveFileIsEmpty = ($LocalizedData.ArchiveFileIsEmpty -f $archiveFile) - Write-Verbose $archiveFileIsEmpty - return - } - - $currentEntryCount = 0 - $progressBarStatus = ($LocalizedData.ExpandProgressBarText -f $archiveFile) - - # Ensures that the last character on the extraction path is the directory separator char. - # Without this, a bad zip file could try to traverse outside of the expected extraction path. - # At this point $expandedDir is a fully qualified path without any relative segments. - if (-not $expandedDir.EndsWith([System.IO.Path]::DirectorySeparatorChar)) - { - $expandedDir += [System.IO.Path]::DirectorySeparatorChar - } - - # The archive entries can either be empty directories or files. - foreach($currentArchiveEntry in $zipArchive.Entries) - { - # Windows filesystem provider will internally convert from `/` to `\` - $currentArchiveEntryPath = Join-Path -Path $expandedDir -ChildPath $currentArchiveEntry.FullName - - # Remove possible relative segments from target - # This is similar to [System.IO.Path]::GetFullPath($currentArchiveEntryPath) but uses PS current dir instead of process-wide current dir - $currentArchiveEntryPath = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($currentArchiveEntryPath) - - # Check that expanded relative paths and absolute paths from the archive are Not going outside of target directory - # Ordinal match is safest, case-sensitive volumes can be mounted within volumes that are case-insensitive. - if (-not ($currentArchiveEntryPath.StartsWith($expandedDir, [System.StringComparison]::Ordinal))) - { - $BadArchiveEntryMessage = ($LocalizedData.BadArchiveEntry -f $currentArchiveEntry.FullName) - # notify user of bad archive entry - Write-Error $BadArchiveEntryMessage - # move on to the next entry in the archive - continue - } - - $extension = [system.IO.Path]::GetExtension($currentArchiveEntryPath) - - # The current archive entry is an empty directory - # The FullName of the Archive Entry representing a directory would end with a trailing directory separator. - if($extension -eq [string]::Empty -and - $currentArchiveEntryPath.EndsWith([System.IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) - { - $pathExists = Test-Path -LiteralPath $currentArchiveEntryPath - - # The current archive entry expects an empty directory. - # Check if the existing directory is empty. If it's not empty - # then it means that user has added this directory by other means. - if($pathExists -eq $false) - { - New-Item $currentArchiveEntryPath -Type Directory -Confirm:$isConfirm | Out-Null - - if(Test-Path -LiteralPath $currentArchiveEntryPath -PathType Container) - { - $addEmptyDirectorytoExpandedPathMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentArchiveEntryPath) - Write-Verbose $addEmptyDirectorytoExpandedPathMessage - - $expandedItems.Value += $currentArchiveEntryPath - } - } - } - else - { - try - { - $currentArchiveEntryFileInfo = New-Object -TypeName System.IO.FileInfo -ArgumentList $currentArchiveEntryPath - $parentDirExists = Test-Path -LiteralPath $currentArchiveEntryFileInfo.DirectoryName -PathType Container - - # If the Parent directory of the current entry in the archive file does not exist, then create it. - if($parentDirExists -eq $false) - { - # note that if any ancestor of this directory doesn't exist, we don't recursively create each one as New-Item - # takes care of this already, so only one DirectoryInfo is returned instead of one for each parent directory - # that only contains directories - New-Item $currentArchiveEntryFileInfo.DirectoryName -Type Directory -Confirm:$isConfirm | Out-Null - - if(!(Test-Path -LiteralPath $currentArchiveEntryFileInfo.DirectoryName -PathType Container)) - { - # The directory referred by $currentArchiveEntryFileInfo.DirectoryName was not successfully created. - # This could be because the user has specified -Confirm parameter when Expand-Archive was invoked - # and authorization was not provided when confirmation was prompted. In such a scenario, - # we skip the current file in the archive and continue with the remaining archive file contents. - Continue - } - - $expandedItems.Value += $currentArchiveEntryFileInfo.DirectoryName - } - - $hasNonTerminatingError = $false - - # Check if the file in to which the current archive entry contents - # would be expanded already exists. - if($currentArchiveEntryFileInfo.Exists) - { - if($force) - { - Remove-Item -LiteralPath $currentArchiveEntryFileInfo.FullName -Force -ErrorVariable ev -Verbose:$isVerbose -Confirm:$isConfirm - if($ev -ne $null) - { - $hasNonTerminatingError = $true - } - - if(Test-Path -LiteralPath $currentArchiveEntryFileInfo.FullName -PathType Leaf) - { - # The file referred by $currentArchiveEntryFileInfo.FullName was not successfully removed. - # This could be because the user has specified -Confirm parameter when Expand-Archive was invoked - # and authorization was not provided when confirmation was prompted. In such a scenario, - # we skip the current file in the archive and continue with the remaining archive file contents. - Continue - } - } - else - { - # Write non-terminating error to the pipeline. - $errorMessage = ($LocalizedData.FileExistsError -f $currentArchiveEntryFileInfo.FullName, $archiveFile, $currentArchiveEntryFileInfo.FullName, $currentArchiveEntryFileInfo.FullName) - $errorRecord = CreateErrorRecordHelper "ExpandArchiveFileExists" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidOperation) $null $currentArchiveEntryFileInfo.FullName - Write-Error -ErrorRecord $errorRecord - $hasNonTerminatingError = $true - } - } - - if(!$hasNonTerminatingError) - { - # The ExtractToFile() method doesn't handle whitespace correctly, strip whitespace which is consistent with how Explorer handles archives - # There is an edge case where an archive contains files whose only difference is whitespace, but this is uncommon and likely not legitimate - [string[]] $parts = $currentArchiveEntryPath.Split([System.IO.Path]::DirectorySeparatorChar) | % { $_.Trim() } - $currentArchiveEntryPath = [string]::Join([System.IO.Path]::DirectorySeparatorChar, $parts) - - [System.IO.Compression.ZipFileExtensions]::ExtractToFile($currentArchiveEntry, $currentArchiveEntryPath, $false) - - # Add the expanded file path to the $expandedItems array, - # to keep track of all the expanded files created while expanding the archive file. - # If user enters CTRL + C then at that point of time, all these expanded files - # would be deleted as part of the clean up process. - $expandedItems.Value += $currentArchiveEntryPath - - $addFiletoExpandedPathMessage = ($LocalizedData.CreateFileAtExpandedPath -f $currentArchiveEntryPath) - Write-Verbose $addFiletoExpandedPathMessage - } - } - finally - { - If($null -ne $destStream) - { - $destStream.Dispose() - } - - If($null -ne $srcStream) - { - $srcStream.Dispose() - } - } - } - - $currentEntryCount += 1 - # $currentSegmentWeight is Set to 100 giving equal weightage to each file that is getting expanded. - # $previousSegmentWeight is set to 0 as there are no prior segments. - $previousSegmentWeight = 0 - $currentSegmentWeight = 100 - ProgressBarHelper "Expand-Archive" $progressBarStatus $previousSegmentWeight $currentSegmentWeight $zipArchive.Entries.Count $currentEntryCount - } - } - finally - { - If($null -ne $zipArchive) - { - $zipArchive.Dispose() - } - - If($null -ne $archiveFileStream) - { - $archiveFileStream.Dispose() - } - - # Complete writing progress. - Write-Progress -Activity "Expand-Archive" -Completed - } -} - -<############################################################################################ -# ProgressBarHelper: This is a helper function used to display progress message. -# This function is used by both Compress-Archive & Expand-Archive to display archive file -# creation/expansion progress. -############################################################################################> -function ProgressBarHelper -{ - param - ( - [string] $cmdletName, - [string] $status, - [double] $previousSegmentWeight, - [double] $currentSegmentWeight, - [int] $totalNumberofEntries, - [int] $currentEntryCount - ) - - if($currentEntryCount -gt 0 -and - $totalNumberofEntries -gt 0 -and - $previousSegmentWeight -ge 0 -and - $currentSegmentWeight -gt 0) - { - $entryDefaultWeight = $currentSegmentWeight/[double]$totalNumberofEntries - - $percentComplete = $previousSegmentWeight + ($entryDefaultWeight * $currentEntryCount) - Write-Progress -Activity $cmdletName -Status $status -PercentComplete $percentComplete - } -} - -<############################################################################################ -# CSVHelper: This is a helper function used to append comma after each path specified by -# the SourcePath array. This helper function is used to display all the user supplied paths -# in the WhatIf message. -############################################################################################> -function CSVHelper -{ - param - ( - [string[]] $sourcePath - ) - - # SourcePath has already been validated by the calling function. - if($sourcePath.Count -gt 1) - { - $sourcePathInCsvFormat = "`n" - for($currentIndex=0; $currentIndex -lt $sourcePath.Count; $currentIndex++) - { - if($currentIndex -eq $sourcePath.Count - 1) - { - $sourcePathInCsvFormat += $sourcePath[$currentIndex] - } - else - { - $sourcePathInCsvFormat += $sourcePath[$currentIndex] + "`n" - } - } - } - else - { - $sourcePathInCsvFormat = $sourcePath - } - - return $sourcePathInCsvFormat -} - -<############################################################################################ -# ThrowTerminatingErrorHelper: This is a helper function used to throw terminating error. -############################################################################################> -function ThrowTerminatingErrorHelper -{ - param - ( - [string] $errorId, - [string] $errorMessage, - [System.Management.Automation.ErrorCategory] $errorCategory, - [object] $targetObject, - [Exception] $innerException - ) - - if($innerException -eq $null) - { - $exception = New-object System.IO.IOException $errorMessage - } - else - { - $exception = New-Object System.IO.IOException $errorMessage, $innerException - } - - $exception = New-Object System.IO.IOException $errorMessage - $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $errorId, $errorCategory, $targetObject - $PSCmdlet.ThrowTerminatingError($errorRecord) -} - -<############################################################################################ -# CreateErrorRecordHelper: This is a helper function used to create an ErrorRecord -############################################################################################> -function CreateErrorRecordHelper -{ - param - ( - [string] $errorId, - [string] $errorMessage, - [System.Management.Automation.ErrorCategory] $errorCategory, - [Exception] $exception, - [object] $targetObject - ) - - if($null -eq $exception) - { - $exception = New-Object System.IO.IOException $errorMessage - } - - $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $errorId, $errorCategory, $targetObject - return $errorRecord -} - -<############################################################################################ -# DirectorySeparatorNormalizeHelper: This is a helper function used to normalize separators -# when compressing archives, creating cross platform archives. -# -# The approach taken is leveraging the fact that .net on Windows all the way back to -# Framework 1.1 specifies `\` as DirectoryPathSeparatorChar and `/` as -# AltDirectoryPathSeparatorChar, while other platforms in .net Core use `/` for -# DirectoryPathSeparatorChar and AltDirectoryPathSeparatorChar. When using a *nix platform, -# the replacements will be no-ops, while Windows will convert all `\` to `/` for the -# purposes of the ZipEntry FullName. -############################################################################################> -function DirectorySeparatorNormalizeHelper -{ - param - ( - [string] $archivePath - ) - - if($null -eq $archivePath) - { - return $archivePath - } - - return $archivePath.replace([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar) -} - -<############################################################################################ -# ArchivePathCompareHelper: This is a helper function used to compare with normalized -# separators. -############################################################################################> -function ArchivePathCompareHelper -{ - param - ( - [string] $pathArgA, - [string] $pathArgB - ) - - $normalizedPathArgA = DirectorySeparatorNormalizeHelper $pathArgA - $normalizedPathArgB = DirectorySeparatorNormalizeHelper $pathArgB - - return $normalizedPathArgA -eq $normalizedPathArgB -} diff --git a/Microsoft.PowerShell.Archive/en-US/ArchiveResources.psd1 b/Microsoft.PowerShell.Archive/en-US/ArchiveResources.psd1 deleted file mode 100644 index d3b713d..0000000 --- a/Microsoft.PowerShell.Archive/en-US/ArchiveResources.psd1 +++ /dev/null @@ -1,26 +0,0 @@ -# Localized ArchiveResources.psd1 - -ConvertFrom-StringData @' -###PSLOC -PathNotFoundError=The path '{0}' either does not exist or is not a valid file system path. -ExpandArchiveInValidDestinationPath=The path '{0}' is not a valid file system directory path. -InvalidZipFileExtensionError={0} is not a supported archive file format. {1} is the only supported archive file format. -ArchiveFileIsReadOnly=The attributes of the archive file {0} is set to 'ReadOnly' hence it cannot be updated. If you intend to update the existing archive file, remove the 'ReadOnly' attribute on the archive file else use -Force parameter to override and create a new archive file. -ZipFileExistError=The archive file {0} already exists. Use the -Update parameter to update the existing archive file or use the -Force parameter to overwrite the existing archive file. -DuplicatePathFoundError=The input to {0} parameter contains a duplicate path '{1}'. Provide a unique set of paths as input to {2} parameter. -ArchiveFileIsEmpty=The archive file {0} is empty. -CompressProgressBarText=The archive file '{0}' creation is in progress... -ExpandProgressBarText=The archive file '{0}' expansion is in progress... -AppendArchiveFileExtensionMessage=The archive file path '{0}' supplied to the DestinationPath parameter does not include .zip extension. Hence .zip is appended to the supplied DestinationPath path and the archive file would be created at '{1}'. -AddItemtoArchiveFile=Adding '{0}'. -BadArchiveEntry=Can not process invalid archive entry '{0}'. -CreateFileAtExpandedPath=Created '{0}'. -InvalidArchiveFilePathError=The archive file path '{0}' specified as input to the {1} parameter is resolving to multiple file system paths. Provide a unique path to the {2} parameter where the archive file has to be created. -InvalidExpandedDirPathError=The directory path '{0}' specified as input to the DestinationPath parameter is resolving to multiple file system paths. Provide a unique path to the Destination parameter where the archive file contents have to be expanded. -FileExistsError=Failed to create file '{0}' while expanding the archive file '{1}' contents as the file '{2}' already exists. Use the -Force parameter if you want to overwrite the existing directory '{3}' contents when expanding the archive file. -DeleteArchiveFile=The partially created archive file '{0}' is deleted as it is not usable. -InvalidDestinationPath=The destination path '{0}' does not contain a valid archive file name. -PreparingToCompressVerboseMessage=Preparing to compress... -PreparingToExpandVerboseMessage=Preparing to expand... -###PSLOC -'@ diff --git a/SimpleBuild.ps1 b/SimpleBuild.ps1 index 9ef520b..9e1dda6 100644 --- a/SimpleBuild.ps1 +++ b/SimpleBuild.ps1 @@ -1,20 +1,17 @@ -if ((Test-Path "$PSScriptRoot\out")) { - Remove-Item -Path $PSScriptRoot\out -Recurse -Force -} - -New-Item -ItemType directory -Path $PSScriptRoot\out | Out-Null -New-Item -ItemType directory -Path $PSScriptRoot\out\Microsoft.PowerShell.Archive | Out-Null +$buildOutputDirectory = "$PSScriptRoot\src\build\net7.0" -$OutPath = Join-Path $PSScriptRoot "out" -$OutModulePath = Join-Path $OutPath "Microsoft.PowerShell.Archive" +if ((Test-Path $buildOutputDirectory)) { + Remove-Item -Path $buildOutputDirectory -Recurse -Force +} -Copy-Item -Recurse -Path "$PSScriptRoot\Microsoft.PowerShell.Archive" -Destination $OutPath -Force +# Perform dotnet build +dotnet build .\src\Microsoft.PowerShell.Archive.csproj -c release -"Build module location: $OutModulePath" | Write-Verbose -Verbose +"Build module location: $buildOutputDirectory" | Write-Verbose -Verbose -"Setting VSTS variable 'BuildOutDir' to '$OutModulePath'" | Write-Verbose -Verbose -Write-Host "##vso[task.setvariable variable=BuildOutDir]$OutModulePath" +"Setting VSTS variable 'BuildOutDir' to '$buildOutputDirectory'" | Write-Verbose -Verbose +Write-Host "##vso[task.setvariable variable=BuildOutDir]$buildOutputDirectory" -$psd1ModuleVersion = (Get-Content -Path "$OutModulePath\Microsoft.PowerShell.Archive.psd1" | Select-String 'ModuleVersion="(.*)"').Matches[0].Groups[1].Value +$psd1ModuleVersion = (Get-Content -Path "$buildOutputDirectory\Microsoft.PowerShell.Archive.psd1" | Select-String 'ModuleVersion="(.*)"').Matches[0].Groups[1].Value "Setting VSTS variable 'PackageVersion' to '$psd1ModuleVersion'" | Write-Verbose -Verbose Write-Host "##vso[task.setvariable variable=PackageVersion]$psd1ModuleVersion" diff --git a/src/PathHelper.cs b/src/PathHelper.cs index 49d8d30..b0e8a45 100644 --- a/src/PathHelper.cs +++ b/src/PathHelper.cs @@ -124,10 +124,12 @@ private void AddArchiveAdditionForUserEnteredNonLiteralPath(string path, List // Add directory seperator to end if it does not already have it if (!path.EndsWith(System.IO.Path.DirectorySeparatorChar)) path += System.IO.Path.DirectorySeparatorChar; // Recurse through the child items and add them to additions - AddDescendentEntries(path: path, additions: additions, shouldPreservePathStructure: shouldPreservePathStructure); + var directoryInfo = new System.IO.DirectoryInfo(path); + AddDescendentEntries(directoryInfo: directoryInfo, additions: additions, shouldPreservePathStructure: shouldPreservePathStructure); additionType = ArchiveAddition.ArchiveAdditionType.Directory; } else if (!System.IO.File.Exists(path)) @@ -228,11 +233,10 @@ private void AddAdditionForFullyQualifiedPath(string path, List /// A fully qualifed path referring to a directory /// Where the ArchiveAddtion object for each child item of the directory will be added /// See above - private void AddDescendentEntries(string path, List additions, bool shouldPreservePathStructure) + private void AddDescendentEntries(System.IO.DirectoryInfo directoryInfo, List additions, bool shouldPreservePathStructure) { try { - var directoryInfo = new System.IO.DirectoryInfo(path); // pathPrefix is used to construct the entry names of the descendents of the directory var pathPrefix = GetPrefixForPath(directoryInfo: directoryInfo); foreach (var childPath in directoryInfo.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)) @@ -249,19 +253,13 @@ private void AddDescendentEntries(string path, List additions, additions.Add(new ArchiveAddition(entryName: entryName, fullPath: childPath.FullName, type: type)); } } - // Throw a non-terminating error if a securityException occurs + // Write a non-terminating error if a securityException occurs catch (System.Security.SecurityException securityException) { var errorId = ErrorCode.InsufficientPermissionsToAccessPath.ToString(); - var errorRecord = new ErrorRecord(securityException, errorId: errorId, ErrorCategory.SecurityError, targetObject: path); + var errorRecord = new ErrorRecord(securityException, errorId: errorId, ErrorCategory.SecurityError, targetObject: directoryInfo); _cmdlet.WriteError(errorRecord); } - // Throw a terminating error if a directoryNotFoundException occurs - catch (System.IO.DirectoryNotFoundException) - { - var errorRecord = ErrorMessages.GetErrorRecord(errorCode: ErrorCode.PathNotFound, errorItem: path); - _cmdlet.ThrowTerminatingError(errorRecord); - } } /// @@ -281,9 +279,20 @@ private string GetEntryName(string path, bool shouldPreservePathStructure) if (path.EndsWith(System.IO.Path.DirectorySeparatorChar)) { // Get substring from second-last directory seperator char till end - int secondLastIndex = path.LastIndexOf(System.IO.Path.DirectorySeparatorChar, path.Length - 2); - if (secondLastIndex == -1) return path; - else return path.Substring(secondLastIndex + 1); + if (path.Length - 2 < 0) + { + return path; + } + + int secondLastIndex = path.LastIndexOf(value: System.IO.Path.DirectorySeparatorChar, startIndex: path.Length - 2); + if (secondLastIndex == -1) + { + return path; + } + else + { + return path.Substring(secondLastIndex + 1); + } } else { @@ -295,13 +304,17 @@ private string GetEntryName(string path, string prefix) { if (prefix == String.Empty) return path; - //If the path does not start with the prefix, throw an exception + // If the path does not start with the prefix, throw an exception if (!path.StartsWith(prefix)) { throw new ArgumentException($"{path} does not begin with {prefix}"); } - if (path.Length <= prefix.Length) throw new ArgumentException($"The length of {path} is shorter than or equal to the length of {prefix}"); + // If the path is the same length as the prefix + if (path.Length == prefix.Length) + { + throw new ArgumentException($"The length of {path} is shorter than or equal to the length of {prefix}"); + } string entryName = path.Substring(prefix.Length); From 5b355a65b2f7fd360edf7fe40aa4b3ba9fa8866c Mon Sep 17 00:00:00 2001 From: Abdullah Yousuf Date: Mon, 25 Jul 2022 10:00:36 -0700 Subject: [PATCH 25/42] used FileSystemInfo instead of String to store full path information --- Tests/Compress-Archive.Tests.ps1 | 59 +++++++++++++++++++------- src/ArchiveAddition.cs | 11 ++--- src/CompressArchiveCommand.cs | 71 ++++++++++++++++---------------- src/PathHelper.cs | 71 ++++++++++++++++++-------------- src/TarArchive.cs | 4 +- src/ZipArchive.cs | 4 +- 6 files changed, 128 insertions(+), 92 deletions(-) diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index 856cd1f..8d23fb5 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -70,6 +70,29 @@ if ($null -ne $archiveFileStream) { $archiveFileStream.Dispose() } } } + + # This function gets a list of a directories descendants formatted as archive entries + function Get-Descendants { + param ( + [string] $Path + ) + + + # Get the folder name + $folderName = Split-Path -Path $Path -Leaf + + # Get descendents + $descendants = Get-ChildItem -Path $Path -Recurse -Name + + $output = @() + + # Prefix each descendant name with folder name + foreach ($name in $descendants) { + $output += ($folderName + '/' + $name).Replace([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar) + } + + return $output + } } AfterAll { @@ -197,7 +220,7 @@ } catch { - $_.FullyQualifiedErrorId | Should -Be "DuplicatePathFound,Microsoft.PowerShell.Archive.CompressArchiveCommand" + $_.FullyQualifiedErrorId | Should -Be "DuplicatePaths,Microsoft.PowerShell.Archive.CompressArchiveCommand" } } @@ -214,7 +237,7 @@ } catch { - $_.FullyQualifiedErrorId | Should -Be "DuplicatePathFound,Microsoft.PowerShell.Archive.CompressArchiveCommand" + $_.FullyQualifiedErrorId | Should -Be "DuplicatePaths,Microsoft.PowerShell.Archive.CompressArchiveCommand" } } @@ -235,11 +258,13 @@ } } - It "Throws an error when Path and DestinationPath are the same" { - $sourcePath = "$TestDrive$($DS)EmptyDirectory" + # This cannot happen in -WriteMode Create because another error will be throw before + It "Throws an error when Path and DestinationPath are the same" -Skip { + $sourcePath = "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt" $destinationPath = $sourcePath try { + # Note the cmdlet performs validation on $destinationPath Compress-Archive -Path $sourcePath -DestinationPath $destinationPath throw "Failed to detect an error when Path and DestinationPath are the same" } catch { @@ -248,31 +273,31 @@ } It "Throws an error when Path and DestinationPath are the same and -Update is specified" { - $sourcePath = "$TestDrive$($DS)EmptyDirectory" + $sourcePath = "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt" $destinationPath = $sourcePath try { - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Update + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Update throw "Failed to detect an error when Path and DestinationPath are the same and -Update is specified" } catch { $_.FullyQualifiedErrorId | Should -Be "SamePathAndDestinationPath,Microsoft.PowerShell.Archive.CompressArchiveCommand" } } - It "Throws an error when Path and DestinationPath are the same and -Overwrite is specified" { + It "Throws an error when Path and DestinationPath are the same and -Overwrite is specified" -Tag td { $sourcePath = "$TestDrive$($DS)EmptyDirectory" $destinationPath = $sourcePath try { - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Overwrite + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite throw "Failed to detect an error when Path and DestinationPath are the same and -Overwrite is specified" } catch { $_.FullyQualifiedErrorId | Should -Be "SamePathAndDestinationPath,Microsoft.PowerShell.Archive.CompressArchiveCommand" } } - It "Throws an error when LiteralPath and DestinationPath are the same" { - $sourcePath = "$TestDrive$($DS)EmptyDirectory" + It "Throws an error when LiteralPath and DestinationPath are the same" -Skip { + $sourcePath = "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt" $destinationPath = $sourcePath try { @@ -284,14 +309,14 @@ } It "Throws an error when LiteralPath and DestinationPath are the same and -Update is specified" { - $sourcePath = "$TestDrive$($DS)EmptyDirectory" + $sourcePath = "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt" $destinationPath = $sourcePath try { - Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath -Update + Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath -WriteMode Update throw "Failed to detect an error when LiteralPath and DestinationPath are the same and -Update is specified" } catch { - $_.FullyQualifiedErrorId | Should -Be "SamePathAndDestinationPath,Microsoft.PowerShell.Archive.CompressArchiveCommand" + $_.FullyQualifiedErrorId | Should -Be "SameLiteralPathAndDestinationPath,Microsoft.PowerShell.Archive.CompressArchiveCommand" } } @@ -300,10 +325,10 @@ $destinationPath = $sourcePath try { - Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath -Overwrite + Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite throw "Failed to detect an error when LiteralPath and DestinationPath are the same and -Overwrite is specified" } catch { - $_.FullyQualifiedErrorId | Should -Be "SamePathAndDestinationPath,Microsoft.PowerShell.Archive.CompressArchiveCommand" + $_.FullyQualifiedErrorId | Should -Be "SameLiteralPathAndDestinationPath,Microsoft.PowerShell.Archive.CompressArchiveCommand" } } @@ -347,6 +372,8 @@ $destinationPath = "$TestDrive$($DS)archive3.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath $destinationPath | Should -Exist + $contents = Get-Descendants -Path $sourcePath + $contents += "SourceDir/" Test-ZipArchive $destinationPath @("SourceDir/", "Sample-2.txt") } } @@ -532,7 +559,7 @@ } } - Contect "Special and Wildcard Characters Tests" { + Context "Special and Wildcard Characters Tests" { BeforeAll { New-Item $TestDrive$($DS)SourceDir -Type Directory | Out-Null diff --git a/src/ArchiveAddition.cs b/src/ArchiveAddition.cs index 5092300..d0ae1f1 100644 --- a/src/ArchiveAddition.cs +++ b/src/ArchiveAddition.cs @@ -19,18 +19,19 @@ internal class ArchiveAddition /// /// The fully qualified path of the file or directory to add to or update in the archive. /// - public string FullPath { get; set; } + //public string FullPath { get; set; } + + public System.IO.FileSystemInfo FileSystemInfo { get; set; } /// /// The type of filesystem entry to add. /// - public ArchiveAdditionType Type { get; set; } + //public ArchiveAdditionType Type { get; set; } - public ArchiveAddition(string entryName, string fullPath, ArchiveAdditionType type) + public ArchiveAddition(string entryName, System.IO.FileSystemInfo fileSystemInfo) { EntryName = entryName; - FullPath = fullPath; - Type = type; + FileSystemInfo = fileSystemInfo; } /// diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index 206d04f..bfb1adc 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -68,6 +68,8 @@ public class CompressArchiveCommand : PSCmdlet private PathHelper _pathHelper; + private System.IO.FileSystemInfo? _destinationPathInfo; + private bool _didCreateNewArchive; public CompressArchiveCommand() @@ -76,17 +78,18 @@ public CompressArchiveCommand() _pathHelper = new PathHelper(this); Messages.Culture = new System.Globalization.CultureInfo("en-US"); _didCreateNewArchive = false; + _destinationPathInfo = null; } protected override void BeginProcessing() { - DestinationPath = _pathHelper.ResolveToSingleFullyQualifiedPath(DestinationPath); + _destinationPathInfo = _pathHelper.ResolveToSingleFullyQualifiedPath(DestinationPath); - // Validate DestinationPath + // Validate ValidateDestinationPath(); - - + // Determine archive format based on DestinationPath + DetermineArchiveFormat(); } protected override void ProcessRecord() @@ -116,12 +119,12 @@ protected override void EndProcessing() // Throw a terminating error if there is a source path as same as DestinationPath. // We don't want to overwrite the file or directory that we want to add to the archive. - var additionsWithSamePathAsDestination = archiveAddtions.Where(addition => addition.FullPath == DestinationPath).ToList(); + var additionsWithSamePathAsDestination = archiveAddtions.Where(addition => addition.FileSystemInfo == _destinationPathInfo).ToList(); if (additionsWithSamePathAsDestination.Count() > 0) { // Since duplicate checking is performed earlier, there must a single ArchiveAddition such that ArchiveAddition.FullPath == DestinationPath var errorCode = ParameterSetName.StartsWith("Path") ? ErrorCode.SamePathAndDestinationPath : ErrorCode.SameLiteralPathAndDestinationPath; - var errorRecord = ErrorMessages.GetErrorRecord(errorCode, errorItem: additionsWithSamePathAsDestination[0].FullPath); + var errorRecord = ErrorMessages.GetErrorRecord(errorCode, errorItem: additionsWithSamePathAsDestination[0].FileSystemInfo.FullName); ThrowTerminatingError(errorRecord); } @@ -142,7 +145,7 @@ protected override void EndProcessing() IArchive? archive = null; try { - if (ShouldProcess(target: DestinationPath, action: "Create")) + if (ShouldProcess(target: _destinationPathInfo.FullName, action: "Create")) { // If the WriteMode is overwrite, delete the existing archive if (WriteMode == WriteMode.Overwrite) @@ -162,7 +165,7 @@ protected override void EndProcessing() WriteProgress(progressRecord); foreach (ArchiveAddition entry in archiveAddtions) { - if (ShouldProcess(target: entry.FullPath, action: "Add")) + if (ShouldProcess(target: entry.FileSystemInfo.FullName, action: "Add")) { archive?.AddFilesytemEntry(entry); // Keep track of number of items added to the archive and use that to update progress @@ -172,7 +175,7 @@ protected override void EndProcessing() WriteProgress(progressRecord); // Write a verbose message saying this item was added to the archive - var addedItemMessage = String.Format(Messages.AddedItemToArchiveVerboseMessage, entry.FullPath); + var addedItemMessage = String.Format(Messages.AddedItemToArchiveVerboseMessage, entry.FileSystemInfo.FullName); WriteVerbose(addedItemMessage); } else { @@ -195,7 +198,7 @@ protected override void EndProcessing() // If -PassThru is specified, write a System.IO.FileInfo object if (PassThru) { - var archiveInfo = new System.IO.FileInfo(DestinationPath); + var archiveInfo = new System.IO.FileInfo(_destinationPathInfo.FullName); WriteObject(archiveInfo); } } @@ -205,9 +208,9 @@ protected override void StopProcessing() // If a new output archive was created, delete it (this does not delete an archive if -WriteMode Update is specified) if (_didCreateNewArchive) { - if (System.IO.File.Exists(DestinationPath)) + if (System.IO.File.Exists(_destinationPathInfo.FullName)) { - System.IO.File.Delete(DestinationPath); + System.IO.File.Delete(_destinationPathInfo.FullName); } } } @@ -220,25 +223,17 @@ private void ValidateDestinationPath() // TODO: Add tests cases for conditions below ErrorCode? errorCode = null; - var archiveAsFile = new System.IO.FileInfo(DestinationPath); - var archiveAsDirectory = new System.IO.DirectoryInfo(DestinationPath); - - // Check if DestinationPath is an existing file - if (archiveAsFile.Exists) + // In this case, DestinationPath does not exist + if (!_destinationPathInfo.Exists) { - // Throw an error if DestinationPath exists and the cmdlet is not in Update mode or Overwrite is not specified - if (WriteMode == WriteMode.Create) - { - errorCode = ErrorCode.ArchiveExists; - } - // Throw an error if the cmdlet is in Update mode but the archive is read only - if (WriteMode == WriteMode.Update && archiveAsFile.Attributes.HasFlag(FileAttributes.ReadOnly)) + // Throw an error if DestinationPath does not exist and cmdlet is in Update mode + if (WriteMode == WriteMode.Update) { - errorCode = ErrorCode.ArchiveReadOnly; + errorCode = ErrorCode.ArchiveDoesNotExist; } - } + } // Check if DestinationPath is an existing directory - else if (archiveAsDirectory.Exists) + else if (_destinationPathInfo.Attributes.HasFlag(FileAttributes.Directory)) { // Throw an error if DestinationPath exists and the cmdlet is not in Update mode or Overwrite is not specified if (WriteMode == WriteMode.Create) @@ -246,23 +241,28 @@ private void ValidateDestinationPath() errorCode = ErrorCode.ArchiveExistsAsDirectory; } // Throw an error if the DestinationPath is a directory and the cmdlet is in Update mode - if (WriteMode == WriteMode.Update) + else if (WriteMode == WriteMode.Update) { errorCode = ErrorCode.ArchiveExistsAsDirectory; } // Throw an error if the DestinationPath is a directory with at least item and the cmdlet is in Overwrite mode - if (WriteMode == WriteMode.Overwrite && archiveAsDirectory.GetFileSystemInfos().Length > 0) + else if (WriteMode == WriteMode.Overwrite && (_destinationPathInfo as System.IO.DirectoryInfo).GetFileSystemInfos().Length > 0) { errorCode = ErrorCode.ArchiveIsNonEmptyDirectory; } - } - // In this case, DestinationPath does not exist + } + // If DestinationPath is an existing file else { - // Throw an error if DestinationPath does not exist and cmdlet is in Update mode - if (WriteMode == WriteMode.Update) + // Throw an error if DestinationPath exists and the cmdlet is not in Update mode or Overwrite is not specified + if (WriteMode == WriteMode.Create) { - errorCode = ErrorCode.ArchiveDoesNotExist; + errorCode = ErrorCode.ArchiveExists; + } + // Throw an error if the cmdlet is in Update mode but the archive is read only + else if (WriteMode == WriteMode.Update && _destinationPathInfo.Attributes.HasFlag(FileAttributes.ReadOnly)) + { + errorCode = ErrorCode.ArchiveReadOnly; } } @@ -282,6 +282,7 @@ private void DeleteDestinationPathIfExists() { System.IO.File.Delete(DestinationPath); } + // TODO: Ensure DestinationPath has no children when deleting it if (System.IO.Directory.Exists(DestinationPath)) { System.IO.Directory.Delete(DestinationPath); @@ -306,7 +307,7 @@ private void DeleteDestinationPathIfExists() private void DetermineArchiveFormat() { // Check if cmdlet is able to determine the format of the archive based on the extension of DestinationPath - bool ableToDetermineArchiveFormat = ArchiveFactory.TryGetArchiveFormatForPath(path: DestinationPath, archiveFormat: out var archiveFormat); + bool ableToDetermineArchiveFormat = ArchiveFactory.TryGetArchiveFormatForPath(path: _destinationPathInfo.FullName, archiveFormat: out var archiveFormat); // If the user did not specify which archive format to use, try to determine it automatically if (Format is null) { diff --git a/src/PathHelper.cs b/src/PathHelper.cs index b0e8a45..10310e2 100644 --- a/src/PathHelper.cs +++ b/src/PathHelper.cs @@ -206,7 +206,7 @@ private void AddArchiveAdditionForUserEnteredLiteralPath(string path, ListIf true, relative path structure will be preserved. If false, relative path structure will NOT be preserved. private void AddAdditionForFullyQualifiedPath(string path, List additions, bool shouldPreservePathStructure) { - var additionType = ArchiveAddition.ArchiveAdditionType.File; + System.IO.FileSystemInfo fileSystemInfo = new System.IO.FileInfo(path); if (System.IO.Directory.Exists(path)) { // Add directory seperator to end if it does not already have it @@ -214,7 +214,7 @@ private void AddAdditionForFullyQualifiedPath(string path, List // Recurse through the child items and add them to additions var directoryInfo = new System.IO.DirectoryInfo(path); AddDescendentEntries(directoryInfo: directoryInfo, additions: additions, shouldPreservePathStructure: shouldPreservePathStructure); - additionType = ArchiveAddition.ArchiveAdditionType.Directory; + fileSystemInfo = directoryInfo; } else if (!System.IO.File.Exists(path)) { @@ -224,7 +224,8 @@ private void AddAdditionForFullyQualifiedPath(string path, List } // Add an entry for the item - additions.Add(new ArchiveAddition(entryName: GetEntryName(path: path, shouldPreservePathStructure: shouldPreservePathStructure), fullPath: path, type: additionType)); + var entryName = GetEntryName(fileSystemInfo: fileSystemInfo, shouldPreservePathStructure: shouldPreservePathStructure); + additions.Add(new ArchiveAddition(entryName: entryName, fileSystemInfo: fileSystemInfo)); } /// @@ -241,16 +242,16 @@ private void AddDescendentEntries(System.IO.DirectoryInfo directoryInfo, ListA fully qualified path /// /// - private string GetEntryName(string path, bool shouldPreservePathStructure) + private string GetEntryName(System.IO.FileSystemInfo fileSystemInfo, bool shouldPreservePathStructure) { // If the path is relative to the current working directory, return the relative path as name - if (shouldPreservePathStructure && TryGetPathRelativeToCurrentWorkingDirectory(path, out var relativePath)) + if (shouldPreservePathStructure && TryGetPathRelativeToCurrentWorkingDirectory(path: fileSystemInfo.FullName, out var relativePath)) { return relativePath; } // Otherwise, return the name of the directory or file - if (path.EndsWith(System.IO.Path.DirectorySeparatorChar)) + var entryName = fileSystemInfo.Name; + if (fileSystemInfo.Attributes.HasFlag(FileAttributes.Directory) && !entryName.EndsWith(System.IO.Path.DirectorySeparatorChar)) { - // Get substring from second-last directory seperator char till end - if (path.Length - 2 < 0) - { - return path; - } - - int secondLastIndex = path.LastIndexOf(value: System.IO.Path.DirectorySeparatorChar, startIndex: path.Length - 2); - if (secondLastIndex == -1) - { - return path; - } - else - { - return path.Substring(secondLastIndex + 1); - } - } - else - { - return System.IO.Path.GetFileName(path); + entryName += System.IO.Path.DirectorySeparatorChar; } + return entryName; } private string GetEntryName(string path, string prefix) @@ -343,7 +328,7 @@ private string GetPrefixForPath(System.IO.DirectoryInfo directoryInfo) /// private IEnumerable GetDuplicatePaths(List additions) { - return additions.GroupBy(x => x.FullPath) + return additions.GroupBy(x => x.FileSystemInfo.FullName) .Where(group => group.Count() > 1) .Select(x => x.Key); } @@ -354,7 +339,7 @@ private IEnumerable GetDuplicatePaths(List additions) /// /// /// - internal string ResolveToSingleFullyQualifiedPath(string path) + internal System.IO.FileSystemInfo ResolveToSingleFullyQualifiedPath(string path) { // Currently, all this function does is return the literal fully qualified path of a path @@ -368,7 +353,9 @@ internal string ResolveToSingleFullyQualifiedPath(string path) _cmdlet.ThrowTerminatingError(errorRecord); } - return fullyQualifiedPath; + // Return filesystem info + + return GetFilesystemInfoForPath(fullyQualifiedPath); } /// @@ -392,5 +379,25 @@ private bool TryGetPathRelativeToCurrentWorkingDirectory(string path, out string relativePath = System.IO.Path.GetRelativePath(_cmdlet.SessionState.Path.CurrentFileSystemLocation.Path, path); return !relativePath.Contains(".."); } + internal static bool ArePathsSame(string path1, string path2) + { + string fullPath1 = System.IO.Path.GetFullPath(path1); + string fullPath2 = System.IO.Path.GetFullPath(path2); + return fullPath1 == fullPath2; + } + + internal static System.IO.FileSystemInfo GetFilesystemInfoForPath(string path) + { + // Check if path exists + if (System.IO.File.Exists(path)) + { + return new System.IO.FileInfo(path); + } + if (System.IO.Directory.Exists(path)) + { + return new System.IO.DirectoryInfo(path); + } + return path.EndsWith(System.IO.Path.DirectorySeparatorChar) ? new System.IO.DirectoryInfo(path) : new System.IO.FileInfo(path); + } } } diff --git a/src/TarArchive.cs b/src/TarArchive.cs index a0d1fc4..b2d88ed 100644 --- a/src/TarArchive.cs +++ b/src/TarArchive.cs @@ -26,13 +26,13 @@ public TarArchive(string path, ArchiveMode mode, FileStream fileStream) { _mode = mode; _path = path; - _tarWriter = new TarWriter(archiveStream: fileStream, archiveFormat: TarFormat.Pax, leaveOpen: false); + _tarWriter = new TarWriter(archiveStream: fileStream, format: TarEntryFormat.Pax, leaveOpen: false); _fileStream = fileStream; } void IArchive.AddFilesytemEntry(ArchiveAddition entry) { - _tarWriter.WriteEntry(fileName: entry.FullPath, entryName: entry.EntryName); + _tarWriter.WriteEntry(fileName: entry.FileSystemInfo.FullName, entryName: entry.EntryName); } string[] IArchive.GetEntries() diff --git a/src/ZipArchive.cs b/src/ZipArchive.cs index 132b33d..35b019a 100644 --- a/src/ZipArchive.cs +++ b/src/ZipArchive.cs @@ -54,7 +54,7 @@ void IArchive.AddFilesytemEntry(ArchiveAddition addition) } // If the addition is a folder, only create the entry in the archive -- nothing else is needed - if (addition.Type == ArchiveAddition.ArchiveAdditionType.Directory) + if (addition.FileSystemInfo.Attributes.HasFlag(System.IO.FileAttributes.Directory)) { // If the archive does not have an entry with the same name, then add an entry for the directory if (entryInArchive == null) @@ -75,7 +75,7 @@ void IArchive.AddFilesytemEntry(ArchiveAddition addition) entryInArchive.Delete(); } // TODO: Add exception handling - _zipArchive.CreateEntryFromFile(sourceFileName: addition.FullPath, entryName: entryName, compressionLevel: _compressionLevel); + _zipArchive.CreateEntryFromFile(sourceFileName: addition.FileSystemInfo.FullName, entryName: entryName, compressionLevel: _compressionLevel); } } From 615994b70c3c385791264f848bc8e9e8003e39d8 Mon Sep 17 00:00:00 2001 From: Abdullah Yousuf Date: Mon, 25 Jul 2022 13:44:12 -0700 Subject: [PATCH 26/42] updated checking for the same source path and destination path --- src/CompressArchiveCommand.cs | 2 +- src/PathHelper.cs | 22 +++++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index bfb1adc..402d65f 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -119,7 +119,7 @@ protected override void EndProcessing() // Throw a terminating error if there is a source path as same as DestinationPath. // We don't want to overwrite the file or directory that we want to add to the archive. - var additionsWithSamePathAsDestination = archiveAddtions.Where(addition => addition.FileSystemInfo == _destinationPathInfo).ToList(); + var additionsWithSamePathAsDestination = archiveAddtions.Where(addition => PathHelper.ArePathsSame(addition.FileSystemInfo, _destinationPathInfo)).ToList(); if (additionsWithSamePathAsDestination.Count() > 0) { // Since duplicate checking is performed earlier, there must a single ArchiveAddition such that ArchiveAddition.FullPath == DestinationPath diff --git a/src/PathHelper.cs b/src/PathHelper.cs index 10310e2..2fe04fd 100644 --- a/src/PathHelper.cs +++ b/src/PathHelper.cs @@ -379,10 +379,26 @@ private bool TryGetPathRelativeToCurrentWorkingDirectory(string path, out string relativePath = System.IO.Path.GetRelativePath(_cmdlet.SessionState.Path.CurrentFileSystemLocation.Path, path); return !relativePath.Contains(".."); } - internal static bool ArePathsSame(string path1, string path2) + internal static bool ArePathsSame(System.IO.FileSystemInfo fileSystemInfo1, System.IO.FileSystemInfo fileSystemInfo2) { - string fullPath1 = System.IO.Path.GetFullPath(path1); - string fullPath2 = System.IO.Path.GetFullPath(path2); + // If one is a file and the other is a directory, return false + if ((fileSystemInfo1.Attributes.HasFlag(FileAttributes.Directory) && !fileSystemInfo2.Attributes.HasFlag(FileAttributes.Directory)) || + (!fileSystemInfo1.Attributes.HasFlag(FileAttributes.Directory) && fileSystemInfo2.Attributes.HasFlag(FileAttributes.Directory))) + { + return false; + } + + string fullPath1 = fileSystemInfo1.FullName; + string fullPath2 = fileSystemInfo2.FullName; + + // If both are directories, compare their paths + if (fileSystemInfo1.Attributes.HasFlag(FileAttributes.Directory) && fileSystemInfo2.Attributes.HasFlag(FileAttributes.Directory)) + { + if (!System.IO.Path.EndsInDirectorySeparator(fullPath1)) fullPath1 += System.IO.Path.DirectorySeparatorChar; + if (!System.IO.Path.EndsInDirectorySeparator(fullPath2)) fullPath2 += System.IO.Path.DirectorySeparatorChar; + } + + return fullPath1 == fullPath2; } From 33d863d92bda9394bcb3a324c0b3e58dc8a2dbed Mon Sep 17 00:00:00 2001 From: Abdullah Yousuf Date: Mon, 25 Jul 2022 13:57:40 -0700 Subject: [PATCH 27/42] updated build configuration --- .azdevops/build.ps1 | 109 +++++++++++++++++++ .azdevops/build.yml | 138 ++++++++++++++++++++++++ SimpleBuild.ps1 | 13 ++- azure-pipelines-release.yml | 7 ++ src/ArchiveFactory.cs | 6 +- src/ArchiveFormat.cs | 4 +- src/CompressArchiveCommand.cs | 3 +- src/Microsoft.PowerShell.Archive.csproj | 4 - src/Microsoft.PowerShell.Archive.psd1 | 10 +- src/TarArchive.cs | 2 +- 10 files changed, 276 insertions(+), 20 deletions(-) create mode 100644 .azdevops/build.ps1 create mode 100644 .azdevops/build.yml diff --git a/.azdevops/build.ps1 b/.azdevops/build.ps1 new file mode 100644 index 0000000..7982e86 --- /dev/null +++ b/.azdevops/build.ps1 @@ -0,0 +1,109 @@ +[CmdletBinding(SupportsShouldProcess=$true)] +param ( + [switch]$test, + [switch]$build, + [switch]$publish, + [switch]$signed, + [switch]$package, + [switch]$coverage, + [switch]$CopySBOM, + [string]$SignedPath + ) + + +$root = (Resolve-Path -Path "${PSScriptRoot}/../")[0] +$Name = "Microsoft.PowerShell.Archive" +$BuildOutputDir = Join-Path $root "\src\bin\release\net7.0" +$ManifestPath = "${BuildOutputDir}\${Name}.psd1" +$ManifestData = Import-PowerShellDataFile -Path $ManifestPath +$Version = $ManifestData.ModuleVersion + +# Path for signed +#$SignedPath = Join-Path $(Build.SourcesDirectory) "signed" + +$SignRoot = "${root}\signed\${Name}" +$SignVersion = "$SignRoot\$Version" + +$PubBase = "${root}\out" +$PubRoot = "${PubBase}\${Name}" +$PubDir = "${PubRoot}\${Version}" + +if (-not $test -and -not $build -and -not $publish -and -not $package) { + throw "must use 'build', 'test', 'publish', 'package'" +} + +[bool]$verboseValue = $PSBoundParameters['Verbose'].IsPresent ? $PSBoundParameters['Verbose'].ToBool() : $false + +$FileManifest = @( + @{ SRC = "${$BuildOutputDir}"; NAME = "Microsoft.PowerShell.Archive.dll" ; SIGN = $true ; DEST = "OUTDIR" } + @{ SRC = "${$BuildOutputDir}"; NAME = "Microsoft.PowerShell.Archive.psm1" ; SIGN = $true ; DEST = "OUTDIR" } +) + +if ($build) { + Write-Verbose -Verbose -Message "No action for build" +} + +# this takes the files for the module and publishes them to a created, local repository +# so the nupkg can be used to publish to the PSGallery +function Export-Module +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] + param() + if ( $signed ) { + $packageRoot = $SignedPath + } + else { + $packageRoot = $PubRoot + } + + if ( -not (test-path $packageRoot)) { + throw "'$PubDir' does not exist" + } + + # now constuct a nupkg by registering a local repository and calling publish module + $repoName = [guid]::newGuid().ToString("N") + Register-PSRepository -Name $repoName -SourceLocation $packageRoot -InstallationPolicy Trusted + Publish-Module -Path $packageRoot -Repository $repoName + Unregister-PSRepository -Name $repoName + Get-ChildItem -Recurse -Name $packageRoot | Write-Verbose + $nupkgName = "{0}.{1}.nupkg" -f ${Name},${Version} + $nupkgPath = Join-Path $packageRoot $nupkgName + if ($env:TF_BUILD) { + # In Azure DevOps + Write-Host "##vso[artifact.upload containerfolder=$nupkgName;artifactname=$nupkgName;]$nupkgPath" + } +} + +if ($publish) { + Write-Verbose "Publishing to '$PubDir'" + if (-not (test-path $PubDir)) { + $null = New-Item -ItemType Directory $PubDir -Force + } + foreach ($file in $FileManifest) { + if ($signed -and $file.SIGN) { + $src = Join-Path -Path $PSScriptRoot -AdditionalChildPath $file.NAME -ChildPath signed + } + else { + $src = Join-Path -Path $file.SRC -ChildPath $file.NAME + } + $targetDir = $file.DEST -creplace "OUTDIR","$PubDir" + if (-not (Test-Path $src)) { + throw ("file '" + $src + "' not found") + } + if (-not (Test-Path $targetDir)) { + $null = New-Item -ItemType Directory $targetDir -Force + } + Copy-Item -Path $src -destination $targetDir -Verbose:$verboseValue + + + } +} + +# this copies the manifest before creating the module nupkg +# if -CopySBOM is used. +if ($package) { + if($CopySBOM) { + #Copy-Item -Recurse -Path "signed/_manifest" -Destination $SignVersion + } + Export-Module +} diff --git a/.azdevops/build.yml b/.azdevops/build.yml new file mode 100644 index 0000000..832d582 --- /dev/null +++ b/.azdevops/build.yml @@ -0,0 +1,138 @@ +name: Microsoft.PowerShell.Archive-$(Build.BuildId) +trigger: none + +pr: none + +variables: + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + POWERSHELL_TELEMETRY_OPTOUT: 1 + +resources: + repositories: + - repository: ComplianceRepo + type: github + endpoint: ComplianceGHRepo + name: PowerShell/compliance + ref: master + +stages: +- stage: Build + displayName: Build + pool: + name: 1ES + demands: + - ImageOverride -equals PSMMS2019-Secure + jobs: + - job: Build_Job + displayName: Build Microsoft.PowerShell.Archive + variables: + - group: ESRP + steps: + - checkout: self + + - task: UseDotNet@2 + displayName: 'Get .NET 7.0 SDK' + inputs: + packageType: sdk + version: 7.x + includePreviewVersions: true + + - pwsh: | + & $(Build.SourcesDirectory)/Microsoft.PowerShell.Archive/SimpleBuild.ps1 + displayName: Build Microsoft.PowerShell.Archive module + + - pwsh: | + dir "$(BuildOutDir)\*" -Recurse + displayName: Show BuildOutDirectory + + - pwsh: | + $signSrcPath = "$(BuildOutDir)" + # Set signing src path variable + $vstsCommandString = "vso[task.setvariable variable=signSrcPath]${signSrcPath}" + Write-Host "sending " + $vstsCommandString + Write-Host "##$vstsCommandString" + # Get the module version + $ManifestPath = Join-Path $(BuildOutDir) "Microsoft.PowerShell.Archive.psd1" + $ManifestData = Import-PowerShellDataFile -Path $ManifestPath + $Version = $ManifestData.ModuleVersion + $signOutPath = "$(Build.SourcesDirectory)\signed\Microsoft.PowerShell.Archive\${Version}" + $null = New-Item -ItemType Directory -Path $signOutPath + # Set signing out path variable + $vstsCommandString = "vso[task.setvariable variable=signOutPath]${signOutPath}" + Write-Host "sending " + $vstsCommandString + Write-Host "##$vstsCommandString" + # Set path variable for guardian codesign validation + $vstsCommandString = "vso[task.setvariable variable=GDN_CODESIGN_TARGETDIRECTORY]${signOutPath}" + Write-Host "sending " + $vstsCommandString + Write-Host "##$vstsCommandString" + displayName: Setup variables for signing + + - checkout: ComplianceRepo + + - task: UseDotNet@2 + displayName: 'Get .NET 2.1 SDK' + inputs: + packageType: sdk + version: 2.x + includePreviewVersions: true + + - template: EsrpSign.yml@ComplianceRepo + parameters: + # the folder which contains the binaries to sign + buildOutputPath: $(signSrcPath) + # the location to put the signed output + signOutputPath: $(signOutPath) + # the certificate ID to use + certificateId: "CP-230012" + # the file pattern to use, comma separated + pattern: '*.psd1,*.dll' + + - template: Sbom.yml@ComplianceRepo + parameters: + BuildDropPath: $(signOutPath) + Build_Repository_Uri: 'https://github.com/PowerShell/Microsoft.PowerShell.Archive' + + - pwsh: | + Get-ChildItem $(signOutPath) -Recurse | Write-Output + + - pwsh: | + Set-Location "$(Build.SourcesDirectory)" + # signOutPath points to directory with version number -- we want to point to the parent of that directory + $ModulePath = Split-Path $signOutPath -Parent + $(Build.SourcesDirectory)/Microsoft.PowerShell.Archive/.azdevops/build.ps1 -package -CopySBOM -signed -SignedPath $ModulePath + Get-ChildItem -recurse -file -name | Write-Verbose -Verbose + displayName: package build + + - publish: "$(signSrcPath)" + artifact: build + displayName: Publish build + +- stage: compliance + displayName: Compliance + dependsOn: Build + jobs: + - job: Compliance_Job + pool: + name: 1ES # Package ES CodeHub Lab E + steps: + - checkout: self + - checkout: ComplianceRepo + - download: current + artifact: build + + - pwsh: | + Get-ChildItem -Path "$(Pipeline.Workspace)\build" -Recurse + displayName: Capture downloaded artifacts + - template: script-module-compliance.yml@ComplianceRepo + parameters: + # component-governance + sourceScanPath: '$(Build.SourcesDirectory)/src' + # credscan + suppressionsFile: '' + # TermCheck + optionsRulesDBPath: '' + optionsFTPath: '' + # tsa-upload + codeBaseName: 'PSNativeCommandProxy_2020' + # selections + APIScan: false # set to false when not using Windows APIs. diff --git a/SimpleBuild.ps1 b/SimpleBuild.ps1 index 9e1dda6..c74d5cf 100644 --- a/SimpleBuild.ps1 +++ b/SimpleBuild.ps1 @@ -1,17 +1,24 @@ -$buildOutputDirectory = "$PSScriptRoot\src\build\net7.0" +$buildOutputDirectory = "$PSScriptRoot\src\bin\release\net7.0" if ((Test-Path $buildOutputDirectory)) { Remove-Item -Path $buildOutputDirectory -Recurse -Force } # Perform dotnet build -dotnet build .\src\Microsoft.PowerShell.Archive.csproj -c release +dotnet build "$PSScriptRoot\src\Microsoft.PowerShell.Archive.csproj" -c release + +# Show build output directory contents +Get-ChildItem $buildOutputDirectory -Recurse | Write-Output + +# Remove unneeded files +rm "$buildOutputDirectory/*.json","$buildOutputDirectory/*.pdb" "Build module location: $buildOutputDirectory" | Write-Verbose -Verbose "Setting VSTS variable 'BuildOutDir' to '$buildOutputDirectory'" | Write-Verbose -Verbose Write-Host "##vso[task.setvariable variable=BuildOutDir]$buildOutputDirectory" -$psd1ModuleVersion = (Get-Content -Path "$buildOutputDirectory\Microsoft.PowerShell.Archive.psd1" | Select-String 'ModuleVersion="(.*)"').Matches[0].Groups[1].Value +#$psd1ModuleVersion = (Get-Content -Path "$buildOutputDirectory\Microsoft.PowerShell.Archive.psd1" | Select-String 'ModuleVersion="(.*)"').Matches[0].Groups[1].Value +$psd1ModuleVersion = '2.0.0' "Setting VSTS variable 'PackageVersion' to '$psd1ModuleVersion'" | Write-Verbose -Verbose Write-Host "##vso[task.setvariable variable=PackageVersion]$psd1ModuleVersion" diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index c418b4b..9585892 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -31,6 +31,13 @@ stages: - group: ESRP steps: + - task: UseDotNet@2 + displayName: 'Get .NET 7.0 SDK' + inputs: + packageType: sdk + version: 7.0 + includePreviewVersions: true + - pwsh: | & $(Build.SourcesDirectory)\SimpleBuild.ps1 displayName: Build Microsoft.PowerShell.Archive module diff --git a/src/ArchiveFactory.cs b/src/ArchiveFactory.cs index 33fb5e4..1ec0aad 100644 --- a/src/ArchiveFactory.cs +++ b/src/ArchiveFactory.cs @@ -22,7 +22,7 @@ internal static IArchive GetArchive(ArchiveFormat format, string archivePath, Ar return format switch { ArchiveFormat.zip => new ZipArchive(archivePath, archiveMode, archiveFileStream, compressionLevel), - //ArchiveFormat.tar => new TarArchive(archivePath, archiveMode, archiveFileStream), + ArchiveFormat.tar => new TarArchive(archivePath, archiveMode, archiveFileStream), // TODO: Add archive types here // TODO: Add message to exception _ => throw new NotImplementedException() @@ -36,11 +36,11 @@ internal static bool TryGetArchiveFormatForPath(string path, out ArchiveFormat? { archiveFormat = ArchiveFormat.zip; } - /*if (path.EndsWith(".tar")) + if (path.EndsWith(".tar")) { archiveFormat = ArchiveFormat.tar; } - if (path.EndsWith(".tar.gz") || path.EndsWith(".tgz")) + /*if (path.EndsWith(".tar.gz") || path.EndsWith(".tgz")) { archiveFormat = ArchiveFormat.tgz; }*/ diff --git a/src/ArchiveFormat.cs b/src/ArchiveFormat.cs index 245b398..5a23b24 100644 --- a/src/ArchiveFormat.cs +++ b/src/ArchiveFormat.cs @@ -7,8 +7,8 @@ namespace Microsoft.PowerShell.Archive public enum ArchiveFormat { zip, - /* Removing these formats for preview relase + /* Removing these formats for preview relase*/ tar, - tgz*/ + tgz } } diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index 206d04f..3b69c3d 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -85,8 +85,7 @@ protected override void BeginProcessing() // Validate DestinationPath ValidateDestinationPath(); - - + DetermineArchiveFormat(); } protected override void ProcessRecord() diff --git a/src/Microsoft.PowerShell.Archive.csproj b/src/Microsoft.PowerShell.Archive.csproj index 69bbcca..7fddcd2 100644 --- a/src/Microsoft.PowerShell.Archive.csproj +++ b/src/Microsoft.PowerShell.Archive.csproj @@ -6,10 +6,6 @@ en-US - - - - PreserveNewest diff --git a/src/Microsoft.PowerShell.Archive.psd1 b/src/Microsoft.PowerShell.Archive.psd1 index 7aa833e..c30bf2a 100644 --- a/src/Microsoft.PowerShell.Archive.psd1 +++ b/src/Microsoft.PowerShell.Archive.psd1 @@ -12,7 +12,7 @@ RootModule = '' # Version number of this module. -ModuleVersion = '2.0.0' +ModuleVersion = '2.0.1-preview1' # Supported PSEditions # CompatiblePSEditions = @() @@ -30,10 +30,10 @@ CompanyName = 'Microsoft' Copyright = '(c) Microsoft. All rights reserved.' # Description of the functionality provided by this module -Description = 'The module allows creating and expanding archives.' +Description = 'PowerShell module for creating and expanding archives.' # Minimum version of the PowerShell engine required by this module -PowerShellVersion = '7.3.0' +PowerShellVersion = '7.2.5' # Name of the PowerShell host required by this module # PowerShellHostName = '' @@ -72,7 +72,7 @@ NestedModules = @('Microsoft.PowerShell.Archive.dll') FunctionsToExport = '*' # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. -CmdletsToExport = @('Compress-Archive', 'Expand-Archive', 'Expand-Archive2') +CmdletsToExport = @('Compress-Archive') # Variables to export from this module VariablesToExport = '*' @@ -110,7 +110,7 @@ PrivateData = @{ # ReleaseNotes = '' # Prerelease string of this module - # Prerelease = 'This module is a prerelease version.' + Prerelease = 'This module is a prerelease version.' # Flag to indicate whether the module requires explicit user acceptance for install/update/save # RequireLicenseAcceptance = $false diff --git a/src/TarArchive.cs b/src/TarArchive.cs index a0d1fc4..634e2bc 100644 --- a/src/TarArchive.cs +++ b/src/TarArchive.cs @@ -26,7 +26,7 @@ public TarArchive(string path, ArchiveMode mode, FileStream fileStream) { _mode = mode; _path = path; - _tarWriter = new TarWriter(archiveStream: fileStream, archiveFormat: TarFormat.Pax, leaveOpen: false); + _tarWriter = new TarWriter(archiveStream: fileStream, format: TarEntryFormat.Pax, leaveOpen: false); _fileStream = fileStream; } From fd9b32772c174a878757559a743d477b7ccf015c Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Mon, 25 Jul 2022 14:26:11 -0700 Subject: [PATCH 28/42] updated project version with prelease info, removed tar support after merging branches --- src/ArchiveFactory.cs | 5 +++-- src/ArchiveFormat.cs | 4 ++-- src/Microsoft.PowerShell.Archive.csproj | 2 ++ src/Microsoft.PowerShell.Archive.psd1 | 17 ++++++----------- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/ArchiveFactory.cs b/src/ArchiveFactory.cs index 1ec0aad..2602894 100644 --- a/src/ArchiveFactory.cs +++ b/src/ArchiveFactory.cs @@ -22,7 +22,7 @@ internal static IArchive GetArchive(ArchiveFormat format, string archivePath, Ar return format switch { ArchiveFormat.zip => new ZipArchive(archivePath, archiveMode, archiveFileStream, compressionLevel), - ArchiveFormat.tar => new TarArchive(archivePath, archiveMode, archiveFileStream), + //ArchiveFormat.tar => new TarArchive(archivePath, archiveMode, archiveFileStream), // TODO: Add archive types here // TODO: Add message to exception _ => throw new NotImplementedException() @@ -36,11 +36,12 @@ internal static bool TryGetArchiveFormatForPath(string path, out ArchiveFormat? { archiveFormat = ArchiveFormat.zip; } + /* Disable support for tar and tar.gz for preview1 release if (path.EndsWith(".tar")) { archiveFormat = ArchiveFormat.tar; } - /*if (path.EndsWith(".tar.gz") || path.EndsWith(".tgz")) + if (path.EndsWith(".tar.gz") || path.EndsWith(".tgz")) { archiveFormat = ArchiveFormat.tgz; }*/ diff --git a/src/ArchiveFormat.cs b/src/ArchiveFormat.cs index 5a23b24..245b398 100644 --- a/src/ArchiveFormat.cs +++ b/src/ArchiveFormat.cs @@ -7,8 +7,8 @@ namespace Microsoft.PowerShell.Archive public enum ArchiveFormat { zip, - /* Removing these formats for preview relase*/ + /* Removing these formats for preview relase tar, - tgz + tgz*/ } } diff --git a/src/Microsoft.PowerShell.Archive.csproj b/src/Microsoft.PowerShell.Archive.csproj index 7fddcd2..df50474 100644 --- a/src/Microsoft.PowerShell.Archive.csproj +++ b/src/Microsoft.PowerShell.Archive.csproj @@ -4,6 +4,8 @@ net7.0 enable en-US + Microsoft.PowerShell.Archive + 2.0.0 diff --git a/src/Microsoft.PowerShell.Archive.psd1 b/src/Microsoft.PowerShell.Archive.psd1 index c30bf2a..944d068 100644 --- a/src/Microsoft.PowerShell.Archive.psd1 +++ b/src/Microsoft.PowerShell.Archive.psd1 @@ -1,18 +1,10 @@ -# -# Module manifest for module '' -# -# Generated by: Microsoft -# -# Generated on: 6/17/2022 -# - @{ # Script module or binary module file associated with this manifest. RootModule = '' # Version number of this module. -ModuleVersion = '2.0.1-preview1' +ModuleVersion = '2.0.0' # Supported PSEditions # CompatiblePSEditions = @() @@ -107,10 +99,13 @@ PrivateData = @{ # IconUri = '' # ReleaseNotes of this module - # ReleaseNotes = '' + ReleaseNotes = @' + ## 2.0.1-preview1 + - Rewrote the archive module in C# +'@ # Prerelease string of this module - Prerelease = 'This module is a prerelease version.' + Prerelease = 'preview1' # Flag to indicate whether the module requires explicit user acceptance for install/update/save # RequireLicenseAcceptance = $false From cb985547a5a1b29a3ca535d6d6066d7e8055e6cb Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Mon, 25 Jul 2022 17:15:16 -0700 Subject: [PATCH 29/42] updated CI config, fixed bug with missing error message, fixed tests --- .azdevops/build.ps1 | 2 +- .azdevops/build.yml | 4 ++-- SimpleBuild.ps1 | 2 +- Tests/Compress-Archive.Tests.ps1 | 33 ++++++++++++--------------- src/CompressArchiveCommand.cs | 21 +++++++++-------- src/ErrorMessages.cs | 1 + src/Localized/Messages.Designer.cs | 9 ++++++++ src/Localized/Messages.resx | 3 +++ src/Microsoft.PowerShell.Archive.psd1 | 2 +- 9 files changed, 43 insertions(+), 34 deletions(-) diff --git a/.azdevops/build.ps1 b/.azdevops/build.ps1 index 7982e86..fea6e2e 100644 --- a/.azdevops/build.ps1 +++ b/.azdevops/build.ps1 @@ -66,7 +66,7 @@ function Export-Module Publish-Module -Path $packageRoot -Repository $repoName Unregister-PSRepository -Name $repoName Get-ChildItem -Recurse -Name $packageRoot | Write-Verbose - $nupkgName = "{0}.{1}.nupkg" -f ${Name},${Version} + $nupkgName = "{0}.{1}-preview1.nupkg" -f ${Name},${Version} $nupkgPath = Join-Path $packageRoot $nupkgName if ($env:TF_BUILD) { # In Azure DevOps diff --git a/.azdevops/build.yml b/.azdevops/build.yml index 832d582..36451cb 100644 --- a/.azdevops/build.yml +++ b/.azdevops/build.yml @@ -98,7 +98,7 @@ stages: - pwsh: | Set-Location "$(Build.SourcesDirectory)" # signOutPath points to directory with version number -- we want to point to the parent of that directory - $ModulePath = Split-Path $signOutPath -Parent + $ModulePath = Split-Path $(signOutPath) -Parent $(Build.SourcesDirectory)/Microsoft.PowerShell.Archive/.azdevops/build.ps1 -package -CopySBOM -signed -SignedPath $ModulePath Get-ChildItem -recurse -file -name | Write-Verbose -Verbose displayName: package build @@ -126,7 +126,7 @@ stages: - template: script-module-compliance.yml@ComplianceRepo parameters: # component-governance - sourceScanPath: '$(Build.SourcesDirectory)/src' + sourceScanPath: '$(Build.SourcesDirectory)\Microsoft.PowerShell.Archive\src' # credscan suppressionsFile: '' # TermCheck diff --git a/SimpleBuild.ps1 b/SimpleBuild.ps1 index c74d5cf..64e7c38 100644 --- a/SimpleBuild.ps1 +++ b/SimpleBuild.ps1 @@ -19,6 +19,6 @@ rm "$buildOutputDirectory/*.json","$buildOutputDirectory/*.pdb" Write-Host "##vso[task.setvariable variable=BuildOutDir]$buildOutputDirectory" #$psd1ModuleVersion = (Get-Content -Path "$buildOutputDirectory\Microsoft.PowerShell.Archive.psd1" | Select-String 'ModuleVersion="(.*)"').Matches[0].Groups[1].Value -$psd1ModuleVersion = '2.0.0' +$psd1ModuleVersion = '2.0.1' "Setting VSTS variable 'PackageVersion' to '$psd1ModuleVersion'" | Write-Verbose -Verbose Write-Host "##vso[task.setvariable variable=PackageVersion]$psd1ModuleVersion" diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index 8d23fb5..0c13301 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -374,7 +374,7 @@ $destinationPath | Should -Exist $contents = Get-Descendants -Path $sourcePath $contents += "SourceDir/" - Test-ZipArchive $destinationPath @("SourceDir/", "Sample-2.txt") + Test-ZipArchive $destinationPath $contents } } @@ -416,7 +416,7 @@ try { - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Update + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Update throw "Failed to validate that an archive file format $destinationPath does not exist and -Update switch parameter is specified." } catch @@ -431,7 +431,7 @@ try { - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Update + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Update throw "Failed to validate that a directory $destinationPath exists and -Update switch parameter is specified." } catch @@ -440,18 +440,18 @@ } } - It "Throws a terminating error when DestinationPath is a folder containing at least 1 item and Overwrite is specified" { + It "Throws a terminating error when DestinationPath is a folder containing at least 1 item and Overwrite is specified" -Tag td2 { $sourcePath = "$TestDrive$($DS)SourceDir" $destinationPath = "$TestDrive" try { - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Overwrite + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite throw "Failed to detect an error when $destinationPath is an existing directory containing at least 1 item and -Overwrite switch parameter is specified." } catch { - $_.FullyQualifiedErrorId | Should -Be "ArchiveExistsAsDirectory,Microsoft.PowerShell.Archive.CompressArchiveCommand" + $_.FullyQualifiedErrorId | Should -Be "ArchiveIsNonEmptyDirectory,Microsoft.PowerShell.Archive.CompressArchiveCommand" } } } @@ -477,7 +477,7 @@ $destinationPath = "$TestDrive$($DS)EmptyDirectory" (Get-Item $destinationPath) -is [System.IO.DirectoryInfo] | Should -Be $true - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Overwrite + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite # Ensure $destiationPath is now a file $destinationPathInfo = Get-Item $destinationPath @@ -493,7 +493,7 @@ # Overwrite the archive $sourcePath = "$TestDrive$($DS)Sample-2.txt" - Compress-Archive -Path $sourcePath -DestinationPath "$TestDrive$($DS)archive.zip" -Overwrite + Compress-Archive -Path $sourcePath -DestinationPath "$TestDrive$($DS)archive.zip" -WriteMode Overwrite # Ensure the original entries and different than the new entries Test-ZipArchive $destinationPath @("Sample-2.txt") @@ -569,17 +569,11 @@ It "Accepts DestinationPath parameter with wildcard characters that resolves to one path" { - $sourcePath = "$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-3.txt" + $sourcePath = "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt" $destinationPath = "$TestDrive$($DS)Sample[]SingleFile.zip" - try - { - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - Test-Path -LiteralPath $destinationPath | Should Be $true - } - finally - { - Remove-Item -LiteralPath $destinationPath -Force - } + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + Test-Path -LiteralPath $destinationPath | Should -Be $true + Remove-Item -LiteralPath $destinationPath } It "Accepts DestinationPath parameter with [ but no matching ]" { @@ -587,8 +581,9 @@ $destinationPath = "$TestDrive$($DS)archive[2.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - Test-Path -LiteralPath $destinationPath | Should Be $true + Test-Path -LiteralPath $destinationPath | Should -Be $true Test-ZipArchive $destinationPath @("SourceDir/", "SourceDir/Sample-1.txt") + Remove-Item -LiteralPath $destinationPath } } } \ No newline at end of file diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index 402d65f..c03f40b 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -84,6 +84,7 @@ public CompressArchiveCommand() protected override void BeginProcessing() { _destinationPathInfo = _pathHelper.ResolveToSingleFullyQualifiedPath(DestinationPath); + //DestinationPath = _destinationPathInfo.FullName; // Validate ValidateDestinationPath(); @@ -154,7 +155,7 @@ protected override void EndProcessing() } // Create an archive -- this is where we will switch between different types of archives - archive = ArchiveFactory.GetArchive(format: Format ?? ArchiveFormat.zip, archivePath: DestinationPath, archiveMode: archiveMode, compressionLevel: CompressionLevel); + archive = ArchiveFactory.GetArchive(format: Format ?? ArchiveFormat.zip, archivePath: _destinationPathInfo.FullName, archiveMode: archiveMode, compressionLevel: CompressionLevel); _didCreateNewArchive = archiveMode == ArchiveMode.Update; } @@ -269,7 +270,7 @@ private void ValidateDestinationPath() if (errorCode != null) { // Throw an error -- since we are validating DestinationPath, the problem is with DestinationPath - var errorRecord = ErrorMessages.GetErrorRecord(errorCode: errorCode.Value, errorItem: DestinationPath); + var errorRecord = ErrorMessages.GetErrorRecord(errorCode: errorCode.Value, errorItem: _destinationPathInfo.FullName); ThrowTerminatingError(errorRecord); } } @@ -278,28 +279,28 @@ private void DeleteDestinationPathIfExists() { try { - if (System.IO.File.Exists(DestinationPath)) + if (System.IO.File.Exists(_destinationPathInfo.FullName)) { - System.IO.File.Delete(DestinationPath); + System.IO.File.Delete(_destinationPathInfo.FullName); } // TODO: Ensure DestinationPath has no children when deleting it - if (System.IO.Directory.Exists(DestinationPath)) + if (System.IO.Directory.Exists(_destinationPathInfo.FullName)) { - System.IO.Directory.Delete(DestinationPath); + System.IO.Directory.Delete(_destinationPathInfo.FullName); } } // Throw a terminating error if an IOException occurs catch (System.IO.IOException ioException) { var errorRecord = new ErrorRecord(ioException, errorId: ErrorCode.OverwriteDestinationPathFailed.ToString(), - errorCategory: ErrorCategory.InvalidOperation, targetObject: DestinationPath); + errorCategory: ErrorCategory.InvalidOperation, targetObject: _destinationPathInfo.FullName); ThrowTerminatingError(errorRecord); } // Throw a terminating error if an UnauthorizedAccessException occurs catch (System.UnauthorizedAccessException unauthorizedAccessException) { var errorRecord = new ErrorRecord(unauthorizedAccessException, errorId: ErrorCode.InsufficientPermissionsToAccessPath.ToString(), - errorCategory: ErrorCategory.PermissionDenied, targetObject: DestinationPath); + errorCategory: ErrorCategory.PermissionDenied, targetObject: _destinationPathInfo.FullName); ThrowTerminatingError(errorRecord); } } @@ -318,7 +319,7 @@ private void DetermineArchiveFormat() else { // If the archive format could not be determined, use zip by default and emit a warning - var warningMsg = String.Format(Messages.ArchiveFormatCouldNotBeDeterminedWarning, DestinationPath); + var warningMsg = String.Format(Messages.ArchiveFormatCouldNotBeDeterminedWarning, _destinationPathInfo.FullName); WriteWarning(warningMsg); Format = ArchiveFormat.zip; } @@ -331,7 +332,7 @@ private void DetermineArchiveFormat() { if (archiveFormat is null || archiveFormat.Value != Format.Value) { - var warningMsg = String.Format(Messages.ArchiveExtensionDoesNotMatchArchiveFormatWarning, DestinationPath); + var warningMsg = String.Format(Messages.ArchiveExtensionDoesNotMatchArchiveFormatWarning, _destinationPathInfo.FullName); WriteWarning(warningMsg); } } diff --git a/src/ErrorMessages.cs b/src/ErrorMessages.cs index 92d0e78..71fb8d8 100644 --- a/src/ErrorMessages.cs +++ b/src/ErrorMessages.cs @@ -24,6 +24,7 @@ internal static string GetErrorMessage(ErrorCode errorCode) ErrorCode.ArchiveExistsAsDirectory => Messages.ArchiveExistsAsDirectoryMessage, ErrorCode.ArchiveReadOnly => Messages.ArchiveIsReadOnlyMessage, ErrorCode.ArchiveDoesNotExist => Messages.ArchiveDoesNotExistMessage, + ErrorCode.ArchiveIsNonEmptyDirectory => Messages.ArchiveIsNonEmptyDirectory, ErrorCode.SamePathAndDestinationPath => Messages.SamePathAndDestinationPathMessage, ErrorCode.SameLiteralPathAndDestinationPath => Messages.SameLiteralPathAndDestinationPathMessage, ErrorCode.InsufficientPermissionsToAccessPath => Messages.InsufficientPermssionsToAccessPathMessage, diff --git a/src/Localized/Messages.Designer.cs b/src/Localized/Messages.Designer.cs index a62813d..bd42e82 100644 --- a/src/Localized/Messages.Designer.cs +++ b/src/Localized/Messages.Designer.cs @@ -123,6 +123,15 @@ internal static string ArchiveFormatDeterminedVerboseMessage { } } + /// + /// Looks up a localized string similar to The archive {0} cannot be overwritten because it is a non-empty directory.. + /// + internal static string ArchiveIsNonEmptyDirectory { + get { + return ResourceManager.GetString("ArchiveIsNonEmptyDirectory", resourceCulture); + } + } + /// /// Looks up a localized string similar to The archive at {0} is read-only.. /// diff --git a/src/Localized/Messages.resx b/src/Localized/Messages.resx index d5b8a2f..3788143 100644 --- a/src/Localized/Messages.resx +++ b/src/Localized/Messages.resx @@ -138,6 +138,9 @@ The -Format was not specified, so the archive format was determined to be {0} based on its extension. + + The archive {0} cannot be overwritten because it is a non-empty directory. + The archive at {0} is read-only. diff --git a/src/Microsoft.PowerShell.Archive.psd1 b/src/Microsoft.PowerShell.Archive.psd1 index 944d068..41227c5 100644 --- a/src/Microsoft.PowerShell.Archive.psd1 +++ b/src/Microsoft.PowerShell.Archive.psd1 @@ -4,7 +4,7 @@ RootModule = '' # Version number of this module. -ModuleVersion = '2.0.0' +ModuleVersion = '2.0.1' # Supported PSEditions # CompatiblePSEditions = @() From 7780be07bfc73ecb23bfd4cdc2e6d8c7fe67cc03 Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Tue, 26 Jul 2022 09:16:12 -0700 Subject: [PATCH 30/42] updated CI to build module and run tests across all platforms --- .azdevops/CI.yml | 77 ++++++++++++++++++++++--------------------- .azdevops/runtest.yml | 70 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 38 deletions(-) create mode 100644 .azdevops/runtest.yml diff --git a/.azdevops/CI.yml b/.azdevops/CI.yml index b56f625..badedc9 100644 --- a/.azdevops/CI.yml +++ b/.azdevops/CI.yml @@ -27,51 +27,52 @@ stages: displayName: Build module steps: + - task: UseDotNet@2 + displayName: 'Get .NET 7.0 SDK' + inputs: + packageType: sdk + version: 7.x + includePreviewVersions: true + - pwsh: | & $(Build.SourcesDirectory)\SimpleBuild.ps1 displayName: Build Microsoft.PowerShell.Archive module - condition: succeededOrFailed() - + - pwsh: | - dir "$(BuildOutDir)\*" -Recurse + dir "$(BuildOutDir)/*" -Recurse displayName: Show BuildOutDirectory - - template: Sbom.yml@ComplianceRepo - parameters: - BuildDropPath: "$(BuildOutDir)" - Build_Repository_Uri: 'https://github.com/PowerShell/Microsoft.PowerShell.Archive' - PackageName: $(PackageName) - PackageVersion: $(PackageVersion) + - task: CopyFiles@2 + displayName: 'Copy build' + inputs: + sourceFolder: '$(BuildOutDir)' + contents: '**' + targetFolder: '$(Build.ArtifactStagingDirectory)/Microsoft.PowerShell.Archive' - pwsh: | - dir "$(BuildOutDir)\*" -Recurse - displayName: Show BuildOutDirectory + dir "$(Build.ArtifactStagingDirectory)/*" -Recurse + - publish: '$(Build.ArtifactStagingDirectory)/Microsoft.PowerShell.Archive' + displayName: 'Publish module build' + artifact: ModuleBuild - - pwsh: | - $signSrcPath = "$(BuildOutDir)" - # Set signing src path variable - $vstsCommandString = "vso[task.setvariable variable=signSrcPath]${signSrcPath}" - Write-Host "sending " + $vstsCommandString - Write-Host "##$vstsCommandString" - $signOutPath = "$(Build.SourcesDirectory)\signed\Microsoft.PowerShell.Archive" - $null = New-Item -ItemType Directory -Path $signOutPath - # Set signing out path variable - $vstsCommandString = "vso[task.setvariable variable=signOutPath]${signOutPath}" - Write-Host "sending " + $vstsCommandString - Write-Host "##$vstsCommandString" - # Set path variable for guardian codesign validation - $vstsCommandString = "vso[task.setvariable variable=GDN_CODESIGN_TARGETDIRECTORY]${signOutPath}" - Write-Host "sending " + $vstsCommandString - Write-Host "##$vstsCommandString" - displayName: Setup variables for signing - - - pwsh: | - Copy-Item -Path "$(signSrcPath)\*" -Destination "$(signOutPath)" - displayName: Fake Signing - - - pwsh: | - Compress-Archive -Path "$(signOutPath)\*" -DestinationPath "$(System.ArtifactsDirectory)\Microsoft.PowerShell.Archive.zip" - displayName: Create Microsoft.PowerShell.Archive.zip +- stage: Test + dependsOn: Build + displayName: Run tests + jobs: + - template: runtest.yml + parameters: + vmImageName: windows-2019 + jobName: run_test_windows + jobDisplayName: Run Windows tests - - publish: $(System.ArtifactsDirectory)\Microsoft.PowerShell.Archive.zip - artifact: SignedModule + - template: runtest.yml + parameters: + vmImageName: ubuntu-latest + jobName: run_test_linux + jobDisplayName: Run Linux tests + + - template: runtest.yml + parameters: + vmImageName: macos-latest + jobName: run_test_macos + jobDisplayName: Run macOS tests \ No newline at end of file diff --git a/.azdevops/runtest.yml b/.azdevops/runtest.yml new file mode 100644 index 0000000..e4fe3a3 --- /dev/null +++ b/.azdevops/runtest.yml @@ -0,0 +1,70 @@ +parameters: + - name: vmImageName + type: string + default: 'windows-2019' + + - name: jobName + type: string + default: 'run_test_windows' + + - name: jobDisplayName + type: string + default: 'Run test' + +jobs: + - job: '${{ parameters.jobName }}' + pool: + vmImage: ${{ parameters.vmImageName }} + displayName: ${{ parameters.jobDisplayName }} + steps: + - download: current + artifact: ModuleBuild + + - pwsh: | + $module = Get-Module -Name "Microsoft.PowerShell.Archive" + if ($module -ne $null) + { + Remove-Module $module + } + Import-Module $(Pipeline.Workspace)/ModuleBuild/Microsoft.PowerShell.Archive.psd1 + $module = Get-Module -Name "Microsoft.PowerShell.Archive" + $module.Path | Write-Output + displayName: Import Module from Build + - pwsh: | + Install-Module -Name "Pester" -Force + $module = Get-Module -Name "Pester" + if ($module -ne $null) + { + Remove-Module "Pester" + } + Import-Module -Name "Pester" + displayName: Install and Import Pester 4 + + - pwsh: | + $OutputFile = "$PWD/build-unit-tests.xml" + $results = $null + $results = Invoke-Pester -Script ./Tests/Compress-Archive.Tests.ps1 -OutputFile $OutputFile -PassThru -OutputFormat NUnitXml -Show Failed, Context, Describe, Fails + Write-Host "##vso[artifact.upload containerfolder=testResults;artifactname=testResults]$OutputFile" + if(!$results -or $results.FailedCount -gt 0 -or !$results.TotalCount) + { + throw "Build or tests failed. Passed: $($results.PassedCount) Failed: $($results.FailedCount) Total: $($results.TotalCount)" + } + displayName: Run Build Unit Tests + + - pwsh: | + $module = Get-Module -Name "Microsoft.PowerShell.Archive" + if ($module -ne $null) + { + Remove-Module $module + } + displayName: Remove Built Module from PSSession + condition: succeededOrFailed() + + - task: PublishTestResults@2 + displayName: 'Publish Test Results **/*tests.xml' + inputs: + testResultsFormat: NUnit + testResultsFiles: '**/*tests.xml' + testRunTitle: 'Build Unit Tests' + continueOnError: true + condition: succeededOrFailed() \ No newline at end of file From 68ffa8fd1be740b365c4e6e51533b6717f63c5b0 Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Tue, 26 Jul 2022 16:31:06 -0700 Subject: [PATCH 31/42] updated CI to run tests, added and reorganized tests, solved a bug where overwriting the working directory could succeed --- .azdevops/RunTests.ps1 | 38 ++++++++++ .azdevops/runtest.yml | 71 ++++++++++-------- Tests/Compress-Archive.Tests.ps1 | 112 +++++++++++++++++++++++------ src/CompressArchiveCommand.cs | 9 ++- src/ErrorMessages.cs | 5 +- src/Localized/Messages.Designer.cs | 9 +++ src/Localized/Messages.resx | 3 + 7 files changed, 190 insertions(+), 57 deletions(-) create mode 100644 .azdevops/RunTests.ps1 diff --git a/.azdevops/RunTests.ps1 b/.azdevops/RunTests.ps1 new file mode 100644 index 0000000..02eed15 --- /dev/null +++ b/.azdevops/RunTests.ps1 @@ -0,0 +1,38 @@ +# Load the module +$module = Get-Module -Name "Microsoft.PowerShell.Archive" +if ($null -ne $module) +{ + Remove-Module $module +} + +# Import the built module +Import-Module "$env:PIPELINE_WORKSPACE/ModuleBuild/Microsoft.PowerShell.Archive.psd1" + +$module = Get-Module -Name "Microsoft.PowerShell.Archive" +$module.Path | Write-Verbose + +# Load Pester +Install-Module -Name "Pester" -Force +$module = Get-Module -Name "Pester" +if ($null -ne $module) +{ + Remove-Module "Pester" +} +Import-Module -Name "Pester" + +# Run tests +$OutputFile = "$PWD/build-unit-tests.xml" +$results = $null +$results = Invoke-Pester -Script ./Tests/Compress-Archive.Tests.ps1 -OutputFile $OutputFile -PassThru -OutputFormat NUnitXml -Show Failed, Context, Describe, Fails +Write-Host "##vso[artifact.upload containerfolder=testResults;artifactname=testResults]$OutputFile" +if(!$results -or $results.FailedCount -gt 0 -or !$results.TotalCount) +{ + throw "Build or tests failed. Passed: $($results.PassedCount) Failed: $($results.FailedCount) Total: $($results.TotalCount)" +} + +# Unload module +$module = Get-Module -Name "Microsoft.PowerShell.Archive" +if ($module -ne $null) +{ + Remove-Module $module +} \ No newline at end of file diff --git a/.azdevops/runtest.yml b/.azdevops/runtest.yml index e4fe3a3..3b38785 100644 --- a/.azdevops/runtest.yml +++ b/.azdevops/runtest.yml @@ -21,44 +21,53 @@ jobs: artifact: ModuleBuild - pwsh: | - $module = Get-Module -Name "Microsoft.PowerShell.Archive" - if ($module -ne $null) + Write-Output ${{ parameters.vmImageName }} + $url = "https://github.com/PowerShell/PowerShell/releases/download/v7.3.0-preview.6/PowerShell-7.3.0-preview.6-win-x64.zip" + $isTar = $false + if ("${{ parameters.vmImageName }}" -like 'macos-*') { - Remove-Module $module + $url = "https://github.com/PowerShell/PowerShell/releases/download/v7.3.0-preview.6/powershell-7.3.0-preview.6-osx-x64.tar.gz" + $isTar = $true + Write-Output "Choose macOS" } - Import-Module $(Pipeline.Workspace)/ModuleBuild/Microsoft.PowerShell.Archive.psd1 - $module = Get-Module -Name "Microsoft.PowerShell.Archive" - $module.Path | Write-Output - displayName: Import Module from Build - - pwsh: | - Install-Module -Name "Pester" -Force - $module = Get-Module -Name "Pester" - if ($module -ne $null) + if ("${{ parameters.vmImageName }}" -like 'ubuntu-*') { - Remove-Module "Pester" - } - Import-Module -Name "Pester" - displayName: Install and Import Pester 4 - - - pwsh: | - $OutputFile = "$PWD/build-unit-tests.xml" - $results = $null - $results = Invoke-Pester -Script ./Tests/Compress-Archive.Tests.ps1 -OutputFile $OutputFile -PassThru -OutputFormat NUnitXml -Show Failed, Context, Describe, Fails - Write-Host "##vso[artifact.upload containerfolder=testResults;artifactname=testResults]$OutputFile" - if(!$results -or $results.FailedCount -gt 0 -or !$results.TotalCount) + $url = "https://github.com/PowerShell/PowerShell/releases/download/v7.3.0-preview.6/powershell-7.3.0-preview.6-linux-x64.tar.gz" + $isTar = $true + Write-Output "Choose Ubuntu" + } + $destination = "powershell-preview-archive" + if ($isTar) { + $destination += ".tar.gz" + } else { + $destination += ".zip" + } + Invoke-WebRequest -Uri $url -OutFile $destination + ## unpack the downloaded file + $powershellPreview = Join-Path $pwd "powershell-preview" + mkdir $powershellPreview + if ($isTar) { - throw "Build or tests failed. Passed: $($results.PassedCount) Failed: $($results.FailedCount) Total: $($results.TotalCount)" + gunzip -d $destination + $destination = $destination.Replace(".gz", "") + tar -x -f $destination -C $powershellPreview + } else { + Expand-Archive -Path $destination -DestinationPath $powershellPreview } - displayName: Run Build Unit Tests - - - pwsh: | - $module = Get-Module -Name "Microsoft.PowerShell.Archive" - if ($module -ne $null) + # Print contents of $powershellPreview + #Get-ChildItem $powershellPreview | Format-Table "Name" + $powershellPreview = Join-Path $powershellPreview "pwsh" + if ("${{ parameters.vmImageName }}" -like 'windows-*') { - Remove-Module $module + $powerShellPreview += ".exe" } - displayName: Remove Built Module from PSSession - condition: succeededOrFailed() + # Write the location of PowerShell Preview + Write-Host "##vso[task.setvariable variable=PowerShellPreviewExecutablePath;]$powershellPreview" + displayName: Download and Install PowerShell Preview + + - pwsh: | + "$(PowerShellPreviewExecutablePath) .azdevops/RunTests.ps1" | Invoke-Expression + displayName: Run Tests - task: PublishTestResults@2 displayName: 'Publish Test Results **/*tests.xml' diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index 0c13301..19cfad0 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -284,7 +284,7 @@ } } - It "Throws an error when Path and DestinationPath are the same and -Overwrite is specified" -Tag td { + It "Throws an error when Path and DestinationPath are the same and -Overwrite is specified" { $sourcePath = "$TestDrive$($DS)EmptyDirectory" $destinationPath = $sourcePath @@ -331,8 +331,34 @@ $_.FullyQualifiedErrorId | Should -Be "SameLiteralPathAndDestinationPath,Microsoft.PowerShell.Archive.CompressArchiveCommand" } } + } + + Context "WriteMode tests" { + BeforeAll { + New-Item $TestDrive$($DS)SourceDir -Type Directory | Out-Null + + $content = "Some Data" + $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)Sample-1.txt + } + It "Throws a terminating error when an incorrect value is supplied to -WriteMode" { + $sourcePath = "$TestDrive$($DS)SourceDir" + $destinationPath = "$TestDrive$($DS)archive1.zip" + try { + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode mode + } catch { + $_.FullyQualifiedErrorId | Should -Be "CannotConvertArgumentNoMessage,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } + + It "-WriteMode Create works" { + $sourcePath = "$TestDrive$($DS)SourceDir" + $destinationPath = "$TestDrive$($DS)archive1.zip" + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + Test-Path $destinationPath + Test-ZipArchive $destinationPath @('SourceDir/', 'SourceDir/Sample-1.txt') + } } Context "Basic functional tests" { @@ -382,7 +408,7 @@ } - Context "DestinationPath tests" { + Context "DestinationPath and -WriteMode Overwrite tests" { BeforeAll { New-Item $TestDrive$($DS)SourceDir -Type Directory | Out-Null @@ -392,6 +418,17 @@ New-Item $TestDrive$($DS)archive3.zip -Type Directory | Out-Null New-Item $TestDrive$($DS)EmptyDirectory -Type Directory | Out-Null + + # Create a read-only archive + $readOnlyArchivePath = "$TestDrive$($DS)readonly.zip" + Compress-Archive -Path $TestDrive$($DS)SourceDir$($DS)Sample-1.txt -DestinationPath $readOnlyArchivePath + Set-ItemProperty -Path $readOnlyArchivePath -Name IsReadOnly -Value $true + + # Create $TestDrive$($DS)archive.zip + Compress-Archive -Path $TestDrive$($DS)SourceDir$($DS)Sample-1.txt -DestinationPath "$TestDrive$($DS)archive.zip" + + # Create Sample-2.txt + $content | Out-File -FilePath $TestDrive$($DS)Sample-2.txt } It "Throws an error when archive file already exists and -Update and -Overwrite parameters are not specified" { @@ -410,18 +447,33 @@ } } - It "Throws a terminating error when archive does not exist and -Update mode is specified" { + It "Throws a terminating error when archive file exists and -Update is specified but the archive is read-only" { $sourcePath = "$TestDrive$($DS)SourceDir" - $destinationPath = "$TestDrive$($DS)archive2.zip" + $destinationPath = "$TestDrive$($DS)readonly.zip" try { Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Update - throw "Failed to validate that an archive file format $destinationPath does not exist and -Update switch parameter is specified." + throw "Failed to detect an that an error was thrown when archive $destinationPath already exists but it is read-only and -WriteMode Update is specified." } catch { - $_.FullyQualifiedErrorId | Should -Be "ArchiveDoesNotExist,Microsoft.PowerShell.Archive.CompressArchiveCommand" + $_.FullyQualifiedErrorId | Should -Be "ArchiveReadOnly,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } + + It "Throws a terminating error when archive already exists as a directory and -Update and -Overwrite parameters are not specified" { + $sourcePath = "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt" + $destinationPath = "$TestDrive$($DS)SourceDir" + + try + { + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + throw "Failed to detect an error was thrown when archive $destinationPath exists as a directory and -WriteMode Update or -WriteMode Overwrite is not specified." + } + catch + { + $_.FullyQualifiedErrorId | Should -Be "ArchiveExistsAsDirectory,Microsoft.PowerShell.Archive.CompressArchiveCommand" } } @@ -440,7 +492,7 @@ } } - It "Throws a terminating error when DestinationPath is a folder containing at least 1 item and Overwrite is specified" -Tag td2 { + It "Throws a terminating error when DestinationPath is a folder containing at least 1 item and Overwrite is specified" { $sourcePath = "$TestDrive$($DS)SourceDir" $destinationPath = "$TestDrive" @@ -454,22 +506,36 @@ $_.FullyQualifiedErrorId | Should -Be "ArchiveIsNonEmptyDirectory,Microsoft.PowerShell.Archive.CompressArchiveCommand" } } - } - Context "-Overwrite Tests" { - BeforeAll { - New-Item $TestDrive$($DS)SourceDir -Type Directory | Out-Null - - $content = "Some Data" - $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)Sample-1.txt + It "Throws a terminating error when archive does not exist and -Update mode is specified" { + $sourcePath = "$TestDrive$($DS)SourceDir" + $destinationPath = "$TestDrive$($DS)archive2.zip" - New-Item $TestDrive$($DS)EmptyDirectory -Type Directory | Out-Null + try + { + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Update + throw "Failed to validate that an archive file format $destinationPath does not exist and -Update switch parameter is specified." + } + catch + { + $_.FullyQualifiedErrorId | Should -Be "ArchiveDoesNotExist,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } - # Create $TestDrive$($DS)archive.zip - Compress-Archive -Path $TestDrive$($DS)SourceDir$($DS)Sample-1.txt -DestinationPath "$TestDrive$($DS)archive.zip" + ## Overwrite tests + It "Throws an error when trying to overwrite an empty directory, which is the working directory" { + $sourcePath = "$TestDrive$($DS)Sample-2.txt" + $destinationPath = "$TestDrive$($DS)EmptyDirectory" - # Create Sample-2.txt - $content | Out-File -FilePath $TestDrive$($DS)Sample-2.txt + Push-Location $destinationPath + + try { + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite + } catch { + $_.FullyQualifiedErrorId | Should -Be "CannotOverwriteWorkingDirectory,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + + Pop-Location } It "Overwrites a directory containing no items when -Overwrite is specified" { @@ -500,7 +566,7 @@ } } - Context "Relative Path tests" -Skip { + Context "Relative Path tests" { BeforeAll { New-Item $TestDrive$($DS)SourceDir -Type Directory | Out-Null New-Item $TestDrive$($DS)SourceDir$($DS)ChildDir-1 -Type Directory | Out-Null @@ -518,7 +584,7 @@ { Push-Location $TestDrive Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - Test-Path $destinationPath | Should Be $true + Test-Path $destinationPath | Should -Be $true } finally { @@ -534,7 +600,7 @@ { Push-Location $TestDrive Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath - Test-Path $destinationPath | Should Be $true + Test-Path $destinationPath | Should -Be $true } finally { @@ -550,7 +616,7 @@ { Push-Location $TestDrive Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - Test-Path $destinationPath | Should Be $true + Test-Path $destinationPath | Should -Be $true } finally { diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index c03f40b..926c830 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -84,7 +84,7 @@ public CompressArchiveCommand() protected override void BeginProcessing() { _destinationPathInfo = _pathHelper.ResolveToSingleFullyQualifiedPath(DestinationPath); - //DestinationPath = _destinationPathInfo.FullName; + DestinationPath = _destinationPathInfo.FullName; // Validate ValidateDestinationPath(); @@ -155,7 +155,7 @@ protected override void EndProcessing() } // Create an archive -- this is where we will switch between different types of archives - archive = ArchiveFactory.GetArchive(format: Format ?? ArchiveFormat.zip, archivePath: _destinationPathInfo.FullName, archiveMode: archiveMode, compressionLevel: CompressionLevel); + archive = ArchiveFactory.GetArchive(format: Format ?? ArchiveFormat.zip, archivePath: DestinationPath, archiveMode: archiveMode, compressionLevel: CompressionLevel); _didCreateNewArchive = archiveMode == ArchiveMode.Update; } @@ -246,6 +246,11 @@ private void ValidateDestinationPath() { errorCode = ErrorCode.ArchiveExistsAsDirectory; } + // Throw an error if the DestinationPath is the current working directory and the cmdlet is in Overwrite mode + else if (WriteMode == WriteMode.Overwrite && _destinationPathInfo.FullName == SessionState.Path.CurrentFileSystemLocation.ProviderPath) + { + errorCode = ErrorCode.CannotOverwriteWorkingDirectory; + } // Throw an error if the DestinationPath is a directory with at least item and the cmdlet is in Overwrite mode else if (WriteMode == WriteMode.Overwrite && (_destinationPathInfo as System.IO.DirectoryInfo).GetFileSystemInfos().Length > 0) { diff --git a/src/ErrorMessages.cs b/src/ErrorMessages.cs index 71fb8d8..dec8a6b 100644 --- a/src/ErrorMessages.cs +++ b/src/ErrorMessages.cs @@ -29,6 +29,7 @@ internal static string GetErrorMessage(ErrorCode errorCode) ErrorCode.SameLiteralPathAndDestinationPath => Messages.SameLiteralPathAndDestinationPathMessage, ErrorCode.InsufficientPermissionsToAccessPath => Messages.InsufficientPermssionsToAccessPathMessage, ErrorCode.OverwriteDestinationPathFailed => Messages.OverwriteDestinationPathFailed, + ErrorCode.CannotOverwriteWorkingDirectory => Messages.CannotOverwriteWorkingDirectoryMessage, _ => throw new NotImplementedException("Error code has not been implemented") }; } @@ -59,6 +60,8 @@ internal enum ErrorCode // Used when the user does not have sufficient permissions to access a path InsufficientPermissionsToAccessPath, // Used when the cmdlet could not overwrite DestinationPath - OverwriteDestinationPathFailed + OverwriteDestinationPathFailed, + // Used when the user enters the working directory as DestinationPath and it is an existing folder and -WriteMode Overwrite is specified + CannotOverwriteWorkingDirectory } } diff --git a/src/Localized/Messages.Designer.cs b/src/Localized/Messages.Designer.cs index bd42e82..55bdff9 100644 --- a/src/Localized/Messages.Designer.cs +++ b/src/Localized/Messages.Designer.cs @@ -141,6 +141,15 @@ internal static string ArchiveIsReadOnlyMessage { } } + /// + /// Looks up a localized string similar to Unable to overwrite the path {0} because it is the same as the current working directory.. + /// + internal static string CannotOverwriteWorkingDirectoryMessage { + get { + return ResourceManager.GetString("CannotOverwriteWorkingDirectoryMessage", resourceCulture); + } + } + /// /// Looks up a localized string similar to The path(s) {0} have been specified more than once.. /// diff --git a/src/Localized/Messages.resx b/src/Localized/Messages.resx index 3788143..ea04ed3 100644 --- a/src/Localized/Messages.resx +++ b/src/Localized/Messages.resx @@ -144,6 +144,9 @@ The archive at {0} is read-only. + + Unable to overwrite the path {0} because it is the same as the current working directory. + The path(s) {0} have been specified more than once. From 03ce64a002a4b4d58bc5d9f7664742b619d9eeb2 Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Thu, 28 Jul 2022 17:31:22 -0700 Subject: [PATCH 32/42] used pascal case for ArchiveFormat enum members, refactored some code based on feedback --- .gitignore | 3 + src/ArchiveFactory.cs | 29 ++-- src/ArchiveFormat.cs | 6 +- src/CompressArchiveCommand.cs | 52 +++---- src/Localized/Messages.Designer.cs | 225 ----------------------------- 5 files changed, 41 insertions(+), 274 deletions(-) delete mode 100644 src/Localized/Messages.Designer.cs diff --git a/.gitignore b/.gitignore index 2b3834d..c575e6e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ # VSCode .vscode +# Auto-generated resx code +src/Localized/Messages.Designer.cs + # Visual Studio Launch Settings src/Properties diff --git a/src/ArchiveFactory.cs b/src/ArchiveFactory.cs index 2602894..3d6c926 100644 --- a/src/ArchiveFactory.cs +++ b/src/ArchiveFactory.cs @@ -16,35 +16,30 @@ internal static IArchive GetArchive(ArchiveFormat format, string archivePath, Ar ArchiveMode.Update => new System.IO.FileStream(archivePath, mode: System.IO.FileMode.Open, access: System.IO.FileAccess.ReadWrite, share: System.IO.FileShare.None), ArchiveMode.Extract => new System.IO.FileStream(archivePath, mode: System.IO.FileMode.Open, access: System.IO.FileAccess.Read, share: System.IO.FileShare.Read), // TODO: Add message to exception - _ => throw new NotImplementedException() + _ => throw new ArgumentOutOfRangeException(nameof(archiveMode)) }; return format switch { - ArchiveFormat.zip => new ZipArchive(archivePath, archiveMode, archiveFileStream, compressionLevel), + ArchiveFormat.Zip => new ZipArchive(archivePath, archiveMode, archiveFileStream, compressionLevel), //ArchiveFormat.tar => new TarArchive(archivePath, archiveMode, archiveFileStream), // TODO: Add archive types here // TODO: Add message to exception - _ => throw new NotImplementedException() + _ => throw new ArgumentOutOfRangeException(nameof(archiveMode)) }; } - internal static bool TryGetArchiveFormatForPath(string path, out ArchiveFormat? archiveFormat) + internal static bool TryGetArchiveFormatFromExtension(string path, out ArchiveFormat? archiveFormat) { - archiveFormat = null; - if (path.EndsWith(".zip")) + archiveFormat = System.IO.Path.GetExtension(path) switch { - archiveFormat = ArchiveFormat.zip; - } - /* Disable support for tar and tar.gz for preview1 release - if (path.EndsWith(".tar")) - { - archiveFormat = ArchiveFormat.tar; - } - if (path.EndsWith(".tar.gz") || path.EndsWith(".tgz")) - { - archiveFormat = ArchiveFormat.tgz; - }*/ + ".zip" => archiveFormat = ArchiveFormat.Zip, + /* Disable support for tar and tar.gz for preview1 release + ".tar" => archiveFormat = ArchiveFormat.Tar, + ".gz" => archiveFormat = ArchiveFormat.Tgz, + */ + _ => null + }; return archiveFormat != null; } } diff --git a/src/ArchiveFormat.cs b/src/ArchiveFormat.cs index 245b398..0f0d1a0 100644 --- a/src/ArchiveFormat.cs +++ b/src/ArchiveFormat.cs @@ -6,9 +6,9 @@ namespace Microsoft.PowerShell.Archive { public enum ArchiveFormat { - zip, + Zip, /* Removing these formats for preview relase - tar, - tgz*/ + Tar, + Tgz*/ } } diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index 926c830..bdb2a79 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -5,12 +5,12 @@ using System.IO; using System.Linq; using System.Management.Automation; -using System.Reflection; +using System.IO.Compression; namespace Microsoft.PowerShell.Archive { [Cmdlet("Compress", "Archive", SupportsShouldProcess = true)] - [OutputType(typeof(System.IO.FileInfo))] + [OutputType(typeof(FileInfo))] public class CompressArchiveCommand : PSCmdlet { @@ -38,7 +38,7 @@ public class CompressArchiveCommand : PSCmdlet /// The LiteralPath parameter - specifies paths of files or directories from the filesystem to add to or update in the archive. /// This parameter does not expand wildcard characters. /// - [Parameter(Mandatory = true, Position = 1, ParameterSetName = "LiteralPath", ValueFromPipeline = false, ValueFromPipelineByPropertyName = true)] + [Parameter(Mandatory = true, ParameterSetName = "LiteralPath", ValueFromPipeline = false, ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] [Alias("PSPath")] public string[]? LiteralPath { get; set; } @@ -55,20 +55,20 @@ public class CompressArchiveCommand : PSCmdlet public WriteMode WriteMode { get; set; } = WriteMode.Create; [Parameter()] - public SwitchParameter PassThru { get; set; } = false; + public SwitchParameter PassThru { get; set; } [Parameter()] [ValidateNotNullOrEmpty] - public System.IO.Compression.CompressionLevel CompressionLevel { get; set; } = System.IO.Compression.CompressionLevel.Optimal; + public CompressionLevel CompressionLevel { get; set; } [Parameter()] public ArchiveFormat? Format { get; set; } = null; private List? _sourcePaths; - private PathHelper _pathHelper; + private readonly PathHelper _pathHelper; - private System.IO.FileSystemInfo? _destinationPathInfo; + private FileSystemInfo? _destinationPathInfo; private bool _didCreateNewArchive; @@ -96,7 +96,7 @@ protected override void BeginProcessing() protected override void ProcessRecord() { // Add each path from -Path or -LiteralPath to _sourcePaths because they can get lost when the next item in the pipeline is sent - if (ParameterSetName.StartsWith("Path")) + if (ParameterSetName == "Path") { _sourcePaths?.AddRange(Path); } @@ -110,7 +110,7 @@ protected override void EndProcessing() { // Get archive entries, validation is performed by PathHelper // _sourcePaths should not be null at this stage, but if it is, prevent a NullReferenceException by doing the following - List archiveAddtions = _sourcePaths != null ? _pathHelper.GetArchiveAdditionsForPath(_sourcePaths.ToArray(), ParameterSetName.StartsWith("LiteralPath")) : new List(); + List archiveAddtions = _sourcePaths != null ? _pathHelper.GetArchiveAdditionsForPath(_sourcePaths.ToArray(), ParameterSetName == "LiteralPath") : new List(); // Remove references to _sourcePaths, Path, and LiteralPath to free up memory // The user could have supplied a lot of paths, so we should do this @@ -124,7 +124,7 @@ protected override void EndProcessing() if (additionsWithSamePathAsDestination.Count() > 0) { // Since duplicate checking is performed earlier, there must a single ArchiveAddition such that ArchiveAddition.FullPath == DestinationPath - var errorCode = ParameterSetName.StartsWith("Path") ? ErrorCode.SamePathAndDestinationPath : ErrorCode.SameLiteralPathAndDestinationPath; + var errorCode = ParameterSetName == "Path" ? ErrorCode.SamePathAndDestinationPath : ErrorCode.SameLiteralPathAndDestinationPath; var errorRecord = ErrorMessages.GetErrorRecord(errorCode, errorItem: additionsWithSamePathAsDestination[0].FileSystemInfo.FullName); ThrowTerminatingError(errorRecord); } @@ -155,7 +155,7 @@ protected override void EndProcessing() } // Create an archive -- this is where we will switch between different types of archives - archive = ArchiveFactory.GetArchive(format: Format ?? ArchiveFormat.zip, archivePath: DestinationPath, archiveMode: archiveMode, compressionLevel: CompressionLevel); + archive = ArchiveFactory.GetArchive(format: Format ?? ArchiveFormat.Zip, archivePath: DestinationPath, archiveMode: archiveMode, compressionLevel: CompressionLevel); _didCreateNewArchive = archiveMode == ArchiveMode.Update; } @@ -199,8 +199,8 @@ protected override void EndProcessing() // If -PassThru is specified, write a System.IO.FileInfo object if (PassThru) { - var archiveInfo = new System.IO.FileInfo(_destinationPathInfo.FullName); - WriteObject(archiveInfo); + _destinationPathInfo = new FileInfo(_destinationPathInfo.FullName); + WriteObject(_destinationPathInfo); } } @@ -209,10 +209,7 @@ protected override void StopProcessing() // If a new output archive was created, delete it (this does not delete an archive if -WriteMode Update is specified) if (_didCreateNewArchive) { - if (System.IO.File.Exists(_destinationPathInfo.FullName)) - { - System.IO.File.Delete(_destinationPathInfo.FullName); - } + _destinationPathInfo?.Delete(); } } @@ -251,8 +248,8 @@ private void ValidateDestinationPath() { errorCode = ErrorCode.CannotOverwriteWorkingDirectory; } - // Throw an error if the DestinationPath is a directory with at least item and the cmdlet is in Overwrite mode - else if (WriteMode == WriteMode.Overwrite && (_destinationPathInfo as System.IO.DirectoryInfo).GetFileSystemInfos().Length > 0) + // Throw an error if the DestinationPath is a directory with at 1 least item and the cmdlet is in Overwrite mode + else if (WriteMode == WriteMode.Overwrite && (_destinationPathInfo as DirectoryInfo).GetFileSystemInfos().Length > 0) { errorCode = ErrorCode.ArchiveIsNonEmptyDirectory; } @@ -284,18 +281,15 @@ private void DeleteDestinationPathIfExists() { try { - if (System.IO.File.Exists(_destinationPathInfo.FullName)) - { - System.IO.File.Delete(_destinationPathInfo.FullName); - } - // TODO: Ensure DestinationPath has no children when deleting it - if (System.IO.Directory.Exists(_destinationPathInfo.FullName)) + // No need to ensure DestinationPath has no children when deleting it + // because ValidateDestinationPath should have already done this + if (_destinationPathInfo.Exists) { - System.IO.Directory.Delete(_destinationPathInfo.FullName); + _destinationPathInfo?.Delete(); } } // Throw a terminating error if an IOException occurs - catch (System.IO.IOException ioException) + catch (IOException ioException) { var errorRecord = new ErrorRecord(ioException, errorId: ErrorCode.OverwriteDestinationPathFailed.ToString(), errorCategory: ErrorCategory.InvalidOperation, targetObject: _destinationPathInfo.FullName); @@ -313,7 +307,7 @@ private void DeleteDestinationPathIfExists() private void DetermineArchiveFormat() { // Check if cmdlet is able to determine the format of the archive based on the extension of DestinationPath - bool ableToDetermineArchiveFormat = ArchiveFactory.TryGetArchiveFormatForPath(path: _destinationPathInfo.FullName, archiveFormat: out var archiveFormat); + bool ableToDetermineArchiveFormat = ArchiveFactory.TryGetArchiveFormatFromExtension(path: _destinationPathInfo.FullName, archiveFormat: out var archiveFormat); // If the user did not specify which archive format to use, try to determine it automatically if (Format is null) { @@ -326,7 +320,7 @@ private void DetermineArchiveFormat() // If the archive format could not be determined, use zip by default and emit a warning var warningMsg = String.Format(Messages.ArchiveFormatCouldNotBeDeterminedWarning, _destinationPathInfo.FullName); WriteWarning(warningMsg); - Format = ArchiveFormat.zip; + Format = ArchiveFormat.Zip; } // Write a verbose message saying that Format is not specified and a format was determined automatically string verboseMessage = String.Format(Messages.ArchiveFormatDeterminedVerboseMessage, Format); diff --git a/src/Localized/Messages.Designer.cs b/src/Localized/Messages.Designer.cs deleted file mode 100644 index 55bdff9..0000000 --- a/src/Localized/Messages.Designer.cs +++ /dev/null @@ -1,225 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Microsoft.PowerShell.Archive.Localized { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Messages { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Messages() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.PowerShell.Archive.Localized.Messages", typeof(Messages).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to {0} was added to the archive.. - /// - internal static string AddedItemToArchiveVerboseMessage { - get { - return ResourceManager.GetString("AddedItemToArchiveVerboseMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The archive {0} does not exist.. - /// - internal static string ArchiveDoesNotExistMessage { - get { - return ResourceManager.GetString("ArchiveDoesNotExistMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The destination path {0} is a directory.. - /// - internal static string ArchiveExistsAsDirectoryMessage { - get { - return ResourceManager.GetString("ArchiveExistsAsDirectoryMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The destination path {0} already exists.. - /// - internal static string ArchiveExistsMessage { - get { - return ResourceManager.GetString("ArchiveExistsMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The archive {0} does not have an extension or an extension that matches the chosen archive format.. - /// - internal static string ArchiveExtensionDoesNotMatchArchiveFormatWarning { - get { - return ResourceManager.GetString("ArchiveExtensionDoesNotMatchArchiveFormatWarning", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The format of the archive {0} could not determined by its extension. The zip format is chosen by default.. - /// - internal static string ArchiveFormatCouldNotBeDeterminedWarning { - get { - return ResourceManager.GetString("ArchiveFormatCouldNotBeDeterminedWarning", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The -Format was not specified, so the archive format was determined to be {0} based on its extension.. - /// - internal static string ArchiveFormatDeterminedVerboseMessage { - get { - return ResourceManager.GetString("ArchiveFormatDeterminedVerboseMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The archive {0} cannot be overwritten because it is a non-empty directory.. - /// - internal static string ArchiveIsNonEmptyDirectory { - get { - return ResourceManager.GetString("ArchiveIsNonEmptyDirectory", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The archive at {0} is read-only.. - /// - internal static string ArchiveIsReadOnlyMessage { - get { - return ResourceManager.GetString("ArchiveIsReadOnlyMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unable to overwrite the path {0} because it is the same as the current working directory.. - /// - internal static string CannotOverwriteWorkingDirectoryMessage { - get { - return ResourceManager.GetString("CannotOverwriteWorkingDirectoryMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The path(s) {0} have been specified more than once.. - /// - internal static string DuplicatePathsMessage { - get { - return ResourceManager.GetString("DuplicatePathsMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to There are insufficient permissions to access the path {0}.. - /// - internal static string InsufficientPermssionsToAccessPathMessage { - get { - return ResourceManager.GetString("InsufficientPermssionsToAccessPathMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The path(s) {0} are invalid.. - /// - internal static string InvalidPathMessage { - get { - return ResourceManager.GetString("InvalidPathMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to There are no items to add to the archive.. - /// - internal static string NoItemsToAddWarning { - get { - return ResourceManager.GetString("NoItemsToAddWarning", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Could not overwrite the destination path.. - /// - internal static string OverwriteDestinationPathFailed { - get { - return ResourceManager.GetString("OverwriteDestinationPathFailed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The path {0} could not be found.. - /// - internal static string PathNotFoundMessage { - get { - return ResourceManager.GetString("PathNotFoundMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A path {0} supplied to -LiteralPath is the same as the path supplied to -DestinationPath.. - /// - internal static string SameLiteralPathAndDestinationPathMessage { - get { - return ResourceManager.GetString("SameLiteralPathAndDestinationPathMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A path {0} supplied to -Path is the same as the path supplied to -DestinationPath.. - /// - internal static string SamePathAndDestinationPathMessage { - get { - return ResourceManager.GetString("SamePathAndDestinationPathMessage", resourceCulture); - } - } - } -} From d9c0e8e9c00796273ba7bd5ed649a46cb45f8057 Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Fri, 29 Jul 2022 10:58:24 -0700 Subject: [PATCH 33/42] fixed formatting for multiple files, updated csproj to generate Messages.Designer.cs and to remove debug symbols in Release config --- .azdevops/CI.yml | 2 +- .azdevops/RunTests.ps1 | 2 +- .azdevops/build.ps1 | 6 +- .gitattributes | 66 +- .gitignore | 2 +- .globalconfig | 2253 +++++++++++++++++++++++ Microsoft.PowerShell.Archive.sln | 3 +- README.md | 4 - SimpleBuild.ps1 | 20 +- Tests/Compress-Archive.Tests.ps1 | 2 +- src/Localized/Messages.resx | 2 +- src/Microsoft.PowerShell.Archive.csproj | 22 +- src/Microsoft.PowerShell.Archive.psd1 | 113 +- 13 files changed, 2290 insertions(+), 207 deletions(-) create mode 100644 .globalconfig diff --git a/.azdevops/CI.yml b/.azdevops/CI.yml index badedc9..9cd5641 100644 --- a/.azdevops/CI.yml +++ b/.azdevops/CI.yml @@ -75,4 +75,4 @@ stages: parameters: vmImageName: macos-latest jobName: run_test_macos - jobDisplayName: Run macOS tests \ No newline at end of file + jobDisplayName: Run macOS tests diff --git a/.azdevops/RunTests.ps1 b/.azdevops/RunTests.ps1 index 02eed15..4eeaec2 100644 --- a/.azdevops/RunTests.ps1 +++ b/.azdevops/RunTests.ps1 @@ -35,4 +35,4 @@ $module = Get-Module -Name "Microsoft.PowerShell.Archive" if ($module -ne $null) { Remove-Module $module -} \ No newline at end of file +} diff --git a/.azdevops/build.ps1 b/.azdevops/build.ps1 index fea6e2e..630610a 100644 --- a/.azdevops/build.ps1 +++ b/.azdevops/build.ps1 @@ -13,14 +13,11 @@ param ( $root = (Resolve-Path -Path "${PSScriptRoot}/../")[0] $Name = "Microsoft.PowerShell.Archive" -$BuildOutputDir = Join-Path $root "\src\bin\release\net7.0" +$BuildOutputDir = Join-Path $root "\src\bin\Release" $ManifestPath = "${BuildOutputDir}\${Name}.psd1" $ManifestData = Import-PowerShellDataFile -Path $ManifestPath $Version = $ManifestData.ModuleVersion -# Path for signed -#$SignedPath = Join-Path $(Build.SourcesDirectory) "signed" - $SignRoot = "${root}\signed\${Name}" $SignVersion = "$SignRoot\$Version" @@ -95,7 +92,6 @@ if ($publish) { } Copy-Item -Path $src -destination $targetDir -Verbose:$verboseValue - } } diff --git a/.gitattributes b/.gitattributes index 1ff0c42..e342d6f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,63 +1,5 @@ -############################################################################### -# Set default behavior to automatically normalize line endings. -############################################################################### +CHANGELOG.md merge=union * text=auto - -############################################################################### -# Set default behavior for command prompt diff. -# -# This is need for earlier builds of msysgit that does not have it on by -# default for csharp files. -# Note: This is only used by command line -############################################################################### -#*.cs diff=csharp - -############################################################################### -# Set the merge driver for project and solution files -# -# Merging from the command prompt will add diff markers to the files if there -# are conflicts (Merging from VS is not affected by the settings below, in VS -# the diff markers are never inserted). Diff markers may cause the following -# file extensions to fail to load in VS. An alternative would be to treat -# these files as binary and thus will always conflict and require user -# intervention with every merge. To do so, just uncomment the entries below -############################################################################### -#*.sln merge=binary -#*.csproj merge=binary -#*.vbproj merge=binary -#*.vcxproj merge=binary -#*.vcproj merge=binary -#*.dbproj merge=binary -#*.fsproj merge=binary -#*.lsproj merge=binary -#*.wixproj merge=binary -#*.modelproj merge=binary -#*.sqlproj merge=binary -#*.wwaproj merge=binary - -############################################################################### -# behavior for image files -# -# image files are treated as binary by default. -############################################################################### -#*.jpg binary -#*.png binary -#*.gif binary - -############################################################################### -# diff behavior for common document formats -# -# Convert binary document formats to text before diffing them. This feature -# is only available from the command line. Turn it on by uncommenting the -# entries below. -############################################################################### -#*.doc diff=astextplain -#*.DOC diff=astextplain -#*.docx diff=astextplain -#*.DOCX diff=astextplain -#*.dot diff=astextplain -#*.DOT diff=astextplain -#*.pdf diff=astextplain -#*.PDF diff=astextplain -#*.rtf diff=astextplain -#*.RTF diff=astextplain +*.png binary +*.rtf binary +*.sh text eol=lf diff --git a/.gitignore b/.gitignore index c575e6e..f9af6f6 100644 --- a/.gitignore +++ b/.gitignore @@ -369,4 +369,4 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd diff --git a/.globalconfig b/.globalconfig new file mode 100644 index 0000000..c51a485 --- /dev/null +++ b/.globalconfig @@ -0,0 +1,2253 @@ +is_global = true + +# CA1000: Do not declare static members on generic types +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1000 +dotnet_diagnostic.CA1000.severity = warning +dotnet_code_quality.CA1000.api_surface = all + +# CA1001: Types that own disposable fields should be disposable +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1001 +dotnet_diagnostic.CA1001.severity = silent + +# CA1002: Do not expose generic lists +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1002 +dotnet_diagnostic.CA1002.severity = none + +# CA1003: Use generic event handler instances +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1003 +dotnet_diagnostic.CA1003.severity = warning +dotnet_code_quality.CA1003.api_surface = private, internal + +# CA1005: Avoid excessive parameters on generic types +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1005 +dotnet_diagnostic.CA1005.severity = none + +# CA1008: Enums should have zero value +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1008 +dotnet_diagnostic.CA1008.severity = none +dotnet_code_quality.CA1008.api_surface = public + +# CA1010: Generic interface should also be implemented +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1010 +dotnet_diagnostic.CA1010.severity = silent +dotnet_code_quality.CA1010.api_surface = public + +# CA1012: Abstract types should not have public constructors +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1012 +dotnet_diagnostic.CA1012.severity = warning +dotnet_code_quality.CA1012.api_surface = all + +# CA1014: Mark assemblies with CLSCompliant +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1014 +dotnet_diagnostic.CA1014.severity = none + +# CA1016: Mark assemblies with assembly version +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1016 +dotnet_diagnostic.CA1016.severity = warning + +# CA1017: Mark assemblies with ComVisible +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1017 +dotnet_diagnostic.CA1017.severity = none + +# CA1018: Mark attributes with AttributeUsageAttribute +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1018 +dotnet_diagnostic.CA1018.severity = warning + +# CA1019: Define accessors for attribute arguments +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1019 +dotnet_diagnostic.CA1019.severity = none + +# CA1021: Avoid out parameters +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1021 +dotnet_diagnostic.CA1021.severity = none + +# CA1024: Use properties where appropriate +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1024 +dotnet_diagnostic.CA1024.severity = none +dotnet_code_quality.CA1024.api_surface = public + +# CA1027: Mark enums with FlagsAttribute +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1027 +dotnet_diagnostic.CA1027.severity = none +dotnet_code_quality.CA1027.api_surface = public + +# CA1028: Enum Storage should be Int32 +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1028 +dotnet_diagnostic.CA1028.severity = none +dotnet_code_quality.CA1028.api_surface = public + +# CA1030: Use events where appropriate +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1030 +dotnet_diagnostic.CA1030.severity = none +dotnet_code_quality.CA1030.api_surface = public + +# CA1031: Do not catch general exception types +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1031 +dotnet_diagnostic.CA1031.severity = none + +# CA1032: Implement standard exception constructors +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1032 +dotnet_diagnostic.CA1032.severity = none + +# CA1033: Interface methods should be callable by child types +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1033 +dotnet_diagnostic.CA1033.severity = none + +# CA1034: Nested types should not be visible +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1034 +dotnet_diagnostic.CA1034.severity = none + +# CA1036: Override methods on comparable types +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1036 +dotnet_diagnostic.CA1036.severity = silent +dotnet_code_quality.CA1036.api_surface = public + +# CA1040: Avoid empty interfaces +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1040 +dotnet_diagnostic.CA1040.severity = none +dotnet_code_quality.CA1040.api_surface = public + +# CA1041: Provide ObsoleteAttribute message +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1041 +dotnet_diagnostic.CA1041.severity = warning +dotnet_code_quality.CA1041.api_surface = public + +# CA1043: Use Integral Or String Argument For Indexers +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1043 +dotnet_diagnostic.CA1043.severity = warning +dotnet_code_quality.CA1043.api_surface = all + +# CA1044: Properties should not be write only +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1044 +dotnet_diagnostic.CA1044.severity = none +dotnet_code_quality.CA1044.api_surface = public + +# CA1045: Do not pass types by reference +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1045 +dotnet_diagnostic.CA1045.severity = none + +# CA1046: Do not overload equality operator on reference types +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1046 +dotnet_diagnostic.CA1046.severity = none + +# CA1047: Do not declare protected member in sealed type +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1047 +dotnet_diagnostic.CA1047.severity = warning + +# CA1050: Declare types in namespaces +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1050 +dotnet_diagnostic.CA1050.severity = warning + +# CA1051: Do not declare visible instance fields +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1051 +dotnet_diagnostic.CA1051.severity = silent +dotnet_code_quality.CA1051.api_surface = public + +# CA1052: Static holder types should be Static or NotInheritable +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1052 +dotnet_diagnostic.CA1052.severity = warning +dotnet_code_quality.CA1052.api_surface = all + +# CA1054: URI-like parameters should not be strings +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1054 +dotnet_diagnostic.CA1054.severity = none +dotnet_code_quality.CA1054.api_surface = public + +# CA1055: URI-like return values should not be strings +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1055 +dotnet_diagnostic.CA1055.severity = none +dotnet_code_quality.CA1055.api_surface = public + +# CA1056: URI-like properties should not be strings +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1056 +dotnet_diagnostic.CA1056.severity = none +dotnet_code_quality.CA1056.api_surface = public + +# CA1058: Types should not extend certain base types +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1058 +dotnet_diagnostic.CA1058.severity = none +dotnet_code_quality.CA1058.api_surface = public + +# CA1060: Move pinvokes to native methods class +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1060 +dotnet_diagnostic.CA1060.severity = none + +# CA1061: Do not hide base class methods +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1061 +dotnet_diagnostic.CA1061.severity = warning + +# CA1062: Validate arguments of public methods +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1062 +dotnet_diagnostic.CA1062.severity = none + +# CA1063: Implement IDisposable Correctly +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1063 +dotnet_diagnostic.CA1063.severity = none +dotnet_code_quality.CA1063.api_surface = public + +# CA1064: Exceptions should be public +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1064 +dotnet_diagnostic.CA1064.severity = none + +# CA1065: Do not raise exceptions in unexpected locations +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1065 +dotnet_diagnostic.CA1065.severity = warning + +# CA1066: Implement IEquatable when overriding Object.Equals +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1066 +dotnet_diagnostic.CA1066.severity = none + +# CA1067: Override Object.Equals(object) when implementing IEquatable +# # https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1067 +dotnet_diagnostic.CA1067.severity = warning + +# CA1068: CancellationToken parameters must come last +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1068 +dotnet_diagnostic.CA1068.severity = warning + +# CA1069: Enums values should not be duplicated +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1069 +dotnet_diagnostic.CA1069.severity = suggestion + +# CA1070: Do not declare event fields as virtual +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1070 +dotnet_diagnostic.CA1070.severity = warning + +# CA1200: Avoid using cref tags with a prefix +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1200 +dotnet_diagnostic.CA1200.severity = silent + +# CA1303: Do not pass literals as localized parameters +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1303 +dotnet_diagnostic.CA1303.severity = none + +# CA1304: Specify CultureInfo +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1304 +dotnet_diagnostic.CA1304.severity = silent + +# CA1305: Specify IFormatProvider +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305 +dotnet_diagnostic.CA1305.severity = silent + +# CA1307: Specify StringComparison for clarity +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1307 +dotnet_diagnostic.CA1307.severity = none + +# CA1308: Normalize strings to uppercase +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1308 +dotnet_diagnostic.CA1308.severity = none + +# CA1309: Use ordinal string comparison +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1309 +dotnet_diagnostic.CA1309.severity = silent + +# CA1310: Specify StringComparison for correctness +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1310 +dotnet_diagnostic.CA1310.severity = silent + +# CA1401: P/Invokes should not be visible +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1401 +dotnet_diagnostic.CA1401.severity = warning + +# CA1416: Validate platform compatibility +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416 +dotnet_diagnostic.CA1416.severity = warning + +# CA1417: Do not use 'OutAttribute' on string parameters for P/Invokes +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1417 +dotnet_diagnostic.CA1417.severity = warning + +# CA1418: Use valid platform string +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1418 +dotnet_diagnostic.CA1418.severity = warning + +# CA1501: Avoid excessive inheritance +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1501 +dotnet_diagnostic.CA1501.severity = none + +# CA1502: Avoid excessive complexity +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1502 +dotnet_diagnostic.CA1502.severity = none + +# CA1505: Avoid unmaintainable code +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1505 +dotnet_diagnostic.CA1505.severity = none + +# CA1506: Avoid excessive class coupling +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1506 +dotnet_diagnostic.CA1506.severity = none + +# CA1507: Use nameof to express symbol names +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1507 +dotnet_diagnostic.CA1507.severity = suggestion + +# CA1508: Avoid dead conditional code +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1508 +dotnet_diagnostic.CA1508.severity = none + +# CA1509: Invalid entry in code metrics rule specification file +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1509 +dotnet_diagnostic.CA1509.severity = none + +# CA1700: Do not name enum values 'Reserved' +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1700 +dotnet_diagnostic.CA1700.severity = none + +# CA1707: Identifiers should not contain underscores +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1707 +dotnet_diagnostic.CA1707.severity = silent + +# CA1708: Identifiers should differ by more than case +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1708 +dotnet_diagnostic.CA1708.severity = silent +dotnet_code_quality.CA1708.api_surface = public + +# CA1710: Identifiers should have correct suffix +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1710 +dotnet_diagnostic.CA1710.severity = silent +dotnet_code_quality.CA1710.api_surface = public + +# CA1711: Identifiers should not have incorrect suffix +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1711 +dotnet_diagnostic.CA1711.severity = silent +dotnet_code_quality.CA1711.api_surface = public + +# CA1712: Do not prefix enum values with type name +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1712 +dotnet_diagnostic.CA1712.severity = silent + +# CA1713: Events should not have 'Before' or 'After' prefix +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1713 +dotnet_diagnostic.CA1713.severity = none + +# CA1715: Identifiers should have correct prefix +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1715 +dotnet_diagnostic.CA1715.severity = silent +dotnet_code_quality.CA1715.api_surface = public + +# CA1716: Identifiers should not match keywords +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1716 +dotnet_diagnostic.CA1716.severity = silent +dotnet_code_quality.CA1716.api_surface = public + +# CA1720: Identifier contains type name +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1720 +dotnet_diagnostic.CA1720.severity = silent +dotnet_code_quality.CA1720.api_surface = public + +# CA1721: Property names should not match get methods +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1721 +dotnet_diagnostic.CA1721.severity = none +dotnet_code_quality.CA1721.api_surface = public + +# CA1724: Type names should not match namespaces +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1724 +dotnet_diagnostic.CA1724.severity = none + +# CA1725: Parameter names should match base declaration +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1725 +dotnet_diagnostic.CA1725.severity = silent +dotnet_code_quality.CA1725.api_surface = public + +# CA1801: Review unused parameters +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1801 +dotnet_diagnostic.CA1801.severity = none +dotnet_code_quality.CA1801.api_surface = all + +# CA1802: Use literals where appropriate +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1802 +dotnet_diagnostic.CA1802.severity = none +dotnet_code_quality.CA1802.api_surface = public + +# CA1805: Do not initialize unnecessarily +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1805 +dotnet_diagnostic.CA1805.severity = suggestion + +# CA1806: Do not ignore method results +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1806 +dotnet_diagnostic.CA1806.severity = suggestion + +# CA1810: Initialize reference type static fields inline +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1810 +dotnet_diagnostic.CA1810.severity = none + +# CA1812: Avoid uninstantiated internal classes +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1812 +dotnet_diagnostic.CA1812.severity = warning + +# CA1813: Avoid unsealed attributes +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1813 +dotnet_diagnostic.CA1813.severity = none + +# CA1814: Prefer jagged arrays over multidimensional +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1814 +dotnet_diagnostic.CA1814.severity = none + +# CA1815: Override equals and operator equals on value types +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1815 +dotnet_diagnostic.CA1815.severity = none +dotnet_code_quality.CA1815.api_surface = public + +# CA1816: Dispose methods should call SuppressFinalize +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1816 +dotnet_diagnostic.CA1816.severity = warning + +# CA1819: Properties should not return arrays +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1819 +dotnet_diagnostic.CA1819.severity = none +dotnet_code_quality.CA1819.api_surface = public + +# CA1820: Test for empty strings using string length +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1820 +dotnet_diagnostic.CA1820.severity = none + +# CA1821: Remove empty Finalizers +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1821 +dotnet_diagnostic.CA1821.severity = warning + +# CA1822: Mark members as static +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822 +dotnet_diagnostic.CA1822.severity = warning +dotnet_code_quality.CA1822.api_surface = private + +# CA1823: Avoid unused private fields +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1823 +dotnet_diagnostic.CA1823.severity = none + +# CA1824: Mark assemblies with NeutralResourcesLanguageAttribute +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1824 +dotnet_diagnostic.CA1824.severity = warning + +# CA1825: Avoid zero-length array allocations +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1825 +dotnet_diagnostic.CA1825.severity = warning + +# CA1826: Do not use Enumerable methods on indexable collections +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1826 +dotnet_diagnostic.CA1826.severity = warning + +# CA1827: Do not use Count() or LongCount() when Any() can be used +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1827 +dotnet_diagnostic.CA1827.severity = warning + +# CA1828: Do not use CountAsync() or LongCountAsync() when AnyAsync() can be used +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1828 +dotnet_diagnostic.CA1828.severity = warning + +# CA1829: Use Length/Count property instead of Count() when available +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1829 +dotnet_diagnostic.CA1829.severity = warning + +# CA1830: Prefer strongly-typed Append and Insert method overloads on StringBuilder +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1830 +dotnet_diagnostic.CA1830.severity = warning + +# CA1831: Use AsSpan or AsMemory instead of Range-based indexers when appropriate +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1831 +dotnet_diagnostic.CA1831.severity = warning + +# CA1832: Use AsSpan or AsMemory instead of Range-based indexers when appropriate +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1832 +dotnet_diagnostic.CA1832.severity = warning + +# CA1833: Use AsSpan or AsMemory instead of Range-based indexers when appropriate +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1833 +dotnet_diagnostic.CA1833.severity = warning + +# CA1834: Consider using 'StringBuilder.Append(char)' when applicable +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1834 +dotnet_diagnostic.CA1834.severity = warning + +# CA1835: Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync' +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1835 +dotnet_diagnostic.CA1835.severity = suggestion + +# CA1836: Prefer IsEmpty over Count +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1836 +dotnet_diagnostic.CA1836.severity = warning + +# CA1837: Use 'Environment.ProcessId' +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1837 +dotnet_diagnostic.CA1837.severity = warning + +# CA1838: Avoid 'StringBuilder' parameters for P/Invokes +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1838 +dotnet_diagnostic.CA1838.severity = silent + +# CA1839: Use 'Environment.ProcessPath' +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1839 +dotnet_diagnostic.CA1839.severity = warning + +# CA1840: Use 'Environment.CurrentManagedThreadId' +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1840 +dotnet_diagnostic.CA1840.severity = warning + +# CA1841: Prefer Dictionary.Contains methods +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1841 +dotnet_diagnostic.CA1841.severity = warning + +# CA1842: Do not use 'WhenAll' with a single task +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1842 +dotnet_diagnostic.CA1842.severity = warning + +# CA1843: Do not use 'WaitAll' with a single task +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1843 +dotnet_diagnostic.CA1843.severity = warning + +# CA1844: Provide memory-based overrides of async methods when subclassing 'Stream' +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1844 +dotnet_diagnostic.CA1844.severity = warning + +# CA1845: Use span-based 'string.Concat' +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1845 +dotnet_diagnostic.CA1845.severity = warning + +# CA1846: Prefer 'AsSpan' over 'Substring' +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1846 +dotnet_diagnostic.CA1846.severity = warning + +# CA1847: Use char literal for a single character lookup +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1847 +dotnet_diagnostic.CA1847.severity = warning + +# CA2000: Dispose objects before losing scope +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2000 +dotnet_diagnostic.CA2000.severity = none + +# CA2002: Do not lock on objects with weak identity +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2002 +dotnet_diagnostic.CA2002.severity = none + +# CA2007: Consider calling ConfigureAwait on the awaited task +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2007 +dotnet_diagnostic.CA2007.severity = none + +# CA2008: Do not create tasks without passing a TaskScheduler +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2008 +dotnet_diagnostic.CA2008.severity = none + +# CA2009: Do not call ToImmutableCollection on an ImmutableCollection value +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2009 +dotnet_diagnostic.CA2009.severity = warning + +# CA2011: Avoid infinite recursion +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2011 +dotnet_diagnostic.CA2011.severity = warning + +# CA2012: Use ValueTasks correctly +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2012 +dotnet_diagnostic.CA2012.severity = warning + +# CA2013: Do not use ReferenceEquals with value types +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2013 +dotnet_diagnostic.CA2013.severity = warning + +# CA2014: Do not use stackalloc in loops +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2014 +dotnet_diagnostic.CA2014.severity = warning + +# CA2015: Do not define finalizers for types derived from MemoryManager +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2015 +dotnet_diagnostic.CA2015.severity = warning + +# CA2016: Forward the 'CancellationToken' parameter to methods that take one +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2016 +dotnet_diagnostic.CA2016.severity = suggestion + +# CA2100: Review SQL queries for security vulnerabilities +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2100 +dotnet_diagnostic.CA2100.severity = none + +# CA2101: Specify marshaling for P/Invoke string arguments +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2101 +dotnet_diagnostic.CA2101.severity = suggestion + +# CA2109: Review visible event handlers +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2109 +dotnet_diagnostic.CA2109.severity = none + +# CA2119: Seal methods that satisfy private interfaces +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2119 +dotnet_diagnostic.CA2119.severity = none + +# CA2153: Do Not Catch Corrupted State Exceptions +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2153 +dotnet_diagnostic.CA2153.severity = none + +# CA2200: Rethrow to preserve stack details +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2200 +dotnet_diagnostic.CA2200.severity = warning + +# CA2201: Do not raise reserved exception types +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2201 +dotnet_diagnostic.CA2201.severity = silent + +# CA2207: Initialize value type static fields inline +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2207 +dotnet_diagnostic.CA2207.severity = warning + +# CA2208: Instantiate argument exceptions correctly +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2208 +dotnet_diagnostic.CA2208.severity = suggestion +dotnet_code_quality.CA2208.api_surface = all + +# CA2211: Non-constant fields should not be visible +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2211 +dotnet_diagnostic.CA2211.severity = warning + +# CA2213: Disposable fields should be disposed +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2213 +dotnet_diagnostic.CA2213.severity = none + +# CA2214: Do not call overridable methods in constructors +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2214 +dotnet_diagnostic.CA2214.severity = none + +# CA2215: Dispose methods should call base class dispose +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2215 +dotnet_diagnostic.CA2215.severity = silent + +# CA2216: Disposable types should declare finalizer +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2216 +dotnet_diagnostic.CA2216.severity = warning + +# CA2217: Do not mark enums with FlagsAttribute +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2217 +dotnet_diagnostic.CA2217.severity = none +dotnet_code_quality.CA2217.api_surface = public + +# CA2218: Override GetHashCode on overriding Equals +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2218 +dotnet_diagnostic.CA2218.severity = suggestion + +# CA2219: Do not raise exceptions in finally clauses +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2219 +dotnet_diagnostic.CA2219.severity = suggestion + +# CA2224: Override Equals on overloading operator equals +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2224 +dotnet_diagnostic.CA2224.severity = suggestion + +# CA2225: Operator overloads have named alternates +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2225 +dotnet_diagnostic.CA2225.severity = none +dotnet_code_quality.CA2225.api_surface = public + +# CA2226: Operators should have symmetrical overloads +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2226 +dotnet_diagnostic.CA2226.severity = none +dotnet_code_quality.CA2226.api_surface = public + +# CA2227: Collection properties should be read only +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2227 +dotnet_diagnostic.CA2227.severity = none + +# CA2229: Implement serialization constructors +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2229 +dotnet_diagnostic.CA2229.severity = silent + +# CA2231: Overload operator equals on overriding value type Equals +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2231 +dotnet_diagnostic.CA2231.severity = suggestion +dotnet_code_quality.CA2231.api_surface = public + +# CA2234: Pass system uri objects instead of strings +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2234 +dotnet_diagnostic.CA2234.severity = none +dotnet_code_quality.CA2234.api_surface = public + +# CA2235: Mark all non-serializable fields +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2235 +dotnet_diagnostic.CA2235.severity = none + +# CA2237: Mark ISerializable types with serializable +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2237 +dotnet_diagnostic.CA2237.severity = none + +# CA2241: Provide correct arguments to formatting methods +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2241 +dotnet_diagnostic.CA2241.severity = suggestion + +# CA2242: Test for NaN correctly +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2242 +dotnet_diagnostic.CA2242.severity = suggestion + +# CA2243: Attribute string literals should parse correctly +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2243 +dotnet_diagnostic.CA2243.severity = warning + +# CA2244: Do not duplicate indexed element initializations +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2244 +dotnet_diagnostic.CA2244.severity = suggestion + +# CA2245: Do not assign a property to itself +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2245 +dotnet_diagnostic.CA2245.severity = suggestion + +# CA2246: Assigning symbol and its member in the same statement +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2246 +dotnet_diagnostic.CA2246.severity = suggestion + +# CA2247: Argument passed to TaskCompletionSource constructor should be TaskCreationOptions enum instead of TaskContinuationOptions enum +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2247 +dotnet_diagnostic.CA2247.severity = warning + +# CA2248: Provide correct 'enum' argument to 'Enum.HasFlag' +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2248 +dotnet_diagnostic.CA2248.severity = suggestion + +# CA2249: Consider using 'string.Contains' instead of 'string.IndexOf' +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2249 +dotnet_diagnostic.CA2249.severity = warning + +# CA2250: Use 'ThrowIfCancellationRequested' +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2250 +dotnet_diagnostic.CA2250.severity = warning + +# CA2251: Use 'string.Equals' +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2251 +dotnet_diagnostic.CA2251.severity = warning + +# CA2252: This API requires opting into preview features +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2252 +dotnet_diagnostic.CA2251.severity = none + +# CA2300: Do not use insecure deserializer BinaryFormatter +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2300 +dotnet_diagnostic.CA2300.severity = none + +# CA2301: Do not call BinaryFormatter.Deserialize without first setting BinaryFormatter.Binder +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2301 +dotnet_diagnostic.CA2301.severity = none + +# CA2302: Ensure BinaryFormatter.Binder is set before calling BinaryFormatter.Deserialize +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2302 +dotnet_diagnostic.CA2302.severity = none + +# CA2305: Do not use insecure deserializer LosFormatter +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2305 +dotnet_diagnostic.CA2305.severity = none + +# CA2310: Do not use insecure deserializer NetDataContractSerializer +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2310 +dotnet_diagnostic.CA2310.severity = none + +# CA2311: Do not deserialize without first setting NetDataContractSerializer.Binder +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2311 +dotnet_diagnostic.CA2311.severity = none + +# CA2312: Ensure NetDataContractSerializer.Binder is set before deserializing +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2312 +dotnet_diagnostic.CA2312.severity = none + +# CA2315: Do not use insecure deserializer ObjectStateFormatter +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2315 +dotnet_diagnostic.CA2315.severity = none + +# CA2321: Do not deserialize with JavaScriptSerializer using a SimpleTypeResolver +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2321 +dotnet_diagnostic.CA2321.severity = none + +# CA2322: Ensure JavaScriptSerializer is not initialized with SimpleTypeResolver before deserializing +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2322 +dotnet_diagnostic.CA2322.severity = none + +# CA2326: Do not use TypeNameHandling values other than None +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2326 +dotnet_diagnostic.CA2326.severity = none + +# CA2327: Do not use insecure JsonSerializerSettings +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2327 +dotnet_diagnostic.CA2327.severity = none + +# CA2328: Ensure that JsonSerializerSettings are secure +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2328 +dotnet_diagnostic.CA2328.severity = none + +# CA2329: Do not deserialize with JsonSerializer using an insecure configuration +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2329 +dotnet_diagnostic.CA2329.severity = none + +# CA2330: Ensure that JsonSerializer has a secure configuration when deserializing +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2330 +dotnet_diagnostic.CA2330.severity = none + +# CA2350: Do not use DataTable.ReadXml() with untrusted data +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2350 +dotnet_diagnostic.CA2350.severity = none + +# CA2351: Do not use DataSet.ReadXml() with untrusted data +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2351 +dotnet_diagnostic.CA2351.severity = none + +# CA2352: Unsafe DataSet or DataTable in serializable type can be vulnerable to remote code execution attacks +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2352 +dotnet_diagnostic.CA2352.severity = none + +# CA2353: Unsafe DataSet or DataTable in serializable type +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2353 +dotnet_diagnostic.CA2353.severity = none + +# CA2354: Unsafe DataSet or DataTable in deserialized object graph can be vulnerable to remote code execution attacks +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2354 +dotnet_diagnostic.CA2354.severity = none + +# CA2355: Unsafe DataSet or DataTable type found in deserializable object graph +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2355 +dotnet_diagnostic.CA2355.severity = none + +# CA2356: Unsafe DataSet or DataTable type in web deserializable object graph +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2356 +dotnet_diagnostic.CA2356.severity = none + +# CA2361: Ensure autogenerated class containing DataSet.ReadXml() is not used with untrusted data +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2361 +dotnet_diagnostic.CA2361.severity = none + +# CA2362: Unsafe DataSet or DataTable in autogenerated serializable type can be vulnerable to remote code execution attacks +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2362 +dotnet_diagnostic.CA2362.severity = none + +# CA3001: Review code for SQL injection vulnerabilities +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3001 +dotnet_diagnostic.CA3001.severity = none + +# CA3002: Review code for XSS vulnerabilities +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3002 +dotnet_diagnostic.CA3002.severity = none + +# CA3003: Review code for file path injection vulnerabilities +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3003 +dotnet_diagnostic.CA3003.severity = none + +# CA3004: Review code for information disclosure vulnerabilities +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3004 +dotnet_diagnostic.CA3004.severity = none + +# CA3005: Review code for LDAP injection vulnerabilities +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3005 +dotnet_diagnostic.CA3005.severity = none + +# CA3006: Review code for process command injection vulnerabilities +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3006 +dotnet_diagnostic.CA3006.severity = none + +# CA3007: Review code for open redirect vulnerabilities +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3007 +dotnet_diagnostic.CA3007.severity = none + +# CA3008: Review code for XPath injection vulnerabilities +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3008 +dotnet_diagnostic.CA3008.severity = none + +# CA3009: Review code for XML injection vulnerabilities +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3009 +dotnet_diagnostic.CA3009.severity = none + +# CA3010: Review code for XAML injection vulnerabilities +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3010 +dotnet_diagnostic.CA3010.severity = none + +# CA3011: Review code for DLL injection vulnerabilities +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3011 +dotnet_diagnostic.CA3011.severity = none + +# CA3012: Review code for regex injection vulnerabilities +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3012 +dotnet_diagnostic.CA3012.severity = none + +# CA3061: Do Not Add Schema By URL +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3061 +dotnet_diagnostic.CA3061.severity = silent + +# CA3075: Insecure DTD processing in XML +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3075 +dotnet_diagnostic.CA3075.severity = silent + +# CA3076: Insecure XSLT script processing. +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3076 +dotnet_diagnostic.CA3076.severity = silent + +# CA3077: Insecure Processing in API Design, XmlDocument and XmlTextReader +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3077 +dotnet_diagnostic.CA3077.severity = silent + +# CA3147: Mark Verb Handlers With Validate Antiforgery Token +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3147 +dotnet_diagnostic.CA3147.severity = silent + +# CA5350: Do Not Use Weak Cryptographic Algorithms +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5350 +dotnet_diagnostic.CA5350.severity = silent + +# CA5351: Do Not Use Broken Cryptographic Algorithms +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5351 +dotnet_diagnostic.CA5351.severity = silent + +# CA5358: Review cipher mode usage with cryptography experts +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5358 +dotnet_diagnostic.CA5358.severity = none + +# CA5359: Do Not Disable Certificate Validation +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5359 +dotnet_diagnostic.CA5359.severity = silent + +# CA5360: Do Not Call Dangerous Methods In Deserialization +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5360 +dotnet_diagnostic.CA5360.severity = silent + +# CA5361: Do Not Disable SChannel Use of Strong Crypto +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5361 +dotnet_diagnostic.CA5361.severity = none + +# CA5362: Potential reference cycle in deserialized object graph +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5362 +dotnet_diagnostic.CA5362.severity = none + +# CA5363: Do Not Disable Request Validation +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5363 +dotnet_diagnostic.CA5363.severity = silent + +# CA5364: Do Not Use Deprecated Security Protocols +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5364 +dotnet_diagnostic.CA5364.severity = silent + +# CA5365: Do Not Disable HTTP Header Checking +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5365 +dotnet_diagnostic.CA5365.severity = silent + +# CA5366: Use XmlReader For DataSet Read Xml +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5366 +dotnet_diagnostic.CA5366.severity = silent + +# CA5367: Do Not Serialize Types With Pointer Fields +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5367 +dotnet_diagnostic.CA5367.severity = none + +# CA5368: Set ViewStateUserKey For Classes Derived From Page +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5368 +dotnet_diagnostic.CA5368.severity = silent + +# CA5369: Use XmlReader For Deserialize +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5369 +dotnet_diagnostic.CA5369.severity = silent + +# CA5370: Use XmlReader For Validating Reader +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5370 +dotnet_diagnostic.CA5370.severity = silent + +# CA5371: Use XmlReader For Schema Read +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5371 +dotnet_diagnostic.CA5371.severity = silent + +# CA5372: Use XmlReader For XPathDocument +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5372 +dotnet_diagnostic.CA5372.severity = silent + +# CA5373: Do not use obsolete key derivation function +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5373 +dotnet_diagnostic.CA5373.severity = silent + +# CA5374: Do Not Use XslTransform +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5374 +dotnet_diagnostic.CA5374.severity = silent + +# CA5375: Do Not Use Account Shared Access Signature +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5375 +dotnet_diagnostic.CA5375.severity = none + +# CA5376: Use SharedAccessProtocol HttpsOnly +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5376 +dotnet_diagnostic.CA5376.severity = none + +# CA5377: Use Container Level Access Policy +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5377 +dotnet_diagnostic.CA5377.severity = none + +# CA5378: Do not disable ServicePointManagerSecurityProtocols +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5378 +dotnet_diagnostic.CA5378.severity = none + +# CA5379: Do Not Use Weak Key Derivation Function Algorithm +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5379 +dotnet_diagnostic.CA5379.severity = silent + +# CA5380: Do Not Add Certificates To Root Store +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5380 +dotnet_diagnostic.CA5380.severity = none + +# CA5381: Ensure Certificates Are Not Added To Root Store +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5381 +dotnet_diagnostic.CA5381.severity = none + +# CA5382: Use Secure Cookies In ASP.Net Core +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5382 +dotnet_diagnostic.CA5382.severity = none + +# CA5383: Ensure Use Secure Cookies In ASP.Net Core +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5383 +dotnet_diagnostic.CA5383.severity = none + +# CA5384: Do Not Use Digital Signature Algorithm (DSA) +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5384 +dotnet_diagnostic.CA5384.severity = silent + +# CA5385: Use Rivest–Shamir–Adleman (RSA) Algorithm With Sufficient Key Size +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5385 +dotnet_diagnostic.CA5385.severity = silent + +# CA5386: Avoid hardcoding SecurityProtocolType value +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5386 +dotnet_diagnostic.CA5386.severity = none + +# CA5387: Do Not Use Weak Key Derivation Function With Insufficient Iteration Count +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5387 +dotnet_diagnostic.CA5387.severity = none + +# CA5388: Ensure Sufficient Iteration Count When Using Weak Key Derivation Function +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5388 +dotnet_diagnostic.CA5388.severity = none + +# CA5389: Do Not Add Archive Item's Path To The Target File System Path +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5389 +dotnet_diagnostic.CA5389.severity = none + +# CA5390: Do not hard-code encryption key +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5390 +dotnet_diagnostic.CA5390.severity = none + +# CA5391: Use antiforgery tokens in ASP.NET Core MVC controllers +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5391 +dotnet_diagnostic.CA5391.severity = none + +# CA5392: Use DefaultDllImportSearchPaths attribute for P/Invokes +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5392 +dotnet_diagnostic.CA5392.severity = none + +# CA5393: Do not use unsafe DllImportSearchPath value +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5393 +dotnet_diagnostic.CA5393.severity = none + +# CA5394: Do not use insecure randomness +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5394 +dotnet_diagnostic.CA5394.severity = none + +# CA5395: Miss HttpVerb attribute for action methods +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5395 +dotnet_diagnostic.CA5395.severity = none + +# CA5396: Set HttpOnly to true for HttpCookie +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5396 +dotnet_diagnostic.CA5396.severity = none + +# CA5397: Do not use deprecated SslProtocols values +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5397 +dotnet_diagnostic.CA5397.severity = silent + +# CA5398: Avoid hardcoded SslProtocols values +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5398 +dotnet_diagnostic.CA5398.severity = none + +# CA5399: HttpClients should enable certificate revocation list checks +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5399 +dotnet_diagnostic.CA5399.severity = none + +# CA5400: Ensure HttpClient certificate revocation list check is not disabled +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5400 +dotnet_diagnostic.CA5400.severity = none + +# CA5401: Do not use CreateEncryptor with non-default IV +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5401 +dotnet_diagnostic.CA5401.severity = none + +# CA5402: Use CreateEncryptor with the default IV +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5402 +dotnet_diagnostic.CA5402.severity = none + +# CA5403: Do not hard-code certificate +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5403 +dotnet_diagnostic.CA5403.severity = none + +# IL3000: Avoid using accessing Assembly file path when publishing as a single-file +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/il3000 +dotnet_diagnostic.IL3000.severity = warning + +# IL3001: Avoid using accessing Assembly file path when publishing as a single-file +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/il3001 +dotnet_diagnostic.IL3001.severity = warning + +# IL3002: Using member with RequiresAssemblyFilesAttribute can break functionality when embedded in a single-file app +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/il3002 +dotnet_diagnostic.IL3002.severity = warning + +# DOC100: PlaceTextInParagraphs +# https://github.com/DotNetAnalyzers/DocumentationAnalyzers/blob/master/docs/DOC100.md +dotnet_diagnostic.DOC100.severity = none + +# DOC101: UseChildBlocksConsistently +# https://github.com/DotNetAnalyzers/DocumentationAnalyzers/blob/master/docs/DOC101.md +dotnet_diagnostic.DOC101.severity = none + +# DOC102: UseChildBlocksConsistentlyAcrossElementsOfTheSameKind +# https://github.com/DotNetAnalyzers/DocumentationAnalyzers/blob/master/docs/DOC102.md +dotnet_diagnostic.DOC102.severity = none + +# DOC103: UseUnicodeCharacters +# https://github.com/DotNetAnalyzers/DocumentationAnalyzers/blob/master/docs/DOC103.md +dotnet_diagnostic.DOC103.severity = none + +# DOC104: UseSeeLangword +# https://github.com/DotNetAnalyzers/DocumentationAnalyzers/blob/master/docs/DOC104.md +dotnet_diagnostic.DOC104.severity = suggestion + +# DOC105: UseParamref +# https://github.com/DotNetAnalyzers/DocumentationAnalyzers/blob/master/docs/DOC105.md +dotnet_diagnostic.DOC105.severity = none + +# DOC106: UseTypeparamref +# https://github.com/DotNetAnalyzers/DocumentationAnalyzers/blob/master/docs/DOC106.md +dotnet_diagnostic.DOC106.severity = none + +# DOC107: UseSeeCref +# https://github.com/DotNetAnalyzers/DocumentationAnalyzers/blob/master/docs/DOC107.md +dotnet_diagnostic.DOC107.severity = none + +# DOC108: AvoidEmptyParagraphs +# https://github.com/DotNetAnalyzers/DocumentationAnalyzers/blob/master/docs/DOC108.md +dotnet_diagnostic.DOC108.severity = none + +# DOC200: UseXmlDocumentationSyntax +# https://github.com/DotNetAnalyzers/DocumentationAnalyzers/blob/master/docs/DOC200.md +dotnet_diagnostic.DOC200.severity = none + +# DOC201: ItemShouldHaveDescription +# https://github.com/DotNetAnalyzers/DocumentationAnalyzers/blob/master/docs/DOC201.md +dotnet_diagnostic.DOC201.severity = none + +# DOC202: UseSectionElementsCorrectly +# https://github.com/DotNetAnalyzers/DocumentationAnalyzers/blob/master/docs/DOC202.md +dotnet_diagnostic.DOC202.severity = none + +# DOC203: UseBlockElementsCorrectly +# https://github.com/DotNetAnalyzers/DocumentationAnalyzers/blob/master/docs/DOC203.md +dotnet_diagnostic.DOC203.severity = none + +# DOC204: UseInlineElementsCorrectly +# https://github.com/DotNetAnalyzers/DocumentationAnalyzers/blob/master/docs/DOC204.md +dotnet_diagnostic.DOC204.severity = none + +# DOC207: UseSeeLangwordCorrectly +# https://github.com/DotNetAnalyzers/DocumentationAnalyzers/blob/master/docs/DOC207.md +dotnet_diagnostic.DOC207.severity = none + +# DOC209: UseSeeHrefCorrectly +# https://github.com/DotNetAnalyzers/DocumentationAnalyzers/blob/master/docs/DOC209.md +dotnet_diagnostic.DOC209.severity = none + +# IDE0001: SimplifyNames +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0001 +dotnet_diagnostic.IDE0001.severity = silent + +# IDE0002: SimplifyMemberAccess +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0002 +dotnet_diagnostic.IDE0002.severity = silent + +# IDE0003: RemoveQualification +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0003 +dotnet_diagnostic.IDE0003.severity = silent + +# IDE0004: RemoveUnnecessaryCast +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0004 +dotnet_diagnostic.IDE0004.severity = silent + +# IDE0005: RemoveUnnecessaryImports +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005 +dotnet_diagnostic.IDE0005.severity = silent + +# IDE0006: IntellisenseBuildFailed +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0006 +dotnet_diagnostic.IDE0006.severity = silent + +# IDE0007: UseImplicitType +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0007 +dotnet_diagnostic.IDE0007.severity = silent + +# IDE0008: UseExplicitType +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0008 +dotnet_diagnostic.IDE0008.severity = silent + +# IDE0009: AddQualification +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0009 +dotnet_diagnostic.IDE0009.severity = silent + +# IDE0010: PopulateSwitchStatement +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0010 +dotnet_diagnostic.IDE0010.severity = silent + +# IDE0011: AddBraces +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0011 +dotnet_diagnostic.IDE0011.severity = silent + +# IDE0016: UseThrowExpression +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0016 +dotnet_diagnostic.IDE0016.severity = silent + +# IDE0017: UseObjectInitializer +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0017 +dotnet_diagnostic.IDE0017.severity = silent + +# IDE0018: InlineDeclaration +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0018 +dotnet_diagnostic.IDE0018.severity = silent + +# IDE0019: InlineAsTypeCheck +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0019 +dotnet_diagnostic.IDE0019.severity = silent + +# IDE0020: InlineIsTypeCheck +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0020 +dotnet_diagnostic.IDE0020.severity = silent + +# IDE0021: UseExpressionBodyForConstructors +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0021 +dotnet_diagnostic.IDE0021.severity = silent + +# IDE0022: UseExpressionBodyForMethods +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0022 +dotnet_diagnostic.IDE0022.severity = silent + +# IDE0023: UseExpressionBodyForConversionOperators +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0023 +dotnet_diagnostic.IDE0023.severity = silent + +# IDE0024: UseExpressionBodyForOperators +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0024 +dotnet_diagnostic.IDE0024.severity = silent + +# IDE0025: UseExpressionBodyForProperties +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0025 +dotnet_diagnostic.IDE0025.severity = silent + +# IDE0026: UseExpressionBodyForIndexers +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0026 +dotnet_diagnostic.IDE0026.severity = silent + +# IDE0027: UseExpressionBodyForAccessors +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0027 +dotnet_diagnostic.IDE0027.severity = silent + +# IDE0028: UseCollectionInitializer +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0028 +dotnet_diagnostic.IDE0028.severity = silent + +# IDE0029: UseCoalesceExpression +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0029 +dotnet_diagnostic.IDE0029.severity = warning + +# IDE0030: UseCoalesceExpressionForNullable +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0030 +dotnet_diagnostic.IDE0030.severity = warning + +# IDE0031: UseNullPropagation +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0031 +dotnet_diagnostic.IDE0031.severity = warning + +# IDE0032: UseAutoProperty +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0032 +dotnet_diagnostic.IDE0032.severity = silent + +# IDE0033: UseExplicitTupleName +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0033 +dotnet_diagnostic.IDE0033.severity = silent + +# IDE0034: UseDefaultLiteral +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0034 +dotnet_diagnostic.IDE0034.severity = silent + +# IDE0035: RemoveUnreachableCode +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0035 +dotnet_diagnostic.IDE0035.severity = silent + +# IDE0036: OrderModifiers +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0036 +dotnet_diagnostic.IDE0036.severity = warning + +# IDE0037: UseInferredMemberName +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0037 +dotnet_diagnostic.IDE0037.severity = silent + +# IDE0038: InlineIsTypeWithoutNameCheck +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0038 +dotnet_diagnostic.IDE0038.severity = silent + +# IDE0039: UseLocalFunction +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0039 +dotnet_diagnostic.IDE0039.severity = silent + +# IDE0040: AddAccessibilityModifiers +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0040 +dotnet_diagnostic.IDE0040.severity = warning + +# IDE0041: UseIsNullCheck +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0041 +dotnet_diagnostic.IDE0041.severity = warning + +# IDE0042: UseDeconstruction +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0042 +dotnet_diagnostic.IDE0042.severity = silent + +# IDE0043: ValidateFormatString +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0043 +dotnet_diagnostic.IDE0043.severity = silent + +# IDE0044: MakeFieldReadonly +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0044 +dotnet_diagnostic.IDE0044.severity = warning + +# IDE0045: UseConditionalExpressionForAssignment +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0045 +dotnet_diagnostic.IDE0045.severity = silent + +# IDE0046: UseConditionalExpressionForReturn +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0046 +dotnet_diagnostic.IDE0046.severity = silent + +# IDE0047: RemoveUnnecessaryParentheses +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0047 +dotnet_diagnostic.IDE0047.severity = silent + +# IDE0048: AddRequiredParentheses +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0048 +dotnet_diagnostic.IDE0048.severity = suggestion + +# IDE0049: PreferBuiltInOrFrameworkType +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0049 +dotnet_diagnostic.IDE0049.severity = warning + +# IDE0050: ConvertAnonymousTypeToTuple +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0050 +dotnet_diagnostic.IDE0050.severity = silent + +# IDE0051: RemoveUnusedMembers +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0051 +dotnet_diagnostic.IDE0051.severity = silent + +# IDE0052: RemoveUnreadMembers +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0052 +dotnet_diagnostic.IDE0052.severity = silent + +# IDE0053: UseExpressionBodyForLambdaExpressions +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0053 +dotnet_diagnostic.IDE0053.severity = silent + +# IDE0054: UseCompoundAssignment +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0054 +dotnet_diagnostic.IDE0054.severity = warning + +# IDE0055: Formatting +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055 +dotnet_diagnostic.IDE0055.severity = silent + +# IDE0056: UseIndexOperator +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0056 +dotnet_diagnostic.IDE0056.severity = silent + +# IDE0057: UseRangeOperator +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0057 +dotnet_diagnostic.IDE0057.severity = silent + +# IDE0058: ExpressionValueIsUnused +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0058 +dotnet_diagnostic.IDE0058.severity = silent + +# IDE0059: ValueAssignedIsUnused +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0059 +dotnet_diagnostic.IDE0059.severity = silent + +# IDE0060: UnusedParameter +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0060 +dotnet_diagnostic.IDE0060.severity = silent + +# IDE0061: UseExpressionBodyForLocalFunctions +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0061 +dotnet_diagnostic.IDE0061.severity = silent + +# IDE0062: MakeLocalFunctionStatic +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0062 +dotnet_diagnostic.IDE0062.severity = warning + +# IDE0063: UseSimpleUsingStatement +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0063 +dotnet_diagnostic.IDE0063.severity = silent + +# IDE0064: MakeStructFieldsWritable +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0064 +dotnet_diagnostic.IDE0064.severity = warning + +# IDE0065: MoveMisplacedUsingDirectives +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0065 +dotnet_diagnostic.IDE0065.severity = silent + +# IDE0066: ConvertSwitchStatementToExpression +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0066 +dotnet_diagnostic.IDE0066.severity = silent + +# IDE0070: UseSystemHashCode +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0070 +dotnet_diagnostic.IDE0070.severity = warning + +# IDE0071: SimplifyInterpolation +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0071 +dotnet_diagnostic.IDE0071.severity = silent + +# IDE0072: PopulateSwitchExpression +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0072 +dotnet_diagnostic.IDE0072.severity = silent + +# IDE0073: FileHeaderMismatch +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0073 +dotnet_diagnostic.IDE0073.severity = suggestion + +# IDE0074: UseCoalesceCompoundAssignment +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0074 +dotnet_diagnostic.IDE0074.severity = warning + +# IDE0075: SimplifyConditionalExpression +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0075 +dotnet_diagnostic.IDE0075.severity = warning + +# IDE0076: InvalidSuppressMessageAttribute +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0076 +dotnet_diagnostic.IDE0076.severity = warning + +# IDE0077: LegacyFormatSuppressMessageAttribute +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0077 +dotnet_diagnostic.IDE0077.severity = warning + +# IDE0078: UsePatternCombinators +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0078 +dotnet_diagnostic.IDE0078.severity = silent + +# IDE0079: RemoveUnnecessarySuppression +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0079 +dotnet_diagnostic.IDE0079.severity = silent + +# IDE0080: RemoveConfusingSuppressionForIsExpression +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0080 +dotnet_diagnostic.IDE0080.severity = silent + +# IDE0081: RemoveUnnecessaryByVal +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0081 +dotnet_diagnostic.IDE0081.severity = silent + +# IDE0082: ConvertTypeOfToNameOf +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0082 +dotnet_diagnostic.IDE0082.severity = warning + +# IDE0083: UseNotPattern +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0083 +dotnet_diagnostic.IDE0083.severity = silent + +# IDE0084: UseIsNotExpression +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0084 +dotnet_diagnostic.IDE0084.severity = silent + +# IDE0090: UseNew +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0090 +dotnet_diagnostic.IDE0090.severity = suggestion + +# IDE0100: RemoveRedundantEquality +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0100 +dotnet_diagnostic.IDE0100.severity = warning + +# IDE0110: RemoveUnnecessaryDiscard +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0110 +dotnet_diagnostic.IDE0110.severity = suggestion + +# IDE0120: SimplifyLINQExpression +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0120 +dotnet_diagnostic.IDE0120.severity = warning + +# IDE0130: NamespaceDoesNotMatchFolderStructure +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0130 +dotnet_diagnostic.IDE0130.severity = silent + +# IDE1001: AnalyzerChanged +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide1001 +dotnet_diagnostic.IDE1001.severity = silent + +# IDE1002: AnalyzerDependencyConflict +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide1002 +dotnet_diagnostic.IDE1002.severity = silent + +# IDE1003: MissingAnalyzerReference +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide1003 +dotnet_diagnostic.IDE1003.severity = silent + +# IDE1004: ErrorReadingRuleset +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide1004 +dotnet_diagnostic.IDE1004.severity = silent + +# IDE1005: InvokeDelegateWithConditionalAccess +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide1005 +dotnet_diagnostic.IDE1005.severity = warning + +# IDE1006: NamingRule +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide1006 +dotnet_diagnostic.IDE1006.severity = silent + +# IDE1007: UnboundIdentifier +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide1007 +dotnet_diagnostic.IDE1007.severity = silent + +# IDE1008: UnboundConstructor +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide1008 +dotnet_diagnostic.IDE1008.severity = silent + +# IDE2000: MultipleBlankLines +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide2000 +dotnet_diagnostic.IDE2000.severity = warning + +# IDE2001: EmbeddedStatementsMustBeOnTheirOwnLine +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide2001 +dotnet_diagnostic.IDE2001.severity = warning + +# IDE2002: ConsecutiveBracesMustNotHaveBlankLinesBetweenThem +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide2002 +dotnet_diagnostic.IDE2002.severity = warning + +# IDE2003: ConsecutiveStatementPlacement +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide2003 +dotnet_diagnostic.IDE2003.severity = warning + +# IDE2004: BlankLineNotAllowedAfterConstructorInitializerColon +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide2004 +dotnet_diagnostic.IDE2004.severity = warning + +# SA0001: XML comment analysis disabled +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA0001.md +dotnet_diagnostic.SA0001.severity = none + +# SA0002: Invalid settings file +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA0002.md +dotnet_diagnostic.SA0002.severity = none + +# SA1000: Keywords should be spaced correctly +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1000.md +dotnet_diagnostic.SA1000.severity = warning + +# SA1001: Commas should be spaced correctly +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1001.md +dotnet_diagnostic.SA1001.severity = warning + +# SA1002: Semicolons should be spaced correctly +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1002.md +dotnet_diagnostic.SA1002.severity = warning + +# SA1003: Symbols should be spaced correctly +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1003.md +dotnet_diagnostic.SA1003.severity = warning + +# SA1004: Documentation lines should begin with single space +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1004.md +dotnet_diagnostic.SA1004.severity = none + +# SA1005: Single line comments should begin with single space +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1005.md +dotnet_diagnostic.SA1005.severity = none + +# SA1006: Preprocessor keywords should not be preceded by space +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1006.md +dotnet_diagnostic.SA1006.severity = warning + +# SA1007: Operator keyword should be followed by space +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1007.md +dotnet_diagnostic.SA1007.severity = warning + +# SA1008: Opening parenthesis should be spaced correctly +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1008.md +dotnet_diagnostic.SA1008.severity = warning + +# SA1009: Closing parenthesis should be spaced correctly +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1009.md +dotnet_diagnostic.SA1009.severity = none + +# SA1010: Opening square brackets should be spaced correctly +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1010.md +dotnet_diagnostic.SA1010.severity = none + +# SA1011: Closing square brackets should be spaced correctly +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1011.md +dotnet_diagnostic.SA1011.severity = none + +# SA1012: Opening braces should be spaced correctly +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1012.md +dotnet_diagnostic.SA1012.severity = none + +# SA1013: Closing braces should be spaced correctly +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1013.md +dotnet_diagnostic.SA1013.severity = none + +# SA1014: Opening generic brackets should be spaced correctly +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1014.md +dotnet_diagnostic.SA1014.severity = none + +# SA1015: Closing generic brackets should be spaced correctly +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1015.md +dotnet_diagnostic.SA1015.severity = none + +# SA1016: Opening attribute brackets should be spaced correctly +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1016.md +dotnet_diagnostic.SA1016.severity = none + +# SA1017: Closing attribute brackets should be spaced correctly +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1017.md +dotnet_diagnostic.SA1017.severity = none + +# SA1018: Nullable type symbols should be spaced correctly +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1018.md +dotnet_diagnostic.SA1018.severity = none + +# SA1019: Member access symbols should be spaced correctly +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1019.md +dotnet_diagnostic.SA1019.severity = none + +# SA1020: Increment decrement symbols should be spaced correctly +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1020.md +dotnet_diagnostic.SA1020.severity = none + +# SA1021: Negative signs should be spaced correctly +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1021.md +dotnet_diagnostic.SA1021.severity = none + +# SA1022: Positive signs should be spaced correctly +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1022.md +dotnet_diagnostic.SA1022.severity = none + +# SA1023: Dereference and access of symbols should be spaced correctly +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1023.md +dotnet_diagnostic.SA1023.severity = none + +# SA1024: Colons Should Be Spaced Correctly +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1024.md +dotnet_diagnostic.SA1024.severity = none + +# SA1025: Code should not contain multiple whitespace in a row +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1025.md +dotnet_diagnostic.SA1025.severity = none + +# SA1026: Code should not contain space after new or stackalloc keyword in implicitly typed array allocation +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1026.md +dotnet_diagnostic.SA1026.severity = none + +# SA1027: Use tabs correctly +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1027.md +dotnet_diagnostic.SA1027.severity = none + +# SA1028: Code should not contain trailing whitespace +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1028.md +dotnet_diagnostic.SA1028.severity = none + +# SA1100: Do not prefix calls with base unless local implementation exists +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1100.md +dotnet_diagnostic.SA1100.severity = none + +# SA1101: Prefix local calls with this +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1101.md +dotnet_diagnostic.SA1101.severity = none + +# SA1102: Query clause should follow previous clause +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1102.md +dotnet_diagnostic.SA1102.severity = none + +# SA1103: Query clauses should be on separate lines or all on one line +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1103.md +dotnet_diagnostic.SA1103.severity = none + +# SA1104: Query clause should begin on new line when previous clause spans multiple lines +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1104.md +dotnet_diagnostic.SA1104.severity = none + +# SA1105: Query clauses spanning multiple lines should begin on own line +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1105.md +dotnet_diagnostic.SA1105.severity = none + +# SA1106: Code should not contain empty statements +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1106.md +dotnet_diagnostic.SA1106.severity = warning + +# SA1107: Code should not contain multiple statements on one line +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1107.md +dotnet_diagnostic.SA1107.severity = none + +# SA1108: Block statements should not contain embedded comments +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1108.md +dotnet_diagnostic.SA1108.severity = none + +# SA1110: Opening parenthesis or bracket should be on declaration line +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1110.md +dotnet_diagnostic.SA1110.severity = none + +# SA1111: Closing parenthesis should be on line of last parameter +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1111.md +dotnet_diagnostic.SA1111.severity = none + +# SA1112: Closing parenthesis should be on line of opening parenthesis +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1112.md +dotnet_diagnostic.SA1112.severity = none + +# SA1113: Comma should be on the same line as previous parameter +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1113.md +dotnet_diagnostic.SA1113.severity = none + +# SA1114: Parameter list should follow declaration +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1114.md +dotnet_diagnostic.SA1114.severity = none + +# SA1115: Parameter should follow comma +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1115.md +dotnet_diagnostic.SA1115.severity = none + +# SA1116: Split parameters should start on line after declaration +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1116.md +dotnet_diagnostic.SA1116.severity = none + +# SA1117: Parameters should be on same line or separate lines +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1117.md +dotnet_diagnostic.SA1117.severity = none + +# SA1118: Parameter should not span multiple lines +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1118.md +dotnet_diagnostic.SA1118.severity = none + +# SA1119: Statement should not use unnecessary parenthesis +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1119.md +dotnet_diagnostic.SA1119.severity = none + +# SA1120: Comments should contain text +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1120.md +dotnet_diagnostic.SA1120.severity = none + +# SA1121: Use built-in type alias +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1121.md +dotnet_diagnostic.SA1121.severity = none + +# SA1122: Use string.Empty for empty strings +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1122.md +dotnet_diagnostic.SA1122.severity = warning + +# SA1123: Do not place regions within elements +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1123.md +dotnet_diagnostic.SA1123.severity = none + +# SA1124: Do not use regions +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1124.md +dotnet_diagnostic.SA1124.severity = none + +# SA1125: Use shorthand for nullable types +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1125.md +dotnet_diagnostic.SA1125.severity = none + +# SA1127: Generic type constraints should be on their own line +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1127.md +dotnet_diagnostic.SA1127.severity = none + +# SA1128: Put constructor initializers on their own line +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1128.md +dotnet_diagnostic.SA1128.severity = none + +# SA1129: Do not use default value type constructor +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1129.md +dotnet_diagnostic.SA1129.severity = none + +# SA1130: Use lambda syntax +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1130.md +dotnet_diagnostic.SA1130.severity = none + +# SA1131: Use readable conditions +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1131.md +dotnet_diagnostic.SA1131.severity = warning + +# SA1132: Do not combine fields +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1132.md +dotnet_diagnostic.SA1132.severity = none + +# SA1133: Do not combine attributes +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1133.md +dotnet_diagnostic.SA1133.severity = none + +# SA1134: Attributes should not share line +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1134.md +dotnet_diagnostic.SA1134.severity = none + +# SA1135: Using directives should be qualified +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1135.md +dotnet_diagnostic.SA1135.severity = none + +# SA1136: Enum values should be on separate lines +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1136.md +dotnet_diagnostic.SA1136.severity = none + +# SA1137: Elements should have the same indentation +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1137.md +dotnet_diagnostic.SA1137.severity = none + +# SA1139: Use literal suffix notation instead of casting +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1139.md +dotnet_diagnostic.SA1139.severity = none + +# SA1141: Use tuple syntax +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1141.md +dotnet_diagnostic.SA1141.severity = none + +# SA1142: Refer to tuple fields by name +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1142.md +dotnet_diagnostic.SA1142.severity = none + +# SA1200: Using directives should be placed correctly +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md +dotnet_diagnostic.SA1200.severity = none + +# SA1201: Elements should appear in the correct order +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1201.md +dotnet_diagnostic.SA1201.severity = none + +# SA1202: Elements should be ordered by access +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1202.md +dotnet_diagnostic.SA1202.severity = none + +# SA1203: Constants should appear before fields +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1203.md +dotnet_diagnostic.SA1203.severity = none + +# SA1204: Static elements should appear before instance elements +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1204.md +dotnet_diagnostic.SA1204.severity = none + +# SA1205: Partial elements should declare access +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1205.md +dotnet_diagnostic.SA1205.severity = warning + +# SA1206: Declaration keywords should follow order +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1206.md +dotnet_diagnostic.SA1206.severity = none + +# SA1207: Protected should come before internal +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1207.md +dotnet_diagnostic.SA1207.severity = none + +# SA1208: System using directives should be placed before other using directives +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1208.md +dotnet_diagnostic.SA1208.severity = none + +# SA1209: Using alias directives should be placed after other using directives +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1209.md +dotnet_diagnostic.SA1209.severity = none + +# SA1210: Using directives should be ordered alphabetically by namespace +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1210.md +dotnet_diagnostic.SA1210.severity = none + +# SA1211: Using alias directives should be ordered alphabetically by alias name +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1211.md +dotnet_diagnostic.SA1211.severity = none + +# SA1212: Property accessors should follow order +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1212.md +dotnet_diagnostic.SA1212.severity = warning + +# SA1213: Event accessors should follow order +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1213.md +dotnet_diagnostic.SA1213.severity = warning + +# SA1214: Readonly fields should appear before non-readonly fields +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1214.md +dotnet_diagnostic.SA1214.severity = none + +# SA1216: Using static directives should be placed at the correct location +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1216.md +dotnet_diagnostic.SA1216.severity = warning + +# SA1217: Using static directives should be ordered alphabetically +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1217.md +dotnet_diagnostic.SA1217.severity = warning + +# SA1300: Element should begin with upper-case letter +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md +dotnet_diagnostic.SA1300.severity = none + +# SA1302: Interface names should begin with I +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1302.md +dotnet_diagnostic.SA1302.severity = none + +# SA1303: Const field names should begin with upper-case letter +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1303.md +dotnet_diagnostic.SA1303.severity = none + +# SA1304: Non-private readonly fields should begin with upper-case letter +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1304.md +dotnet_diagnostic.SA1304.severity = none + +# SA1305: Field names should not use Hungarian notation +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1305.md +dotnet_diagnostic.SA1305.severity = none + +# SA1306: Field names should begin with lower-case letter +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1306.md +dotnet_diagnostic.SA1306.severity = none + +# SA1307: Accessible fields should begin with upper-case letter +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1307.md +dotnet_diagnostic.SA1307.severity = none + +# SA1308: Variable names should not be prefixed +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1308.md +dotnet_diagnostic.SA1308.severity = none + +# SA1309: Field names should not begin with underscore +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1309.md +dotnet_diagnostic.SA1309.severity = none + +# SA1310: Field names should not contain underscore +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1310.md +dotnet_diagnostic.SA1310.severity = none + +# SA1311: Static readonly fields should begin with upper-case letter +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1311.md +dotnet_diagnostic.SA1311.severity = none + +# SA1312: Variable names should begin with lower-case letter +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1312.md +dotnet_diagnostic.SA1312.severity = none + +# SA1313: Parameter names should begin with lower-case letter +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1313.md +dotnet_diagnostic.SA1313.severity = none + +# SA1314: Type parameter names should begin with T +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1314.md +dotnet_diagnostic.SA1314.severity = warning + +# SA1316: Tuple element names should use correct casing +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1316.md +dotnet_diagnostic.SA1316.severity = none + +# SA1400: Access modifier should be declared +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1400.md +dotnet_diagnostic.SA1400.severity = none + +# SA1401: Fields should be private +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1401.md +dotnet_diagnostic.SA1401.severity = none + +# SA1402: File may only contain a single type +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1402.md +dotnet_diagnostic.SA1402.severity = none + +# SA1403: File may only contain a single namespace +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1403.md +dotnet_diagnostic.SA1403.severity = none + +# SA1404: Code analysis suppression should have justification +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1404.md +dotnet_diagnostic.SA1404.severity = none + +# SA1405: Debug.Assert should provide message text +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1405.md +dotnet_diagnostic.SA1405.severity = none + +# SA1406: Debug.Fail should provide message text +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1406.md +dotnet_diagnostic.SA1406.severity = none + +# SA1407: Arithmetic expressions should declare precedence +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1407.md +dotnet_diagnostic.SA1407.severity = none + +# SA1408: Conditional expressions should declare precedence +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1408.md +dotnet_diagnostic.SA1408.severity = none + +# SA1410: Remove delegate parenthesis when possible +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1410.md +dotnet_diagnostic.SA1410.severity = none + +# SA1411: Attribute constructor should not use unnecessary parenthesis +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1411.md +dotnet_diagnostic.SA1411.severity = none + +# SA1412: Store files as UTF-8 with byte order mark +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1412.md +dotnet_diagnostic.SA1412.severity = none + +# SA1413: Use trailing comma in multi-line initializers +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1413.md +dotnet_diagnostic.SA1413.severity = none + +# SA1414: Tuple types in signatures should have element names +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1414.md +dotnet_diagnostic.SA1414.severity = none + +# SA1500: Braces for multi-line statements should not share line +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1500.md +dotnet_diagnostic.SA1500.severity = none + +# SA1501: Statement should not be on a single line +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1501.md +dotnet_diagnostic.SA1501.severity = none + +# SA1502: Element should not be on a single line +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1502.md +dotnet_diagnostic.SA1502.severity = none + +# SA1503: Braces should not be omitted +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1503.md +dotnet_diagnostic.SA1503.severity = none + +# SA1504: All accessors should be single-line or multi-line +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1504.md +dotnet_diagnostic.SA1504.severity = warning + +# SA1505: Opening braces should not be followed by blank line +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1505.md +dotnet_diagnostic.SA1505.severity = none + +# SA1506: Element documentation headers should not be followed by blank line +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1506.md +dotnet_diagnostic.SA1506.severity = none + +# SA1507: Code should not contain multiple blank lines in a row +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1507.md +dotnet_diagnostic.SA1507.severity = warning + +# SA1508: Closing braces should not be preceded by blank line +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1508.md +dotnet_diagnostic.SA1508.severity = none + +# SA1509: Opening braces should not be preceded by blank line +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1509.md +dotnet_diagnostic.SA1509.severity = none + +# SA1510: Chained statement blocks should not be preceded by blank line +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1510.md +dotnet_diagnostic.SA1510.severity = none + +# SA1511: While-do footer should not be preceded by blank line +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1511.md +dotnet_diagnostic.SA1511.severity = none + +# SA1512: Single-line comments should not be followed by blank line +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1512.md +dotnet_diagnostic.SA1512.severity = none + +# SA1513: Closing brace should be followed by blank line +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1513.md +dotnet_diagnostic.SA1513.severity = none + +# SA1514: Element documentation header should be preceded by blank line +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1514.md +dotnet_diagnostic.SA1514.severity = none + +# SA1515: Single-line comment should be preceded by blank line +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1515.md +dotnet_diagnostic.SA1515.severity = none + +# SA1516: Elements should be separated by blank line +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1516.md +dotnet_diagnostic.SA1516.severity = warning + +# SA1517: Code should not contain blank lines at start of file +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1517.md +dotnet_diagnostic.SA1517.severity = warning + +# SA1518: Use line endings correctly at end of file +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1518.md +dotnet_diagnostic.SA1518.severity = warning + +# SA1519: Braces should not be omitted from multi-line child statement +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1519.md +dotnet_diagnostic.SA1519.severity = none + +# SA1520: Use braces consistently +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1520.md +dotnet_diagnostic.SA1520.severity = none + +# SA1600: Elements should be documented +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1600.md +dotnet_diagnostic.SA1600.severity = none + +# SA1601: Partial elements should be documented +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1601.md +dotnet_diagnostic.SA1601.severity = none + +# SA1602: Enumeration items should be documented +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1602.md +dotnet_diagnostic.SA1602.severity = none + +# SA1604: Element documentation should have summary +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1604.md +dotnet_diagnostic.SA1604.severity = none + +# SA1605: Partial element documentation should have summary +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1605.md +dotnet_diagnostic.SA1605.severity = none + +# SA1606: Element documentation should have summary text +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1606.md +dotnet_diagnostic.SA1606.severity = none + +# SA1607: Partial element documentation should have summary text +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1607.md +dotnet_diagnostic.SA1607.severity = none + +# SA1608: Element documentation should not have default summary +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1608.md +dotnet_diagnostic.SA1608.severity = none + +# SA1609: Property documentation should have value +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1609.md +dotnet_diagnostic.SA1609.severity = none + +# SA1610: Property documentation should have value text +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1610.md +dotnet_diagnostic.SA1610.severity = none + +# SA1611: Element parameters should be documented +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1611.md +dotnet_diagnostic.SA1611.severity = none + +# SA1612: Element parameter documentation should match element parameters +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1612.md +dotnet_diagnostic.SA1612.severity = none + +# SA1613: Element parameter documentation should declare parameter name +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1613.md +dotnet_diagnostic.SA1613.severity = none + +# SA1614: Element parameter documentation should have text +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1614.md +dotnet_diagnostic.SA1614.severity = none + +# SA1615: Element return value should be documented +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1615.md +dotnet_diagnostic.SA1615.severity = none + +# SA1616: Element return value documentation should have text +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1616.md +dotnet_diagnostic.SA1616.severity = none + +# SA1617: Void return value should not be documented +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1617.md +dotnet_diagnostic.SA1617.severity = none + +# SA1618: Generic type parameters should be documented +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1618.md +dotnet_diagnostic.SA1618.severity = none + +# SA1619: Generic type parameters should be documented partial class +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1619.md +dotnet_diagnostic.SA1619.severity = none + +# SA1620: Generic type parameter documentation should match type parameters +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1620.md +dotnet_diagnostic.SA1620.severity = none + +# SA1621: Generic type parameter documentation should declare parameter name +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1621.md +dotnet_diagnostic.SA1621.severity = none + +# SA1622: Generic type parameter documentation should have text +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1622.md +dotnet_diagnostic.SA1622.severity = none + +# SA1623: Property summary documentation should match accessors +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1623.md +dotnet_diagnostic.SA1623.severity = none + +# SA1624: Property summary documentation should omit accessor with restricted access +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1624.md +dotnet_diagnostic.SA1624.severity = none + +# SA1625: Element documentation should not be copied and pasted +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1625.md +dotnet_diagnostic.SA1625.severity = none + +# SA1626: Single-line comments should not use documentation style slashes +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1626.md +dotnet_diagnostic.SA1626.severity = none + +# SA1627: Documentation text should not be empty +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1627.md +dotnet_diagnostic.SA1627.severity = none + +# SA1629: Documentation text should end with a period +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1629.md +dotnet_diagnostic.SA1629.severity = none + +# SA1633: File should have header +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1633.md +dotnet_diagnostic.SA1633.severity = none + +# SA1634: File header should show copyright +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1634.md +dotnet_diagnostic.SA1634.severity = none + +# SA1635: File header should have copyright text +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1635.md +dotnet_diagnostic.SA1635.severity = none + +# SA1636: File header copyright text should match +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1636.md +dotnet_diagnostic.SA1636.severity = none + +# SA1637: File header should contain file name +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1637.md +dotnet_diagnostic.SA1637.severity = none + +# SA1638: File header file name documentation should match file name +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1638.md +dotnet_diagnostic.SA1638.severity = none + +# SA1639: File header should have summary +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1639.md +dotnet_diagnostic.SA1639.severity = none + +# SA1640: File header should have valid company text +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1640.md +dotnet_diagnostic.SA1640.severity = none + +# SA1641: File header company name text should match +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1641.md +dotnet_diagnostic.SA1641.severity = none + +# SA1642: Constructor summary documentation should begin with standard text +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1642.md +dotnet_diagnostic.SA1642.severity = none + +# SA1643: Destructor summary documentation should begin with standard text +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1643.md +dotnet_diagnostic.SA1643.severity = warning + +# SA1648: inheritdoc should be used with inheriting class +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1648.md +dotnet_diagnostic.SA1648.severity = none + +# SA1649: File name should match first type name +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1649.md +dotnet_diagnostic.SA1649.severity = none + +# SA1651: Do not use placeholder elements +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1651.md +dotnet_diagnostic.SA1651.severity = none + +# SX1101: Do not prefix local calls with 'this.' +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SX1101.md +dotnet_diagnostic.SX1101.severity = none + +# SX1309: Field names should begin with underscore +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SX1309.md +dotnet_diagnostic.SX1309.severity = none + +# SX1309S: Static field names should begin with underscore +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SX1309S.md +dotnet_diagnostic.SX1309S.severity = none \ No newline at end of file diff --git a/Microsoft.PowerShell.Archive.sln b/Microsoft.PowerShell.Archive.sln index 8e57f5f..54234d6 100644 --- a/Microsoft.PowerShell.Archive.sln +++ b/Microsoft.PowerShell.Archive.sln @@ -1,5 +1,4 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.3.32611.2 MinimumVisualStudioVersion = 10.0.40219.1 diff --git a/README.md b/README.md index 961344f..e3c7b8e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ -<<<<<<< HEAD # Microsoft.PowerShell.Archive Module [Microsoft.PowerShell.Archive module](https://technet.microsoft.com/en-us/library/dn818910.aspx) contains cmdlets that let you create and extract ZIP archives. @@ -13,6 +12,3 @@ ## [Expand-Archive](https://technet.microsoft.com/library/dn841359.aspx) examples 1. Extract the contents of an archive in the current folder: `Expand-Archive -Path SampleArchive.zip` 2. Use -Force parameter to overwrite existing files by those in the archive: `Expand-Archive -Path .\SampleArchive.zip -DestinationPath .\ExistingDir -Force` -======= -# Microsoft.PowerShell.Archive-vnext ->>>>>>> 168d6d4 (Add .gitattributes, .gitignore, and README.md.) diff --git a/SimpleBuild.ps1 b/SimpleBuild.ps1 index 64e7c38..74b2552 100644 --- a/SimpleBuild.ps1 +++ b/SimpleBuild.ps1 @@ -1,24 +1,20 @@ -$buildOutputDirectory = "$PSScriptRoot\src\bin\release\net7.0" +$buildOutputDirectory = "$PSScriptRoot\src\bin\release" if ((Test-Path $buildOutputDirectory)) { Remove-Item -Path $buildOutputDirectory -Recurse -Force } # Perform dotnet build -dotnet build "$PSScriptRoot\src\Microsoft.PowerShell.Archive.csproj" -c release - -# Show build output directory contents -Get-ChildItem $buildOutputDirectory -Recurse | Write-Output - -# Remove unneeded files -rm "$buildOutputDirectory/*.json","$buildOutputDirectory/*.pdb" +dotnet build "$PSScriptRoot\src\Microsoft.PowerShell.Archive.csproj" -c Release "Build module location: $buildOutputDirectory" | Write-Verbose -Verbose "Setting VSTS variable 'BuildOutDir' to '$buildOutputDirectory'" | Write-Verbose -Verbose Write-Host "##vso[task.setvariable variable=BuildOutDir]$buildOutputDirectory" -#$psd1ModuleVersion = (Get-Content -Path "$buildOutputDirectory\Microsoft.PowerShell.Archive.psd1" | Select-String 'ModuleVersion="(.*)"').Matches[0].Groups[1].Value -$psd1ModuleVersion = '2.0.1' -"Setting VSTS variable 'PackageVersion' to '$psd1ModuleVersion'" | Write-Verbose -Verbose -Write-Host "##vso[task.setvariable variable=PackageVersion]$psd1ModuleVersion" +# Get module version +$ManifestData = Import-PowerShellDataFile -Path $buildOutputDirectory\Microsoft.PowerShell.Archive.psd1" +$Version = $ManifestData.ModuleVersion + +"Setting VSTS variable 'PackageVersion' to '$Version'" | Write-Verbose -Verbose +Write-Host "##vso[task.setvariable variable=PackageVersion]$Version" diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index 19cfad0..5e98bc0 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -652,4 +652,4 @@ Remove-Item -LiteralPath $destinationPath } } -} \ No newline at end of file +} diff --git a/src/Localized/Messages.resx b/src/Localized/Messages.resx index ea04ed3..307f9d7 100644 --- a/src/Localized/Messages.resx +++ b/src/Localized/Messages.resx @@ -171,4 +171,4 @@ A path {0} supplied to -Path is the same as the path supplied to -DestinationPath. - \ No newline at end of file + diff --git a/src/Microsoft.PowerShell.Archive.csproj b/src/Microsoft.PowerShell.Archive.csproj index df50474..a033299 100644 --- a/src/Microsoft.PowerShell.Archive.csproj +++ b/src/Microsoft.PowerShell.Archive.csproj @@ -1,11 +1,18 @@ - + net7.0 enable en-US Microsoft.PowerShell.Archive - 2.0.0 + true + false + + + + False + None + false @@ -18,18 +25,13 @@ - - - True - True - Messages.resx - - - ResXFileCodeGenerator Messages.Designer.cs + CSharp + Microsoft.PowerShell.Archive.Localized + Messages diff --git a/src/Microsoft.PowerShell.Archive.psd1 b/src/Microsoft.PowerShell.Archive.psd1 index 41227c5..373ce80 100644 --- a/src/Microsoft.PowerShell.Archive.psd1 +++ b/src/Microsoft.PowerShell.Archive.psd1 @@ -1,127 +1,26 @@ @{ - -# Script module or binary module file associated with this manifest. RootModule = '' - -# Version number of this module. ModuleVersion = '2.0.1' - -# Supported PSEditions -# CompatiblePSEditions = @() - -# ID used to uniquely identify this module GUID = '06a335eb-dd10-4d25-b753-4f6a80163516' - -# Author of this module Author = 'Microsoft' - -# Company or vendor of this module CompanyName = 'Microsoft' - -# Copyright statement for this module Copyright = '(c) Microsoft. All rights reserved.' - -# Description of the functionality provided by this module Description = 'PowerShell module for creating and expanding archives.' - -# Minimum version of the PowerShell engine required by this module PowerShellVersion = '7.2.5' - -# Name of the PowerShell host required by this module -# PowerShellHostName = '' - -# Minimum version of the PowerShell host required by this module -# PowerShellHostVersion = '' - -# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. -# DotNetFrameworkVersion = '' - -# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. -# ClrVersion = '' - -# Processor architecture (None, X86, Amd64) required by this module -# ProcessorArchitecture = '' - -# Modules that must be imported into the global environment prior to importing this module -# RequiredModules = @() - -# Assemblies that must be loaded prior to importing this module -# RequiredAssemblies = @() - -# Script files (.ps1) that are run in the caller's environment prior to importing this module. -# ScriptsToProcess = @() - -# Type files (.ps1xml) to be loaded when importing this module -# TypesToProcess = @() - -# Format files (.ps1xml) to be loaded when importing this module -# FormatsToProcess = @() - -# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess NestedModules = @('Microsoft.PowerShell.Archive.dll') - -# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. -FunctionsToExport = '*' - -# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. +FunctionsToExport = '' CmdletsToExport = @('Compress-Archive') - -# Variables to export from this module -VariablesToExport = '*' - -# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. -AliasesToExport = '*' - -# DSC resources to export from this module -# DscResourcesToExport = @() - -# List of all modules packaged with this module -# ModuleList = @() - -# List of all files packaged with this module -# FileList = @() - -# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +VariablesToExport = '' +AliasesToExport = '' PrivateData = @{ - PSData = @{ - - # Tags applied to this module. These help with module discovery in online galleries. Tags = @('Archive', 'Zip', 'Compress') - - # A URL to the license for this module. - # LicenseUri = '' - - # A URL to the main website for this project. ProjectUri = 'https://github.com/PowerShell/Microsoft.PowerShell.Archive' - - # A URL to an icon representing this module. - # IconUri = '' - - # ReleaseNotes of this module ReleaseNotes = @' ## 2.0.1-preview1 - - Rewrote the archive module in C# + - Rewrote Compress-Archive cmdlet in C# '@ - - # Prerelease string of this module Prerelease = 'preview1' - - # Flag to indicate whether the module requires explicit user acceptance for install/update/save - # RequireLicenseAcceptance = $false - - # External dependent modules of this module - # ExternalModuleDependencies = @() - - } # End of PSData hashtable - -} # End of PrivateData hashtable - -# HelpInfo URI of this module -# HelpInfoURI = '' - -# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. -# DefaultCommandPrefix = '' - + } +} } - From d384f5074203dc06bfe688650b0ccdfb48eb2d59 Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Fri, 29 Jul 2022 11:54:49 -0700 Subject: [PATCH 34/42] fixed missing quotation mark in CI config, updated switch code in ArchiveFactory.TryGetArchiveFormatFromExtension, added another list to keep track of paths from -LiteralPath and -Path seperately --- SimpleBuild.ps1 | 10 +++++----- src/ArchiveFactory.cs | 7 +++---- src/CompressArchiveCommand.cs | 37 +++++++++++++++++++++++------------ 3 files changed, 33 insertions(+), 21 deletions(-) diff --git a/SimpleBuild.ps1 b/SimpleBuild.ps1 index 74b2552..1d89ba8 100644 --- a/SimpleBuild.ps1 +++ b/SimpleBuild.ps1 @@ -1,4 +1,4 @@ -$buildOutputDirectory = "$PSScriptRoot\src\bin\release" +$buildOutputDirectory = "$PSScriptRoot\src\bin\Release" if ((Test-Path $buildOutputDirectory)) { Remove-Item -Path $buildOutputDirectory -Recurse -Force @@ -9,12 +9,12 @@ dotnet build "$PSScriptRoot\src\Microsoft.PowerShell.Archive.csproj" -c Release "Build module location: $buildOutputDirectory" | Write-Verbose -Verbose -"Setting VSTS variable 'BuildOutDir' to '$buildOutputDirectory'" | Write-Verbose -Verbose -Write-Host "##vso[task.setvariable variable=BuildOutDir]$buildOutputDirectory" - # Get module version -$ManifestData = Import-PowerShellDataFile -Path $buildOutputDirectory\Microsoft.PowerShell.Archive.psd1" +$ManifestData = Import-PowerShellDataFile -Path "$buildOutputDirectory\Microsoft.PowerShell.Archive.psd1" $Version = $ManifestData.ModuleVersion +"Setting VSTS variable 'BuildOutDir' to '$buildOutputDirectory'" | Write-Verbose -Verbose +Write-Host "##vso[task.setvariable variable=BuildOutDir]$buildOutputDirectory" + "Setting VSTS variable 'PackageVersion' to '$Version'" | Write-Verbose -Verbose Write-Host "##vso[task.setvariable variable=PackageVersion]$Version" diff --git a/src/ArchiveFactory.cs b/src/ArchiveFactory.cs index 3d6c926..6ce5bb3 100644 --- a/src/ArchiveFactory.cs +++ b/src/ArchiveFactory.cs @@ -31,12 +31,11 @@ internal static IArchive GetArchive(ArchiveFormat format, string archivePath, Ar internal static bool TryGetArchiveFormatFromExtension(string path, out ArchiveFormat? archiveFormat) { - archiveFormat = System.IO.Path.GetExtension(path) switch + archiveFormat = System.IO.Path.GetExtension(path).ToLowerInvariant() switch { - ".zip" => archiveFormat = ArchiveFormat.Zip, + ".zip" => ArchiveFormat.Zip, /* Disable support for tar and tar.gz for preview1 release - ".tar" => archiveFormat = ArchiveFormat.Tar, - ".gz" => archiveFormat = ArchiveFormat.Tgz, + ".gz" => path.EndsWith(".tar.gz) ? ArchiveFormat.Tgz : null, */ _ => null }; diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index bdb2a79..ac7a2d5 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -64,7 +64,10 @@ public class CompressArchiveCommand : PSCmdlet [Parameter()] public ArchiveFormat? Format { get; set; } = null; - private List? _sourcePaths; + // Paths from -Path parameter + private List? _literalPaths; + + private List? _nonliteralPaths; private readonly PathHelper _pathHelper; @@ -74,7 +77,8 @@ public class CompressArchiveCommand : PSCmdlet public CompressArchiveCommand() { - _sourcePaths = new List(); + _literalPaths = new List(); + _nonliteralPaths = new List(); _pathHelper = new PathHelper(this); Messages.Culture = new System.Globalization.CultureInfo("en-US"); _didCreateNewArchive = false; @@ -95,32 +99,41 @@ protected override void BeginProcessing() protected override void ProcessRecord() { - // Add each path from -Path or -LiteralPath to _sourcePaths because they can get lost when the next item in the pipeline is sent + // Add each path from -Path or -LiteralPath to _nonliteralPaths or _literalPaths because they can get lost when the next item in the pipeline is sent if (ParameterSetName == "Path") { - _sourcePaths?.AddRange(Path); + _nonliteralPaths?.AddRange(Path); } else { - _sourcePaths?.AddRange(LiteralPath); + _literalPaths?.AddRange(LiteralPath); } } protected override void EndProcessing() { // Get archive entries, validation is performed by PathHelper - // _sourcePaths should not be null at this stage, but if it is, prevent a NullReferenceException by doing the following - List archiveAddtions = _sourcePaths != null ? _pathHelper.GetArchiveAdditionsForPath(_sourcePaths.ToArray(), ParameterSetName == "LiteralPath") : new List(); + // _literalPaths should not be null at this stage, but if it is, prevent a NullReferenceException by doing the following + List archiveAdditions = _literalPaths != null ? _pathHelper.GetArchiveAdditionsForPath(paths: _literalPaths.ToArray(), literalPath: true) : new List(); + + // Do the same as above for _nonliteralPaths + List? nonliteralArchiveAdditions = _nonliteralPaths != null ? _pathHelper.GetArchiveAdditionsForPath(paths: _nonliteralPaths.ToArray(), literalPath: false) : new List(); + + // Add nonliteralArchiveAdditions to archive additions, so we can keep track of one list only + archiveAdditions.AddRange(nonliteralArchiveAdditions); // Remove references to _sourcePaths, Path, and LiteralPath to free up memory // The user could have supplied a lot of paths, so we should do this Path = null; LiteralPath = null; - _sourcePaths = null; + _literalPaths = null; + _nonliteralPaths = null; + // Remove reference to nonliteralArchiveAdditions since we do not use it any more + nonliteralArchiveAdditions = null; // Throw a terminating error if there is a source path as same as DestinationPath. // We don't want to overwrite the file or directory that we want to add to the archive. - var additionsWithSamePathAsDestination = archiveAddtions.Where(addition => PathHelper.ArePathsSame(addition.FileSystemInfo, _destinationPathInfo)).ToList(); + var additionsWithSamePathAsDestination = archiveAdditions.Where(addition => PathHelper.ArePathsSame(addition.FileSystemInfo, _destinationPathInfo)).ToList(); if (additionsWithSamePathAsDestination.Count() > 0) { // Since duplicate checking is performed earlier, there must a single ArchiveAddition such that ArchiveAddition.FullPath == DestinationPath @@ -130,7 +143,7 @@ protected override void EndProcessing() } // Warn the user if there are no items to add for some reason (e.g., no items matched the filter) - if (archiveAddtions.Count == 0) + if (archiveAdditions.Count == 0) { WriteWarning(Messages.NoItemsToAddWarning); } @@ -160,11 +173,11 @@ protected override void EndProcessing() } // TODO: Update progress - long numberOfAdditions = archiveAddtions.Count; + long numberOfAdditions = archiveAdditions.Count; long numberOfAddedItems = 0; var progressRecord = new ProgressRecord(activityId: 1, activity: "Compress-Archive", "0% complete"); WriteProgress(progressRecord); - foreach (ArchiveAddition entry in archiveAddtions) + foreach (ArchiveAddition entry in archiveAdditions) { if (ShouldProcess(target: entry.FileSystemInfo.FullName, action: "Add")) { From 3ae03556b66b0ff7e8e928443f7000fba7a10ccb Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Mon, 1 Aug 2022 16:18:13 -0700 Subject: [PATCH 35/42] added assertions to prevent possible null error, added RequiredVersion when installing Pester in tests script, removed default valuesm in parameter attributes, and other minor changes --- .azdevops/RunTests.ps1 | 8 ++--- src/ArchiveAddition.cs | 25 ++------------ src/ArchiveFactory.cs | 6 ++-- src/CompressArchiveCommand.cs | 60 +++++++++++++++++---------------- src/ErrorMessages.cs | 4 +-- src/IArchive.cs | 2 +- src/PathHelper.cs | 63 +++++++++++++++++++++-------------- src/TarArchive.cs | 10 +++--- src/ZipArchive.cs | 14 ++++---- 9 files changed, 93 insertions(+), 99 deletions(-) diff --git a/.azdevops/RunTests.ps1 b/.azdevops/RunTests.ps1 index 4eeaec2..d5440a6 100644 --- a/.azdevops/RunTests.ps1 +++ b/.azdevops/RunTests.ps1 @@ -9,16 +9,16 @@ if ($null -ne $module) Import-Module "$env:PIPELINE_WORKSPACE/ModuleBuild/Microsoft.PowerShell.Archive.psd1" $module = Get-Module -Name "Microsoft.PowerShell.Archive" -$module.Path | Write-Verbose +$module.Path | Write-Verbose -Verbose # Load Pester -Install-Module -Name "Pester" -Force +Install-Module -Name "Pester" -RequiredVersion "5.3.3" -Force $module = Get-Module -Name "Pester" if ($null -ne $module) { Remove-Module "Pester" } -Import-Module -Name "Pester" +Import-Module -Name "Pester" -RequiredVersion "5.3.3" # Run tests $OutputFile = "$PWD/build-unit-tests.xml" @@ -32,7 +32,7 @@ if(!$results -or $results.FailedCount -gt 0 -or !$results.TotalCount) # Unload module $module = Get-Module -Name "Microsoft.PowerShell.Archive" -if ($module -ne $null) +if ($null -ne $module) { Remove-Module $module } diff --git a/src/ArchiveAddition.cs b/src/ArchiveAddition.cs index d0ae1f1..4c59a7f 100644 --- a/src/ArchiveAddition.cs +++ b/src/ArchiveAddition.cs @@ -14,33 +14,14 @@ internal class ArchiveAddition /// The name of the file or directory in the archive. /// This is a path of the file or directory in the archive (e.g., 'file1.txt` means the file is a top-level file in the archive). /// - public string EntryName { get; set; } + internal string EntryName { get; set; } - /// - /// The fully qualified path of the file or directory to add to or update in the archive. - /// - //public string FullPath { get; set; } - - public System.IO.FileSystemInfo FileSystemInfo { get; set; } - - /// - /// The type of filesystem entry to add. - /// - //public ArchiveAdditionType Type { get; set; } + internal System.IO.FileSystemInfo FileSystemInfo { get; set; } - public ArchiveAddition(string entryName, System.IO.FileSystemInfo fileSystemInfo) + internal ArchiveAddition(string entryName, System.IO.FileSystemInfo fileSystemInfo) { EntryName = entryName; FileSystemInfo = fileSystemInfo; } - - /// - /// This enum tracks types of filesystem entries - /// - internal enum ArchiveAdditionType - { - File, - Directory, - } } } diff --git a/src/ArchiveFactory.cs b/src/ArchiveFactory.cs index 6ce5bb3..35b557e 100644 --- a/src/ArchiveFactory.cs +++ b/src/ArchiveFactory.cs @@ -15,7 +15,6 @@ internal static IArchive GetArchive(ArchiveFormat format, string archivePath, Ar ArchiveMode.Create => new System.IO.FileStream(archivePath, mode: System.IO.FileMode.CreateNew, access: System.IO.FileAccess.Write, share: System.IO.FileShare.None), ArchiveMode.Update => new System.IO.FileStream(archivePath, mode: System.IO.FileMode.Open, access: System.IO.FileAccess.ReadWrite, share: System.IO.FileShare.None), ArchiveMode.Extract => new System.IO.FileStream(archivePath, mode: System.IO.FileMode.Open, access: System.IO.FileAccess.Read, share: System.IO.FileShare.Read), - // TODO: Add message to exception _ => throw new ArgumentOutOfRangeException(nameof(archiveMode)) }; @@ -23,8 +22,7 @@ internal static IArchive GetArchive(ArchiveFormat format, string archivePath, Ar { ArchiveFormat.Zip => new ZipArchive(archivePath, archiveMode, archiveFileStream, compressionLevel), //ArchiveFormat.tar => new TarArchive(archivePath, archiveMode, archiveFileStream), - // TODO: Add archive types here - // TODO: Add message to exception + // TODO: Add Tar.gz here _ => throw new ArgumentOutOfRangeException(nameof(archiveMode)) }; } @@ -39,7 +37,7 @@ internal static bool TryGetArchiveFormatFromExtension(string path, out ArchiveFo */ _ => null }; - return archiveFormat != null; + return archiveFormat is not null; } } } diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index ac7a2d5..cdd25a8 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -6,26 +6,20 @@ using System.Linq; using System.Management.Automation; using System.IO.Compression; +using System.Diagnostics; namespace Microsoft.PowerShell.Archive { [Cmdlet("Compress", "Archive", SupportsShouldProcess = true)] [OutputType(typeof(FileInfo))] - public class CompressArchiveCommand : PSCmdlet + public sealed class CompressArchiveCommand : PSCmdlet { // TODO: Add filter parameter - // TODO: Add format parameter // TODO: Add flatten parameter // TODO: Add comments to methods - - // TODO: Add warnings for archive file extension // TODO: Add tar support - // TODO: Add comments to ArchiveEntry and for adding filesystem entry to zip - - // TODO: Add error messages for each error code - /// /// The Path parameter - specifies paths of files or directories from the filesystem to add to or update in the archive. /// This parameter does expand wildcard characters. @@ -38,7 +32,7 @@ public class CompressArchiveCommand : PSCmdlet /// The LiteralPath parameter - specifies paths of files or directories from the filesystem to add to or update in the archive. /// This parameter does not expand wildcard characters. /// - [Parameter(Mandatory = true, ParameterSetName = "LiteralPath", ValueFromPipeline = false, ValueFromPipelineByPropertyName = true)] + [Parameter(Mandatory = true, ParameterSetName = "LiteralPath", ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] [Alias("PSPath")] public string[]? LiteralPath { get; set; } @@ -46,13 +40,13 @@ public class CompressArchiveCommand : PSCmdlet /// /// The DestinationPath parameter - specifies the location of the archive in the filesystem. /// - [Parameter(Mandatory = true, Position = 2, ValueFromPipeline = false, ValueFromPipelineByPropertyName = false)] + [Parameter(Mandatory = true, Position = 1)] [ValidateNotNullOrEmpty] [NotNull] public string? DestinationPath { get; set; } [Parameter()] - public WriteMode WriteMode { get; set; } = WriteMode.Create; + public WriteMode WriteMode { get; set; } [Parameter()] public SwitchParameter PassThru { get; set; } @@ -64,9 +58,10 @@ public class CompressArchiveCommand : PSCmdlet [Parameter()] public ArchiveFormat? Format { get; set; } = null; - // Paths from -Path parameter + // Stores paths from -Path parameter private List? _literalPaths; + // Stores paths from -LiteralPath parameter private List? _nonliteralPaths; private readonly PathHelper _pathHelper; @@ -89,8 +84,6 @@ protected override void BeginProcessing() { _destinationPathInfo = _pathHelper.ResolveToSingleFullyQualifiedPath(DestinationPath); DestinationPath = _destinationPathInfo.FullName; - - // Validate ValidateDestinationPath(); // Determine archive format based on DestinationPath @@ -102,22 +95,28 @@ protected override void ProcessRecord() // Add each path from -Path or -LiteralPath to _nonliteralPaths or _literalPaths because they can get lost when the next item in the pipeline is sent if (ParameterSetName == "Path") { + Debug.Assert(Path is not null); _nonliteralPaths?.AddRange(Path); } else { + Debug.Assert(LiteralPath is not null); _literalPaths?.AddRange(LiteralPath); } } protected override void EndProcessing() { + Debug.Assert(_destinationPathInfo is not null); + Debug.Assert(_literalPaths is not null); + Debug.Assert(_nonliteralPaths is not null); + // Get archive entries, validation is performed by PathHelper // _literalPaths should not be null at this stage, but if it is, prevent a NullReferenceException by doing the following - List archiveAdditions = _literalPaths != null ? _pathHelper.GetArchiveAdditionsForPath(paths: _literalPaths.ToArray(), literalPath: true) : new List(); + List archiveAdditions = _pathHelper.GetArchiveAdditionsForPath(paths: _literalPaths.ToArray(), literalPath: true); // Do the same as above for _nonliteralPaths - List? nonliteralArchiveAdditions = _nonliteralPaths != null ? _pathHelper.GetArchiveAdditionsForPath(paths: _nonliteralPaths.ToArray(), literalPath: false) : new List(); + List? nonliteralArchiveAdditions = _pathHelper.GetArchiveAdditionsForPath(paths: _nonliteralPaths.ToArray(), literalPath: false); // Add nonliteralArchiveAdditions to archive additions, so we can keep track of one list only archiveAdditions.AddRange(nonliteralArchiveAdditions); @@ -134,7 +133,7 @@ protected override void EndProcessing() // Throw a terminating error if there is a source path as same as DestinationPath. // We don't want to overwrite the file or directory that we want to add to the archive. var additionsWithSamePathAsDestination = archiveAdditions.Where(addition => PathHelper.ArePathsSame(addition.FileSystemInfo, _destinationPathInfo)).ToList(); - if (additionsWithSamePathAsDestination.Count() > 0) + if (additionsWithSamePathAsDestination.Count > 0) { // Since duplicate checking is performed earlier, there must a single ArchiveAddition such that ArchiveAddition.FullPath == DestinationPath var errorCode = ParameterSetName == "Path" ? ErrorCode.SamePathAndDestinationPath : ErrorCode.SameLiteralPathAndDestinationPath; @@ -172,16 +171,17 @@ protected override void EndProcessing() _didCreateNewArchive = archiveMode == ArchiveMode.Update; } - // TODO: Update progress + long numberOfAdditions = archiveAdditions.Count; long numberOfAddedItems = 0; var progressRecord = new ProgressRecord(activityId: 1, activity: "Compress-Archive", "0% complete"); WriteProgress(progressRecord); + foreach (ArchiveAddition entry in archiveAdditions) { if (ShouldProcess(target: entry.FileSystemInfo.FullName, action: "Add")) { - archive?.AddFilesytemEntry(entry); + archive?.AddFileSystemEntry(entry); // Keep track of number of items added to the archive and use that to update progress numberOfAddedItems++; var percentComplete = numberOfAddedItems / (float)numberOfAdditions * 100f; @@ -189,7 +189,7 @@ protected override void EndProcessing() WriteProgress(progressRecord); // Write a verbose message saying this item was added to the archive - var addedItemMessage = String.Format(Messages.AddedItemToArchiveVerboseMessage, entry.FileSystemInfo.FullName); + var addedItemMessage = string.Format(Messages.AddedItemToArchiveVerboseMessage, entry.FileSystemInfo.FullName); WriteVerbose(addedItemMessage); } else { @@ -231,7 +231,7 @@ protected override void StopProcessing() /// private void ValidateDestinationPath() { - // TODO: Add tests cases for conditions below + Debug.Assert(_destinationPathInfo is not null); ErrorCode? errorCode = null; // In this case, DestinationPath does not exist @@ -262,7 +262,7 @@ private void ValidateDestinationPath() errorCode = ErrorCode.CannotOverwriteWorkingDirectory; } // Throw an error if the DestinationPath is a directory with at 1 least item and the cmdlet is in Overwrite mode - else if (WriteMode == WriteMode.Overwrite && (_destinationPathInfo as DirectoryInfo).GetFileSystemInfos().Length > 0) + else if (WriteMode == WriteMode.Overwrite && _destinationPathInfo is DirectoryInfo directory && directory.GetFileSystemInfos().Length > 0) { errorCode = ErrorCode.ArchiveIsNonEmptyDirectory; } @@ -282,7 +282,7 @@ private void ValidateDestinationPath() } } - if (errorCode != null) + if (errorCode is not null) { // Throw an error -- since we are validating DestinationPath, the problem is with DestinationPath var errorRecord = ErrorMessages.GetErrorRecord(errorCode: errorCode.Value, errorItem: _destinationPathInfo.FullName); @@ -292,26 +292,27 @@ private void ValidateDestinationPath() private void DeleteDestinationPathIfExists() { + Debug.Assert(_destinationPathInfo is not null); try { // No need to ensure DestinationPath has no children when deleting it // because ValidateDestinationPath should have already done this if (_destinationPathInfo.Exists) { - _destinationPathInfo?.Delete(); + _destinationPathInfo.Delete(); } } // Throw a terminating error if an IOException occurs catch (IOException ioException) { - var errorRecord = new ErrorRecord(ioException, errorId: ErrorCode.OverwriteDestinationPathFailed.ToString(), + var errorRecord = new ErrorRecord(ioException, errorId: nameof(ErrorCode.OverwriteDestinationPathFailed), errorCategory: ErrorCategory.InvalidOperation, targetObject: _destinationPathInfo.FullName); ThrowTerminatingError(errorRecord); } // Throw a terminating error if an UnauthorizedAccessException occurs catch (System.UnauthorizedAccessException unauthorizedAccessException) { - var errorRecord = new ErrorRecord(unauthorizedAccessException, errorId: ErrorCode.InsufficientPermissionsToAccessPath.ToString(), + var errorRecord = new ErrorRecord(unauthorizedAccessException, errorId: nameof(ErrorCode.InsufficientPermissionsToAccessPath), errorCategory: ErrorCategory.PermissionDenied, targetObject: _destinationPathInfo.FullName); ThrowTerminatingError(errorRecord); } @@ -319,6 +320,7 @@ private void DeleteDestinationPathIfExists() private void DetermineArchiveFormat() { + Debug.Assert(_destinationPathInfo is not null); // Check if cmdlet is able to determine the format of the archive based on the extension of DestinationPath bool ableToDetermineArchiveFormat = ArchiveFactory.TryGetArchiveFormatFromExtension(path: _destinationPathInfo.FullName, archiveFormat: out var archiveFormat); // If the user did not specify which archive format to use, try to determine it automatically @@ -331,12 +333,12 @@ private void DetermineArchiveFormat() else { // If the archive format could not be determined, use zip by default and emit a warning - var warningMsg = String.Format(Messages.ArchiveFormatCouldNotBeDeterminedWarning, _destinationPathInfo.FullName); + var warningMsg = string.Format(Messages.ArchiveFormatCouldNotBeDeterminedWarning, _destinationPathInfo.FullName); WriteWarning(warningMsg); Format = ArchiveFormat.Zip; } // Write a verbose message saying that Format is not specified and a format was determined automatically - string verboseMessage = String.Format(Messages.ArchiveFormatDeterminedVerboseMessage, Format); + string verboseMessage = string.Format(Messages.ArchiveFormatDeterminedVerboseMessage, Format); WriteVerbose(verboseMessage); } // If the user did specify which archive format to use, emit a warning if DestinationPath does not match the chosen archive format @@ -344,7 +346,7 @@ private void DetermineArchiveFormat() { if (archiveFormat is null || archiveFormat.Value != Format.Value) { - var warningMsg = String.Format(Messages.ArchiveExtensionDoesNotMatchArchiveFormatWarning, _destinationPathInfo.FullName); + var warningMsg = string.Format(Messages.ArchiveExtensionDoesNotMatchArchiveFormatWarning, _destinationPathInfo.FullName); WriteWarning(warningMsg); } } diff --git a/src/ErrorMessages.cs b/src/ErrorMessages.cs index dec8a6b..2040b92 100644 --- a/src/ErrorMessages.cs +++ b/src/ErrorMessages.cs @@ -8,7 +8,7 @@ internal static class ErrorMessages { internal static ErrorRecord GetErrorRecord(ErrorCode errorCode, string errorItem) { - var errorMsg = String.Format(GetErrorMessage(errorCode: errorCode), errorItem); + var errorMsg = string.Format(GetErrorMessage(errorCode: errorCode), errorItem); var exception = new ArgumentException(errorMsg); return new ErrorRecord(exception, errorCode.ToString(), ErrorCategory.InvalidArgument, errorItem); } @@ -30,7 +30,7 @@ internal static string GetErrorMessage(ErrorCode errorCode) ErrorCode.InsufficientPermissionsToAccessPath => Messages.InsufficientPermssionsToAccessPathMessage, ErrorCode.OverwriteDestinationPathFailed => Messages.OverwriteDestinationPathFailed, ErrorCode.CannotOverwriteWorkingDirectory => Messages.CannotOverwriteWorkingDirectoryMessage, - _ => throw new NotImplementedException("Error code has not been implemented") + _ => throw new ArgumentOutOfRangeException(nameof(errorCode)) }; } } diff --git a/src/IArchive.cs b/src/IArchive.cs index c7cb82e..5903ee5 100644 --- a/src/IArchive.cs +++ b/src/IArchive.cs @@ -15,7 +15,7 @@ internal interface IArchive: IDisposable // Add a file or folder to the archive. The entry name of the added item in the // will be ArchiveEntry.Name. // Throws an exception if the archive is in read mode. - internal void AddFilesytemEntry(ArchiveAddition entry); + internal void AddFileSystemEntry(ArchiveAddition entry); // Get the entries in the archive. // Throws an exception if the archive is in create mode. diff --git a/src/PathHelper.cs b/src/PathHelper.cs index 2fe04fd..4457fd6 100644 --- a/src/PathHelper.cs +++ b/src/PathHelper.cs @@ -9,10 +9,9 @@ namespace Microsoft.PowerShell.Archive { - // TODO: Add exception handling internal class PathHelper { - private PSCmdlet _cmdlet; + private readonly PSCmdlet _cmdlet; private const string FileSystemProviderName = "FileSystem"; @@ -50,17 +49,17 @@ internal List GetArchiveAdditionsForPath(string[] paths, bool l if (nonfilesystemPaths.Count > 0) { // Get an error record and throw it - var commaSperatedPaths = String.Join(separator: ',', values: nonfilesystemPaths); + var commaSperatedPaths = string.Join(separator: ',', values: nonfilesystemPaths); var errorRecord = ErrorMessages.GetErrorRecord(errorCode: ErrorCode.InvalidPath, errorItem: commaSperatedPaths); _cmdlet.ThrowTerminatingError(errorRecord: errorRecord); } // If there are duplicate paths, throw an error var duplicates = GetDuplicatePaths(additions); - if (duplicates.Count() > 0) + if (duplicates.Any()) { // Get an error record and throw it - var commaSperatedPaths = String.Join(separator: ',', values: duplicates); + var commaSperatedPaths = string.Join(separator: ',', values: duplicates); var errorRecord = ErrorMessages.GetErrorRecord(errorCode: ErrorCode.DuplicatePaths, errorItem: commaSperatedPaths); _cmdlet.ThrowTerminatingError(errorRecord: errorRecord); } @@ -124,15 +123,15 @@ private void AddArchiveAdditionForUserEnteredNonLiteralPath(string path, List + /// Determines the entry name of a file or directory based on its path and a prefix. + /// The prefix is removed from the path and the remaining portion is the entry name. + /// + /// + /// + /// + /// + private static string GetEntryName(string path, string prefix) { - if (prefix == String.Empty) return path; + if (prefix == string.Empty) return path; // If the path does not start with the prefix, throw an exception if (!path.StartsWith(prefix)) @@ -306,12 +306,18 @@ private string GetEntryName(string path, string prefix) return entryName; } - private string GetPrefixForPath(System.IO.DirectoryInfo directoryInfo) + /// + /// Gets the prefix from the path to a directory. This prefix is necessary to determine the entry names of all + /// descendents of the directory. + /// + /// + /// + private static string GetPrefixForPath(System.IO.DirectoryInfo directoryInfo) { // Get the parent directory of the path if (directoryInfo.Parent is null) { - return String.Empty; + return string.Empty; } var prefix = directoryInfo.Parent.FullName; if (!prefix.EndsWith(System.IO.Path.DirectorySeparatorChar)) @@ -326,7 +332,7 @@ private string GetPrefixForPath(System.IO.DirectoryInfo directoryInfo) /// /// /// - private IEnumerable GetDuplicatePaths(List additions) + private static IEnumerable GetDuplicatePaths(List additions) { return additions.GroupBy(x => x.FileSystemInfo.FullName) .Where(group => group.Count() > 1) @@ -363,7 +369,7 @@ internal System.IO.FileSystemInfo ResolveToSingleFullyQualifiedPath(string path) /// /// /// - private bool CanPreservePathStructure(string path) + private static bool CanPreservePathStructure(string path) { return !System.IO.Path.IsPathRooted(path); } @@ -379,6 +385,13 @@ private bool TryGetPathRelativeToCurrentWorkingDirectory(string path, out string relativePath = System.IO.Path.GetRelativePath(_cmdlet.SessionState.Path.CurrentFileSystemLocation.Path, path); return !relativePath.Contains(".."); } + + /// + /// Determines if two paths are the same + /// + /// + /// + /// internal static bool ArePathsSame(System.IO.FileSystemInfo fileSystemInfo1, System.IO.FileSystemInfo fileSystemInfo2) { // If one is a file and the other is a directory, return false diff --git a/src/TarArchive.cs b/src/TarArchive.cs index b2d88ed..0dcb148 100644 --- a/src/TarArchive.cs +++ b/src/TarArchive.cs @@ -10,13 +10,13 @@ internal class TarArchive : IArchive { private bool disposedValue; - private ArchiveMode _mode; + private readonly ArchiveMode _mode; - private string _path; + private readonly string _path; - private TarWriter _tarWriter; + private readonly TarWriter _tarWriter; - private FileStream _fileStream; + private readonly FileStream _fileStream; ArchiveMode IArchive.Mode => _mode; @@ -30,7 +30,7 @@ public TarArchive(string path, ArchiveMode mode, FileStream fileStream) _fileStream = fileStream; } - void IArchive.AddFilesytemEntry(ArchiveAddition entry) + void IArchive.AddFileSystemEntry(ArchiveAddition entry) { _tarWriter.WriteEntry(fileName: entry.FileSystemInfo.FullName, entryName: entry.EntryName); } diff --git a/src/ZipArchive.cs b/src/ZipArchive.cs index 35b019a..29defc2 100644 --- a/src/ZipArchive.cs +++ b/src/ZipArchive.cs @@ -9,15 +9,15 @@ internal class ZipArchive : IArchive { private bool disposedValue; - private ArchiveMode _mode; + private readonly ArchiveMode _mode; - private string _archivePath; + private readonly string _archivePath; - private System.IO.FileStream _archiveStream; + private readonly System.IO.FileStream _archiveStream; - private System.IO.Compression.ZipArchive _zipArchive; + private readonly System.IO.Compression.ZipArchive _zipArchive; - private System.IO.Compression.CompressionLevel _compressionLevel; + private readonly System.IO.Compression.CompressionLevel _compressionLevel; private const char ZipArchiveDirectoryPathTerminator = '/'; @@ -39,7 +39,7 @@ public ZipArchive(string archivePath, ArchiveMode mode, System.IO.FileStream arc // it is up to the extraction software to deal with it (this is how it's done in other archive software). // The .NET API differentiates a file and folder based on the last character being '/'. In other words, if the last character in a path is '/', it is treated as a folder. // Otherwise, the .NET API treats the path as a file. - void IArchive.AddFilesytemEntry(ArchiveAddition addition) + void IArchive.AddFileSystemEntry(ArchiveAddition addition) { if (_mode == ArchiveMode.Extract) throw new InvalidOperationException("Cannot add a filesystem entry to an archive in read mode"); @@ -89,7 +89,7 @@ void IArchive.Expand(string destinationPath) throw new NotImplementedException(); } - private System.IO.Compression.ZipArchiveMode ConvertToZipArchiveMode(ArchiveMode archiveMode) + private static System.IO.Compression.ZipArchiveMode ConvertToZipArchiveMode(ArchiveMode archiveMode) { switch (archiveMode) { From abeb7a2a1acad9ad738aca89f049ce6af1a7b9bd Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Tue, 2 Aug 2022 12:34:48 -0700 Subject: [PATCH 36/42] added localized messages for Add, Create, and progress bar text, minor formatting changes --- .azdevops/RunTests.ps1 | 27 ++++++++----- .azdevops/build.ps1 | 4 +- .azdevops/runtest.yml | 2 +- src/CompressArchiveCommand.cs | 54 ++++++++++++------------- src/Localized/Messages.resx | 11 ++++- src/Microsoft.PowerShell.Archive.csproj | 10 ++++- src/PathHelper.cs | 5 ++- src/ZipArchive.cs | 10 +++-- 8 files changed, 77 insertions(+), 46 deletions(-) diff --git a/.azdevops/RunTests.ps1 b/.azdevops/RunTests.ps1 index d5440a6..9f76f2b 100644 --- a/.azdevops/RunTests.ps1 +++ b/.azdevops/RunTests.ps1 @@ -8,17 +8,26 @@ if ($null -ne $module) # Import the built module Import-Module "$env:PIPELINE_WORKSPACE/ModuleBuild/Microsoft.PowerShell.Archive.psd1" -$module = Get-Module -Name "Microsoft.PowerShell.Archive" -$module.Path | Write-Verbose -Verbose +$pesterRequiredVersion = "5.3.3" + +# If Pester 5.3.3 is not installed, install it +$shouldInstallPester = $true + +if ($pesterModules = Get-Module -Name "Pester" -ListAvailable) { + foreach ($module in $pesterModules) { + if ($module.Version.ToString() -eq $pesterRequiredVersion) { + $shouldInstallPester = $false + break + } + } +} + +if ($shouldInstallPester) { + Install-Module -Name "Pester" -RequiredVersion $pesterRequiredVersion -Force +} # Load Pester -Install-Module -Name "Pester" -RequiredVersion "5.3.3" -Force -$module = Get-Module -Name "Pester" -if ($null -ne $module) -{ - Remove-Module "Pester" -} -Import-Module -Name "Pester" -RequiredVersion "5.3.3" +Import-Module -Name "Pester" -RequiredVersion $pesterRequiredVersion # Run tests $OutputFile = "$PWD/build-unit-tests.xml" diff --git a/.azdevops/build.ps1 b/.azdevops/build.ps1 index 630610a..0c9a0ff 100644 --- a/.azdevops/build.ps1 +++ b/.azdevops/build.ps1 @@ -32,8 +32,8 @@ if (-not $test -and -not $build -and -not $publish -and -not $package) { [bool]$verboseValue = $PSBoundParameters['Verbose'].IsPresent ? $PSBoundParameters['Verbose'].ToBool() : $false $FileManifest = @( - @{ SRC = "${$BuildOutputDir}"; NAME = "Microsoft.PowerShell.Archive.dll" ; SIGN = $true ; DEST = "OUTDIR" } - @{ SRC = "${$BuildOutputDir}"; NAME = "Microsoft.PowerShell.Archive.psm1" ; SIGN = $true ; DEST = "OUTDIR" } + @{ SRC = "${$BuildOutputDir}"; NAME = "Microsoft.PowerShell.Archive.dll"; SIGN = $true ; DEST = "OUTDIR" } + @{ SRC = "${$BuildOutputDir}"; NAME = "Microsoft.PowerShell.Archive.psm1"; SIGN = $true ; DEST = "OUTDIR" } ) if ($build) { diff --git a/.azdevops/runtest.yml b/.azdevops/runtest.yml index 3b38785..604ba77 100644 --- a/.azdevops/runtest.yml +++ b/.azdevops/runtest.yml @@ -76,4 +76,4 @@ jobs: testResultsFiles: '**/*tests.xml' testRunTitle: 'Build Unit Tests' continueOnError: true - condition: succeededOrFailed() \ No newline at end of file + condition: succeededOrFailed() diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index cdd25a8..fa2ef80 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -1,12 +1,13 @@ -using Microsoft.PowerShell.Archive.Localized; -using System; +using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.IO.Compression; using System.Linq; using System.Management.Automation; -using System.IO.Compression; -using System.Diagnostics; + +using Microsoft.PowerShell.Archive.Localized; namespace Microsoft.PowerShell.Archive { @@ -20,11 +21,17 @@ public sealed class CompressArchiveCommand : PSCmdlet // TODO: Add comments to methods // TODO: Add tar support + private enum ParameterSet + { + Path, + LiteralPath + } + /// /// The Path parameter - specifies paths of files or directories from the filesystem to add to or update in the archive. /// This parameter does expand wildcard characters. /// - [Parameter(Mandatory = true, Position = 0, ParameterSetName = "Path", ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [Parameter(Mandatory = true, Position = 0, ParameterSetName = nameof(ParameterSet.Path), ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] public string[]? Path { get; set; } @@ -32,7 +39,7 @@ public sealed class CompressArchiveCommand : PSCmdlet /// The LiteralPath parameter - specifies paths of files or directories from the filesystem to add to or update in the archive. /// This parameter does not expand wildcard characters. /// - [Parameter(Mandatory = true, ParameterSetName = "LiteralPath", ValueFromPipelineByPropertyName = true)] + [Parameter(Mandatory = true, ParameterSetName = nameof(ParameterSet.LiteralPath), ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] [Alias("PSPath")] public string[]? LiteralPath { get; set; } @@ -93,7 +100,7 @@ protected override void BeginProcessing() protected override void ProcessRecord() { // Add each path from -Path or -LiteralPath to _nonliteralPaths or _literalPaths because they can get lost when the next item in the pipeline is sent - if (ParameterSetName == "Path") + if (ParameterSetName == nameof(ParameterSet.Path)) { Debug.Assert(Path is not null); _nonliteralPaths?.AddRange(Path); @@ -136,7 +143,7 @@ protected override void EndProcessing() if (additionsWithSamePathAsDestination.Count > 0) { // Since duplicate checking is performed earlier, there must a single ArchiveAddition such that ArchiveAddition.FullPath == DestinationPath - var errorCode = ParameterSetName == "Path" ? ErrorCode.SamePathAndDestinationPath : ErrorCode.SameLiteralPathAndDestinationPath; + var errorCode = ParameterSetName == nameof(ParameterSet.Path) ? ErrorCode.SamePathAndDestinationPath : ErrorCode.SameLiteralPathAndDestinationPath; var errorRecord = ErrorMessages.GetErrorRecord(errorCode, errorItem: additionsWithSamePathAsDestination[0].FileSystemInfo.FullName); ThrowTerminatingError(errorRecord); } @@ -148,22 +155,19 @@ protected override void EndProcessing() } // Get the ArchiveMode for the archive to be created or updated - ArchiveMode archiveMode = ArchiveMode.Create; - if (WriteMode == WriteMode.Update) - { - archiveMode = ArchiveMode.Update; - } + ArchiveMode archiveMode = WriteMode == WriteMode.Update ? ArchiveMode.Update : ArchiveMode.Create; // Don't create the archive object yet because the user could have specified -WhatIf or -Confirm IArchive? archive = null; try { - if (ShouldProcess(target: _destinationPathInfo.FullName, action: "Create")) + if (ShouldProcess(target: _destinationPathInfo.FullName, action: Messages.Create)) { // If the WriteMode is overwrite, delete the existing archive if (WriteMode == WriteMode.Overwrite) { DeleteDestinationPathIfExists(); + _destinationPathInfo = new FileInfo(_destinationPathInfo.FullName); } // Create an archive -- this is where we will switch between different types of archives @@ -174,33 +178,30 @@ protected override void EndProcessing() long numberOfAdditions = archiveAdditions.Count; long numberOfAddedItems = 0; - var progressRecord = new ProgressRecord(activityId: 1, activity: "Compress-Archive", "0% complete"); + var progressRecord = new ProgressRecord(activityId: 1, activity: "Compress-Archive", statusDescription: string.Format(Messages.ProgressDisplay, "0.0")); WriteProgress(progressRecord); foreach (ArchiveAddition entry in archiveAdditions) { - if (ShouldProcess(target: entry.FileSystemInfo.FullName, action: "Add")) + if (ShouldProcess(target: entry.FileSystemInfo.FullName, action: Messages.Add)) { archive?.AddFileSystemEntry(entry); - // Keep track of number of items added to the archive and use that to update progress - numberOfAddedItems++; - var percentComplete = numberOfAddedItems / (float)numberOfAdditions * 100f; - progressRecord.StatusDescription = $"{percentComplete:0.0}% complete"; - WriteProgress(progressRecord); - // Write a verbose message saying this item was added to the archive var addedItemMessage = string.Format(Messages.AddedItemToArchiveVerboseMessage, entry.FileSystemInfo.FullName); WriteVerbose(addedItemMessage); - } else - { - numberOfAdditions--; } + + // Keep track of number of items added to the archive and use that to update progress + numberOfAddedItems++; + var percentComplete = numberOfAddedItems / (float)numberOfAdditions * 100f; + progressRecord.StatusDescription = string.Format(Messages.ProgressDisplay, "{percentComplete:0.0}"); + WriteProgress(progressRecord); } // If there were no items to add, show progress as 100% if (numberOfAdditions == 0) { - progressRecord.StatusDescription = "100% complete"; + progressRecord.StatusDescription = string.Format(Messages.ProgressDisplay, "100.0"); WriteProgress(progressRecord); } } @@ -212,7 +213,6 @@ protected override void EndProcessing() // If -PassThru is specified, write a System.IO.FileInfo object if (PassThru) { - _destinationPathInfo = new FileInfo(_destinationPathInfo.FullName); WriteObject(_destinationPathInfo); } } diff --git a/src/Localized/Messages.resx b/src/Localized/Messages.resx index 307f9d7..c5535d6 100644 --- a/src/Localized/Messages.resx +++ b/src/Localized/Messages.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Add + {0} was added to the archive. @@ -147,6 +150,9 @@ Unable to overwrite the path {0} because it is the same as the current working directory. + + Create + The path(s) {0} have been specified more than once. @@ -165,10 +171,13 @@ The path {0} could not be found. + + {0}% complete + A path {0} supplied to -LiteralPath is the same as the path supplied to -DestinationPath. A path {0} supplied to -Path is the same as the path supplied to -DestinationPath. - + \ No newline at end of file diff --git a/src/Microsoft.PowerShell.Archive.csproj b/src/Microsoft.PowerShell.Archive.csproj index a033299..b8bd133 100644 --- a/src/Microsoft.PowerShell.Archive.csproj +++ b/src/Microsoft.PowerShell.Archive.csproj @@ -5,7 +5,7 @@ enable en-US Microsoft.PowerShell.Archive - true + true false @@ -25,6 +25,14 @@ + + + True + True + Messages.resx + + + ResXFileCodeGenerator diff --git a/src/PathHelper.cs b/src/PathHelper.cs index 4457fd6..c9ece93 100644 --- a/src/PathHelper.cs +++ b/src/PathHelper.cs @@ -86,7 +86,10 @@ private void AddArchiveAdditionForUserEnteredNonLiteralPath(string path, List Date: Wed, 3 Aug 2022 12:43:08 -0700 Subject: [PATCH 37/42] added copyright header, removed usage of DS in tests, minor formatting changes --- .azdevops/CI.yml | 12 +- .../{build.yml => ReleaseBuildPipeline.yml} | 4 +- .azdevops/RunTests.ps1 | 14 +- .azdevops/SignAndPackageModule.ps1 | 44 + .azdevops/{runtest.yml => TestsTemplate.yml} | 0 .azdevops/build.ps1 | 105 -- .travis.yml | 22 - SimpleBuild.ps1 => Build.ps1 | 5 +- Tests/Compress-Archive.Tests.ps1 | 328 ++--- .../Pester.Commands.Cmdlets.Archive.Tests.ps1 | 1258 ----------------- Tests/SamplePreCreatedArchive.archive | Bin 270 -> 0 bytes Tests/TrailingSpacer.archive | Bin 156 -> 0 bytes TravisCI.ps1 | 6 - appveyor.yml | 22 - azure-pipelines-release.yml | 109 -- src/ArchiveAddition.cs | 5 +- src/ArchiveFactory.cs | 18 +- src/ArchiveFormat.cs | 5 +- src/ArchiveMode.cs | 5 +- src/CompressArchiveCommand.cs | 117 +- src/ErrorMessages.cs | 5 +- src/IArchive.cs | 5 +- src/Microsoft.PowerShell.Archive.psd1 | 4 - src/PathHelper.cs | 7 +- src/TarArchive.cs | 5 +- src/WriteMode.cs | 5 +- src/ZipArchive.cs | 5 +- 27 files changed, 305 insertions(+), 1810 deletions(-) rename .azdevops/{build.yml => ReleaseBuildPipeline.yml} (97%) create mode 100644 .azdevops/SignAndPackageModule.ps1 rename .azdevops/{runtest.yml => TestsTemplate.yml} (100%) delete mode 100644 .azdevops/build.ps1 delete mode 100644 .travis.yml rename SimpleBuild.ps1 => Build.ps1 (83%) delete mode 100644 Tests/Pester.Commands.Cmdlets.Archive.Tests.ps1 delete mode 100644 Tests/SamplePreCreatedArchive.archive delete mode 100644 Tests/TrailingSpacer.archive delete mode 100644 TravisCI.ps1 delete mode 100644 appveyor.yml delete mode 100644 azure-pipelines-release.yml diff --git a/.azdevops/CI.yml b/.azdevops/CI.yml index 9cd5641..cb955ad 100644 --- a/.azdevops/CI.yml +++ b/.azdevops/CI.yml @@ -35,12 +35,8 @@ stages: includePreviewVersions: true - pwsh: | - & $(Build.SourcesDirectory)\SimpleBuild.ps1 + & "$(Build.SourcesDirectory)\SimpleBuild.ps1" displayName: Build Microsoft.PowerShell.Archive module - - - pwsh: | - dir "$(BuildOutDir)/*" -Recurse - displayName: Show BuildOutDirectory - task: CopyFiles@2 displayName: 'Copy build' @@ -59,19 +55,19 @@ stages: dependsOn: Build displayName: Run tests jobs: - - template: runtest.yml + - template: TestsTemplate.yml parameters: vmImageName: windows-2019 jobName: run_test_windows jobDisplayName: Run Windows tests - - template: runtest.yml + - template: TestsTemplate.yml parameters: vmImageName: ubuntu-latest jobName: run_test_linux jobDisplayName: Run Linux tests - - template: runtest.yml + - template: TestsTemplate.yml parameters: vmImageName: macos-latest jobName: run_test_macos diff --git a/.azdevops/build.yml b/.azdevops/ReleaseBuildPipeline.yml similarity index 97% rename from .azdevops/build.yml rename to .azdevops/ReleaseBuildPipeline.yml index 36451cb..cf05994 100644 --- a/.azdevops/build.yml +++ b/.azdevops/ReleaseBuildPipeline.yml @@ -42,7 +42,7 @@ stages: displayName: Build Microsoft.PowerShell.Archive module - pwsh: | - dir "$(BuildOutDir)\*" -Recurse + Get-ChildItem "$(BuildOutDir)\*" -Recurse | Write-Verbose -Verbose displayName: Show BuildOutDirectory - pwsh: | @@ -99,7 +99,7 @@ stages: Set-Location "$(Build.SourcesDirectory)" # signOutPath points to directory with version number -- we want to point to the parent of that directory $ModulePath = Split-Path $(signOutPath) -Parent - $(Build.SourcesDirectory)/Microsoft.PowerShell.Archive/.azdevops/build.ps1 -package -CopySBOM -signed -SignedPath $ModulePath + $(Build.SourcesDirectory)/Microsoft.PowerShell.Archive/.azdevops/SignAndPackageModule.ps1 -SignedPath $ModulePath Get-ChildItem -recurse -file -name | Write-Verbose -Verbose displayName: package build diff --git a/.azdevops/RunTests.ps1 b/.azdevops/RunTests.ps1 index 9f76f2b..44198cf 100644 --- a/.azdevops/RunTests.ps1 +++ b/.azdevops/RunTests.ps1 @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + # Load the module $module = Get-Module -Name "Microsoft.PowerShell.Archive" if ($null -ne $module) @@ -8,7 +11,7 @@ if ($null -ne $module) # Import the built module Import-Module "$env:PIPELINE_WORKSPACE/ModuleBuild/Microsoft.PowerShell.Archive.psd1" -$pesterRequiredVersion = "5.3.3" +$pesterRequiredVersion = "5.3" # If Pester 5.3.3 is not installed, install it $shouldInstallPester = $true @@ -37,11 +40,4 @@ Write-Host "##vso[artifact.upload containerfolder=testResults;artifactname=testR if(!$results -or $results.FailedCount -gt 0 -or !$results.TotalCount) { throw "Build or tests failed. Passed: $($results.PassedCount) Failed: $($results.FailedCount) Total: $($results.TotalCount)" -} - -# Unload module -$module = Get-Module -Name "Microsoft.PowerShell.Archive" -if ($null -ne $module) -{ - Remove-Module $module -} +} \ No newline at end of file diff --git a/.azdevops/SignAndPackageModule.ps1 b/.azdevops/SignAndPackageModule.ps1 new file mode 100644 index 0000000..06427ac --- /dev/null +++ b/.azdevops/SignAndPackageModule.ps1 @@ -0,0 +1,44 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +[CmdletBinding(SupportsShouldProcess=$true)] +param ( + [string]$SignedPath + ) + + +$root = (Resolve-Path -Path "${PSScriptRoot}/../")[0] +$Name = "Microsoft.PowerShell.Archive" +$BuildOutputDir = Join-Path $root "\src\bin\Release" +$ManifestPath = "${BuildOutputDir}\${Name}.psd1" +$ManifestData = Import-PowerShellDataFile -Path $ManifestPath +$Version = $ManifestData.ModuleVersion + +# this takes the files for the module and publishes them to a created, local repository +# so the nupkg can be used to publish to the PSGallery +function Export-Module +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] + param() + $packageRoot = $SignedPath + + if ( -not (Test-Path $packageRoot)) { + throw "'$PubDir' does not exist" + } + + # now constuct a nupkg by registering a local repository and calling publish module + $repoName = [guid]::newGuid().ToString("N") + Register-PSRepository -Name $repoName -SourceLocation $packageRoot -InstallationPolicy Trusted + Publish-Module -Path $packageRoot -Repository $repoName + Unregister-PSRepository -Name $repoName + Get-ChildItem -Recurse -Name $packageRoot | Write-Verbose + $nupkgName = "{0}.{1}-preview1.nupkg" -f ${Name},${Version} + $nupkgPath = Join-Path $packageRoot $nupkgName + if ($env:TF_BUILD) { + # In Azure DevOps + Write-Host "##vso[artifact.upload containerfolder=$nupkgName;artifactname=$nupkgName;]$nupkgPath" + } +} + +# The SBOM should already be in -SignedPath, so there is no need to copy it + +Export-Module diff --git a/.azdevops/runtest.yml b/.azdevops/TestsTemplate.yml similarity index 100% rename from .azdevops/runtest.yml rename to .azdevops/TestsTemplate.yml diff --git a/.azdevops/build.ps1 b/.azdevops/build.ps1 deleted file mode 100644 index 0c9a0ff..0000000 --- a/.azdevops/build.ps1 +++ /dev/null @@ -1,105 +0,0 @@ -[CmdletBinding(SupportsShouldProcess=$true)] -param ( - [switch]$test, - [switch]$build, - [switch]$publish, - [switch]$signed, - [switch]$package, - [switch]$coverage, - [switch]$CopySBOM, - [string]$SignedPath - ) - - -$root = (Resolve-Path -Path "${PSScriptRoot}/../")[0] -$Name = "Microsoft.PowerShell.Archive" -$BuildOutputDir = Join-Path $root "\src\bin\Release" -$ManifestPath = "${BuildOutputDir}\${Name}.psd1" -$ManifestData = Import-PowerShellDataFile -Path $ManifestPath -$Version = $ManifestData.ModuleVersion - -$SignRoot = "${root}\signed\${Name}" -$SignVersion = "$SignRoot\$Version" - -$PubBase = "${root}\out" -$PubRoot = "${PubBase}\${Name}" -$PubDir = "${PubRoot}\${Version}" - -if (-not $test -and -not $build -and -not $publish -and -not $package) { - throw "must use 'build', 'test', 'publish', 'package'" -} - -[bool]$verboseValue = $PSBoundParameters['Verbose'].IsPresent ? $PSBoundParameters['Verbose'].ToBool() : $false - -$FileManifest = @( - @{ SRC = "${$BuildOutputDir}"; NAME = "Microsoft.PowerShell.Archive.dll"; SIGN = $true ; DEST = "OUTDIR" } - @{ SRC = "${$BuildOutputDir}"; NAME = "Microsoft.PowerShell.Archive.psm1"; SIGN = $true ; DEST = "OUTDIR" } -) - -if ($build) { - Write-Verbose -Verbose -Message "No action for build" -} - -# this takes the files for the module and publishes them to a created, local repository -# so the nupkg can be used to publish to the PSGallery -function Export-Module -{ - [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] - param() - if ( $signed ) { - $packageRoot = $SignedPath - } - else { - $packageRoot = $PubRoot - } - - if ( -not (test-path $packageRoot)) { - throw "'$PubDir' does not exist" - } - - # now constuct a nupkg by registering a local repository and calling publish module - $repoName = [guid]::newGuid().ToString("N") - Register-PSRepository -Name $repoName -SourceLocation $packageRoot -InstallationPolicy Trusted - Publish-Module -Path $packageRoot -Repository $repoName - Unregister-PSRepository -Name $repoName - Get-ChildItem -Recurse -Name $packageRoot | Write-Verbose - $nupkgName = "{0}.{1}-preview1.nupkg" -f ${Name},${Version} - $nupkgPath = Join-Path $packageRoot $nupkgName - if ($env:TF_BUILD) { - # In Azure DevOps - Write-Host "##vso[artifact.upload containerfolder=$nupkgName;artifactname=$nupkgName;]$nupkgPath" - } -} - -if ($publish) { - Write-Verbose "Publishing to '$PubDir'" - if (-not (test-path $PubDir)) { - $null = New-Item -ItemType Directory $PubDir -Force - } - foreach ($file in $FileManifest) { - if ($signed -and $file.SIGN) { - $src = Join-Path -Path $PSScriptRoot -AdditionalChildPath $file.NAME -ChildPath signed - } - else { - $src = Join-Path -Path $file.SRC -ChildPath $file.NAME - } - $targetDir = $file.DEST -creplace "OUTDIR","$PubDir" - if (-not (Test-Path $src)) { - throw ("file '" + $src + "' not found") - } - if (-not (Test-Path $targetDir)) { - $null = New-Item -ItemType Directory $targetDir -Force - } - Copy-Item -Path $src -destination $targetDir -Verbose:$verboseValue - - } -} - -# this copies the manifest before creating the module nupkg -# if -CopySBOM is used. -if ($package) { - if($CopySBOM) { - #Copy-Item -Recurse -Path "signed/_manifest" -Destination $SignVersion - } - Export-Module -} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7c69a18..0000000 --- a/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ -language: csharp - -git: - depth: 1000 - -os: - - linux -sudo: required -dist: trusty - -matrix: - fast_finish: true - -install: - - git clone https://github.com/PowerShell/PowerShell.git - - pushd PowerShell/tools - - ./install-powershell.sh - - popd - -script: - - ulimit -n 4096 - - pwsh -File ./TravisCI.ps1 diff --git a/SimpleBuild.ps1 b/Build.ps1 similarity index 83% rename from SimpleBuild.ps1 rename to Build.ps1 index 1d89ba8..f16b5b0 100644 --- a/SimpleBuild.ps1 +++ b/Build.ps1 @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + $buildOutputDirectory = "$PSScriptRoot\src\bin\Release" if ((Test-Path $buildOutputDirectory)) { @@ -7,7 +10,7 @@ if ((Test-Path $buildOutputDirectory)) { # Perform dotnet build dotnet build "$PSScriptRoot\src\Microsoft.PowerShell.Archive.csproj" -c Release -"Build module location: $buildOutputDirectory" | Write-Verbose -Verbose +"Build module location: $buildOutputDirectory" | Write-Verbose -Verbose # Get module version $ManifestData = Import-PowerShellDataFile -Path "$buildOutputDirectory\Microsoft.PowerShell.Archive.psd1" diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index 5e98bc0..20c8979 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -1,22 +1,12 @@ -<############################################################################################ - # File: Compress-Archive.Tests.ps1 - ############################################################################################> - $script:TestSourceRoot = $PSScriptRoot - $DS = [System.IO.Path]::DirectorySeparatorChar - if ($IsWindows -eq $null) { - $IsWindows = $PSVersionTable.PSEdition -eq "Desktop" - } - +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + Describe("Microsoft.PowerShell.Archive tests") { BeforeAll { - $DS = [System.IO.Path]::DirectorySeparatorChar - $originalProgressPref = $ProgressPreference $ProgressPreference = "SilentlyContinue" $originalPSModulePath = $env:PSModulePath - # make sure we use the one in this repo - $env:PSModulePath = "$($script:TestSourceRoot)\..;$($env:PSModulePath)" # Add compression assemblies function Add-CompressionAssemblies { @@ -38,11 +28,19 @@ param ( [string] $archivePath, - [string[]] $expectedEntries + [string[]] $expectedEntries, + [switch] $Literal ) try { + if ($Literal) { + $archivePath = Convert-Path -LiteralPath $archivePath + } else { + $archivePath = Convert-Path -Path $archivePath + } + + $archiveFileStreamArgs = @($archivePath, [System.IO.FileMode]::Open) $archiveFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList $archiveFileStreamArgs @@ -102,116 +100,74 @@ Context "Parameter set validation tests" { BeforeAll { - function CompressArchivePathParameterSetValidator { - param - ( - [string[]] $path, - [string] $destinationPath - ) - - try - { - Compress-Archive -Path $path -DestinationPath $destinationPath - throw "ValidateNotNullOrEmpty attribute is missing on one of parameters belonging to Path parameterset." - } - catch - { - $_.FullyQualifiedErrorId | Should -Be "ParameterArgumentValidationError,Microsoft.PowerShell.Archive.CompressArchiveCommand" - } + # Set up files for tests + New-Item TestDrive:/SourceDir -Type Directory + "Some Data" | Out-File -FilePath TestDrive:/SourceDir/Sample-1.txt + New-Item TestDrive:/EmptyDirectory -Type Directory | Out-Null + } + + + It "Validate errors from Compress-Archive with null and empty values for Path, LiteralPath, and DestinationPath parameters" -ForEach @( + @{ Path = $null; DestinationPath = "TestDrive:/archive1.zip" } + @{ Path = "TestDrive:/SourceDir"; DestinationPath = $null } + @{ Path = $null; DestinationPath = $null } + @{ Path = ""; DestinationPath = "TestDrive:/archive1.zip" } + @{ Path = "TestDrive:/SourceDir"; DestinationPath = "" } + @{ Path = ""; DestinationPath = "" } + ) { + try + { + Compress-Archive -Path $Path -DestinationPath $DestinationPath + throw "ValidateNotNullOrEmpty attribute is missing on one of parameters belonging to LiteralPath parameterset." } - - function CompressArchiveLiteralPathParameterSetValidator { - param - ( - [string[]] $literalPath, - [string] $destinationPath, - [string] $compressionLevel = "Optimal" - ) - - try - { - Compress-Archive -LiteralPath $literalPath -DestinationPath $destinationPath - throw "ValidateNotNullOrEmpty attribute is missing on one of parameters belonging to LiteralPath parameterset." - } - catch - { - $_.FullyQualifiedErrorId | Should -Be "ParameterArgumentValidationError,Microsoft.PowerShell.Archive.CompressArchiveCommand" - } + catch + { + $_.FullyQualifiedErrorId | Should -Be "ParameterArgumentValidationError,Microsoft.PowerShell.Archive.CompressArchiveCommand" } - - - function CompressArchiveInvalidPathValidator { - param - ( - [string[]] $path, - [string] $destinationPath, - [string] $invalidPath, - [string] $expectedFullyQualifiedErrorId - ) - - try + + try + { + Compress-Archive -LiteralPath $Path -DestinationPath $DestinationPath + throw "ValidateNotNullOrEmpty attribute is missing on one of parameters belonging to LiteralPath parameterset." + } + catch + { + $_.FullyQualifiedErrorId | Should -Be "ParameterArgumentValidationError,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } + + It "Validate errors from Compress-Archive when invalid path (non-existing path / non-filesystem path) is supplied for Path or LiteralPath parameters" -ForEach @( + @{ Path = "TestDrive:/InvalidPath" } + @{ Path = @("TestDrive:/", "TestDrive:/InvalidPath") } + ) { + $DestinationPath = "TestDrive:/archive2.zip" + + try { - Compress-Archive -Path $path -DestinationPath $destinationPath - throw "Failed to validate that an invalid Path $invalidPath was supplied as input to Compress-Archive cmdlet." + Compress-Archive -Path $Path -DestinationPath $DestinationPath + throw "Failed to validate that an invalid Path was supplied as input to Compress-Archive cmdlet." } catch { - $_.FullyQualifiedErrorId | Should -Be $expectedFullyQualifiedErrorId + $_.FullyQualifiedErrorId | Should -Be "PathNotFound,Microsoft.PowerShell.Archive.CompressArchiveCommand" } try { - Compress-Archive -LiteralPath $path -DestinationPath $destinationPath - throw "Failed to validate that an invalid LiteralPath $invalidPath was supplied as input to Compress-Archive cmdlet." + Compress-Archive -LiteralPath $Path -DestinationPath $DestinationPath + throw "Failed to validate that an invalid LiteralPath was supplied as input to Compress-Archive cmdlet." } catch { - $_.FullyQualifiedErrorId | Should -Be $expectedFullyQualifiedErrorId + $_.FullyQualifiedErrorId | Should -Be "PathNotFound,Microsoft.PowerShell.Archive.CompressArchiveCommand" } - } - - # Set up files for tests - New-Item $TestDrive$($DS)SourceDir -Type Directory | Out-Null - $content = "Some Data" - $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)Sample-1.txt - - New-Item $TestDrive$($DS)EmptyDirectory -Type Directory | Out-Null - } - - - It "Validate errors from Compress-Archive with NULL & EMPTY values for Path, LiteralPath, DestinationPath, CompressionLevel parameters" { - $sourcePath = "$TestDrive$($DS)SourceDir" - $destinationPath = "$TestDrive$($DS)SampleSingleFile.zip" - - CompressArchivePathParameterSetValidator $null $destinationPath - CompressArchivePathParameterSetValidator $sourcePath $null - CompressArchivePathParameterSetValidator $null $null - - CompressArchivePathParameterSetValidator "" $destinationPath - CompressArchivePathParameterSetValidator $sourcePath "" - CompressArchivePathParameterSetValidator "" "" - - CompressArchiveLiteralPathParameterSetValidator $null $destinationPath - CompressArchiveLiteralPathParameterSetValidator $sourcePath $null - CompressArchiveLiteralPathParameterSetValidator $null $null - - CompressArchiveLiteralPathParameterSetValidator "" $destinationPath - CompressArchiveLiteralPathParameterSetValidator $sourcePath "" - CompressArchiveLiteralPathParameterSetValidator "" "" - } - - It "Validate errors from Compress-Archive when invalid path (non-existing path / non-filesystem path) is supplied for Path or LiteralPath parameters" { - CompressArchiveInvalidPathValidator "$TestDrive$($DS)InvalidPath" "$TestDrive($DS)archive.zip" "$TestDrive$($DS)InvalidPath" "PathNotFound,Microsoft.PowerShell.Archive.CompressArchiveCommand" - - $path = @("$TestDrive", "$TestDrive$($DS)InvalidPath") - CompressArchiveInvalidPathValidator $path "$TestDrive($DS)archive.zip" "$TestDrive$($DS)InvalidPath" "PathNotFound,Microsoft.PowerShell.Archive.CompressArchiveCommand" } It "Validate error from Compress-Archive when duplicate paths are supplied as input to Path parameter" { $sourcePath = @( - "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt", - "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt") - $destinationPath = "$TestDrive$($DS)DuplicatePaths.zip" + "TestDrive:/SourceDir/Sample-1.txt", + "TestDrive:/SourceDir/Sample-1.txt") + $destinationPath = "TestDrive:/DuplicatePaths.zip" try { @@ -226,9 +182,9 @@ It "Validate error from Compress-Archive when duplicate paths are supplied as input to LiteralPath parameter" { $sourcePath = @( - "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt", - "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt") - $destinationPath = "$TestDrive$($DS)DuplicatePaths.zip" + "TestDrive:/SourceDir/Sample-1.txt", + "TestDrive:/SourceDir/Sample-1.txt") + $destinationPath = "TestDrive:/DuplicatePaths.zip" try { @@ -243,10 +199,10 @@ ## From 504 It "Validate that Source Path can be at SystemDrive location" -Skip { - $sourcePath = "$env:SystemDrive$($DS)SourceDir" - $destinationPath = "$TestDrive$($DS)SampleFromSystemDrive.zip" + $sourcePath = "$env:SystemDrive/SourceDir" + $destinationPath = "TestDrive:/SampleFromSystemDrive.zip" New-Item $sourcePath -Type Directory | Out-Null # not enough permissions to write to drive root on Linux - "Some Data" | Out-File -FilePath $sourcePath$($DS)SampleSourceFileForArchive.txt + "Some Data" | Out-File -FilePath $sourcePath/SampleSourceFileForArchive.txt try { Compress-Archive -Path $sourcePath -DestinationPath $destinationPath @@ -260,7 +216,7 @@ # This cannot happen in -WriteMode Create because another error will be throw before It "Throws an error when Path and DestinationPath are the same" -Skip { - $sourcePath = "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt" + $sourcePath = "TestDrive:/SourceDir/Sample-1.txt" $destinationPath = $sourcePath try { @@ -273,7 +229,7 @@ } It "Throws an error when Path and DestinationPath are the same and -Update is specified" { - $sourcePath = "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt" + $sourcePath = "TestDrive:/SourceDir/Sample-1.txt" $destinationPath = $sourcePath try { @@ -285,7 +241,7 @@ } It "Throws an error when Path and DestinationPath are the same and -Overwrite is specified" { - $sourcePath = "$TestDrive$($DS)EmptyDirectory" + $sourcePath = "TestDrive:/EmptyDirectory" $destinationPath = $sourcePath try { @@ -297,7 +253,7 @@ } It "Throws an error when LiteralPath and DestinationPath are the same" -Skip { - $sourcePath = "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt" + $sourcePath = "TestDrive:/SourceDir/Sample-1.txt" $destinationPath = $sourcePath try { @@ -309,7 +265,7 @@ } It "Throws an error when LiteralPath and DestinationPath are the same and -Update is specified" { - $sourcePath = "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt" + $sourcePath = "TestDrive:/SourceDir/Sample-1.txt" $destinationPath = $sourcePath try { @@ -321,7 +277,7 @@ } It "Throws an error when LiteralPath and DestinationPath are the same and -Overwrite is specified" { - $sourcePath = "$TestDrive$($DS)EmptyDirectory" + $sourcePath = "TestDrive:/EmptyDirectory" $destinationPath = $sourcePath try { @@ -335,15 +291,15 @@ Context "WriteMode tests" { BeforeAll { - New-Item $TestDrive$($DS)SourceDir -Type Directory | Out-Null + New-Item TestDrive:/SourceDir -Type Directory | Out-Null $content = "Some Data" - $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)Sample-1.txt + $content | Out-File -FilePath TestDrive:/SourceDir/Sample-1.txt } It "Throws a terminating error when an incorrect value is supplied to -WriteMode" { - $sourcePath = "$TestDrive$($DS)SourceDir" - $destinationPath = "$TestDrive$($DS)archive1.zip" + $sourcePath = "TestDrive:/SourceDir" + $destinationPath = "TestDrive:/archive1.zip" try { Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode mode @@ -352,9 +308,9 @@ } } - It "-WriteMode Create works" { - $sourcePath = "$TestDrive$($DS)SourceDir" - $destinationPath = "$TestDrive$($DS)archive1.zip" + It "-WriteMode Create works" -Tag this2 { + $sourcePath = "TestDrive:/SourceDir" + $destinationPath = "TestDrive:/archive1.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath Test-Path $destinationPath Test-ZipArchive $destinationPath @('SourceDir/', 'SourceDir/Sample-1.txt') @@ -363,39 +319,39 @@ Context "Basic functional tests" { BeforeAll { - New-Item $TestDrive$($DS)SourceDir -Type Directory | Out-Null - New-Item $TestDrive$($DS)SourceDir$($DS)ChildDir-1 -Type Directory | Out-Null - New-Item $TestDrive$($DS)SourceDir$($DS)ChildDir-2 -Type Directory | Out-Null - New-Item $TestDrive$($DS)SourceDir$($DS)ChildEmptyDir -Type Directory | Out-Null + New-Item TestDrive:/SourceDir -Type Directory | Out-Null + New-Item TestDrive:/SourceDir/ChildDir-1 -Type Directory | Out-Null + New-Item TestDrive:/SourceDir/ChildDir-2 -Type Directory | Out-Null + New-Item TestDrive:/SourceDir/ChildEmptyDir -Type Directory | Out-Null # create an empty directory - New-Item $TestDrive$($DS)EmptyDir -Type Directory | Out-Null + New-Item TestDrive:/EmptyDir -Type Directory | Out-Null $content = "Some Data" - $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)Sample-1.txt - $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-2.txt - $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)ChildDir-2$($DS)Sample-3.txt + $content | Out-File -FilePath TestDrive:/SourceDir/Sample-1.txt + $content | Out-File -FilePath TestDrive:/SourceDir/ChildDir-1/Sample-2.txt + $content | Out-File -FilePath TestDrive:/SourceDir/ChildDir-2/Sample-3.txt } It "Validate that a single file can be compressed" { - $sourcePath = "$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-2.txt" - $destinationPath = "$TestDrive$($DS)archive1.zip" + $sourcePath = "TestDrive:/SourceDir/ChildDir-1/Sample-2.txt" + $destinationPath = "TestDrive:/archive1.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath $destinationPath | Should -Exist Test-ZipArchive $destinationPath @('Sample-2.txt') } It "Validate that an empty folder can be compressed" { - $sourcePath = "$TestDrive$($DS)EmptyDir" - $destinationPath = "$TestDrive$($DS)archive2.zip" + $sourcePath = "TestDrive:/EmptyDir" + $destinationPath = "TestDrive:/archive2.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath $destinationPath | Should -Exist Test-ZipArchive $destinationPath @('EmptyDir/') } It "Validate a folder containing files, non-empty folders, and empty folders can be compressed" { - $sourcePath = "$TestDrive$($DS)SourceDir" - $destinationPath = "$TestDrive$($DS)archive3.zip" + $sourcePath = "TestDrive:/SourceDir" + $destinationPath = "TestDrive:/archive3.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath $destinationPath | Should -Exist $contents = Get-Descendants -Path $sourcePath @@ -410,30 +366,30 @@ Context "DestinationPath and -WriteMode Overwrite tests" { BeforeAll { - New-Item $TestDrive$($DS)SourceDir -Type Directory | Out-Null + New-Item TestDrive:/SourceDir -Type Directory | Out-Null $content = "Some Data" - $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)Sample-1.txt + $content | Out-File -FilePath TestDrive:/SourceDir/Sample-1.txt - New-Item $TestDrive$($DS)archive3.zip -Type Directory | Out-Null + New-Item TestDrive:/archive3.zip -Type Directory | Out-Null - New-Item $TestDrive$($DS)EmptyDirectory -Type Directory | Out-Null + New-Item TestDrive:/EmptyDirectory -Type Directory | Out-Null # Create a read-only archive - $readOnlyArchivePath = "$TestDrive$($DS)readonly.zip" - Compress-Archive -Path $TestDrive$($DS)SourceDir$($DS)Sample-1.txt -DestinationPath $readOnlyArchivePath + $readOnlyArchivePath = "TestDrive:/readonly.zip" + Compress-Archive -Path TestDrive:/SourceDir/Sample-1.txt -DestinationPath $readOnlyArchivePath Set-ItemProperty -Path $readOnlyArchivePath -Name IsReadOnly -Value $true - # Create $TestDrive$($DS)archive.zip - Compress-Archive -Path $TestDrive$($DS)SourceDir$($DS)Sample-1.txt -DestinationPath "$TestDrive$($DS)archive.zip" + # Create TestDrive:/archive.zip + Compress-Archive -Path TestDrive:/SourceDir/Sample-1.txt -DestinationPath "TestDrive:/archive.zip" # Create Sample-2.txt - $content | Out-File -FilePath $TestDrive$($DS)Sample-2.txt + $content | Out-File -FilePath TestDrive:/Sample-2.txt } It "Throws an error when archive file already exists and -Update and -Overwrite parameters are not specified" { - $sourcePath = "$TestDrive$($DS)SourceDir" - $destinationPath = "$TestDrive$($DS)archive1.zip" + $sourcePath = "TestDrive:/SourceDir" + $destinationPath = "TestDrive:/archive1.zip" try { @@ -448,8 +404,8 @@ } It "Throws a terminating error when archive file exists and -Update is specified but the archive is read-only" { - $sourcePath = "$TestDrive$($DS)SourceDir" - $destinationPath = "$TestDrive$($DS)readonly.zip" + $sourcePath = "TestDrive:/SourceDir" + $destinationPath = "TestDrive:/readonly.zip" try { @@ -463,8 +419,8 @@ } It "Throws a terminating error when archive already exists as a directory and -Update and -Overwrite parameters are not specified" { - $sourcePath = "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt" - $destinationPath = "$TestDrive$($DS)SourceDir" + $sourcePath = "TestDrive:/SourceDir/Sample-1.txt" + $destinationPath = "TestDrive:/SourceDir" try { @@ -478,8 +434,8 @@ } It "Throws a terminating error when DestinationPath is a directory and -Update is specified" { - $sourcePath = "$TestDrive$($DS)SourceDir" - $destinationPath = "$TestDrive$($DS)archive3.zip" + $sourcePath = "TestDrive:/SourceDir" + $destinationPath = "TestDrive:/archive3.zip" try { @@ -493,8 +449,8 @@ } It "Throws a terminating error when DestinationPath is a folder containing at least 1 item and Overwrite is specified" { - $sourcePath = "$TestDrive$($DS)SourceDir" - $destinationPath = "$TestDrive" + $sourcePath = "TestDrive:/SourceDir" + $destinationPath = "TestDrive:" try { @@ -508,8 +464,8 @@ } It "Throws a terminating error when archive does not exist and -Update mode is specified" { - $sourcePath = "$TestDrive$($DS)SourceDir" - $destinationPath = "$TestDrive$($DS)archive2.zip" + $sourcePath = "TestDrive:/SourceDir" + $destinationPath = "TestDrive:/archive2.zip" try { @@ -524,8 +480,8 @@ ## Overwrite tests It "Throws an error when trying to overwrite an empty directory, which is the working directory" { - $sourcePath = "$TestDrive$($DS)Sample-2.txt" - $destinationPath = "$TestDrive$($DS)EmptyDirectory" + $sourcePath = "TestDrive:/Sample-2.txt" + $destinationPath = "TestDrive:/EmptyDirectory" Push-Location $destinationPath @@ -539,8 +495,8 @@ } It "Overwrites a directory containing no items when -Overwrite is specified" { - $sourcePath = "$TestDrive$($DS)SourceDir" - $destinationPath = "$TestDrive$($DS)EmptyDirectory" + $sourcePath = "TestDrive:/SourceDir" + $destinationPath = "TestDrive:/EmptyDirectory" (Get-Item $destinationPath) -is [System.IO.DirectoryInfo] | Should -Be $true Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite @@ -552,14 +508,14 @@ } It "Overwrites an archive that already exists" { - $destinationPath = "$TestDrive$($DS)archive.zip" + $destinationPath = "TestDrive:/archive.zip" # Get the entries of the original zip archive Test-ZipArchive $destinationPath @("Sample-1.txt") # Overwrite the archive - $sourcePath = "$TestDrive$($DS)Sample-2.txt" - Compress-Archive -Path $sourcePath -DestinationPath "$TestDrive$($DS)archive.zip" -WriteMode Overwrite + $sourcePath = "TestDrive:/Sample-2.txt" + Compress-Archive -Path $sourcePath -DestinationPath "TestDrive:/archive.zip" -WriteMode Overwrite # Ensure the original entries and different than the new entries Test-ZipArchive $destinationPath @("Sample-2.txt") @@ -568,21 +524,21 @@ Context "Relative Path tests" { BeforeAll { - New-Item $TestDrive$($DS)SourceDir -Type Directory | Out-Null - New-Item $TestDrive$($DS)SourceDir$($DS)ChildDir-1 -Type Directory | Out-Null + New-Item TestDrive:/SourceDir -Type Directory | Out-Null + New-Item TestDrive:/SourceDir/ChildDir-1 -Type Directory | Out-Null $content = "Some Data" - $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)Sample-1.txt - $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-2.txt + $content | Out-File -FilePath TestDrive:/SourceDir/Sample-1.txt + $content | Out-File -FilePath TestDrive:/SourceDir/ChildDir-1/Sample-2.txt } # From 568 It "Validate that relative path can be specified as Path parameter of Compress-Archive cmdlet" { - $sourcePath = ".$($DS)SourceDir" + $sourcePath = "./SourceDir" $destinationPath = "RelativePathForPathParameter.zip" try { - Push-Location $TestDrive + Push-Location TestDrive:/ Compress-Archive -Path $sourcePath -DestinationPath $destinationPath Test-Path $destinationPath | Should -Be $true } @@ -594,11 +550,11 @@ # From 582 It "Validate that relative path can be specified as LiteralPath parameter of Compress-Archive cmdlet" { - $sourcePath = ".$($DS)SourceDir" + $sourcePath = "./SourceDir" $destinationPath = "RelativePathForLiteralPathParameter.zip" try { - Push-Location $TestDrive + Push-Location TestDrive:/ Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath Test-Path $destinationPath | Should -Be $true } @@ -609,12 +565,12 @@ } # From 596 - It "Validate that relative path can be specified as DestinationPath parameter of Compress-Archive cmdlet" { - $sourcePath = "$TestDrive$($DS)SourceDir" - $destinationPath = ".$($DS)RelativePathForDestinationPathParameter.zip" + It "Validate that relative path can be specified as DestinationPath parameter of Compress-Archive cmdlet" -Tag this3 { + $sourcePath = "TestDrive:/SourceDir" + $destinationPath = "./RelativePathForDestinationPathParameter.zip" try { - Push-Location $TestDrive + Push-Location TestDrive:/ Compress-Archive -Path $sourcePath -DestinationPath $destinationPath Test-Path $destinationPath | Should -Be $true } @@ -627,28 +583,28 @@ Context "Special and Wildcard Characters Tests" { BeforeAll { - New-Item $TestDrive$($DS)SourceDir -Type Directory | Out-Null + New-Item TestDrive:/SourceDir -Type Directory | Out-Null $content = "Some Data" - $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)Sample-1.txt + $content | Out-File -FilePath TestDrive:/SourceDir/Sample-1.txt } It "Accepts DestinationPath parameter with wildcard characters that resolves to one path" { - $sourcePath = "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt" - $destinationPath = "$TestDrive$($DS)Sample[]SingleFile.zip" + $sourcePath = "TestDrive:/SourceDir/Sample-1.txt" + $destinationPath = "TestDrive:/Sample[]SingleFile.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath Test-Path -LiteralPath $destinationPath | Should -Be $true Remove-Item -LiteralPath $destinationPath } It "Accepts DestinationPath parameter with [ but no matching ]" { - $sourcePath = "$TestDrive$($DS)SourceDir" - $destinationPath = "$TestDrive$($DS)archive[2.zip" + $sourcePath = "TestDrive:/SourceDir" + $destinationPath = "TestDrive:/archive[2.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath Test-Path -LiteralPath $destinationPath | Should -Be $true - Test-ZipArchive $destinationPath @("SourceDir/", "SourceDir/Sample-1.txt") + Test-ZipArchive $destinationPath @("SourceDir/", "SourceDir/Sample-1.txt") -Literal Remove-Item -LiteralPath $destinationPath } } diff --git a/Tests/Pester.Commands.Cmdlets.Archive.Tests.ps1 b/Tests/Pester.Commands.Cmdlets.Archive.Tests.ps1 deleted file mode 100644 index 7c3c619..0000000 --- a/Tests/Pester.Commands.Cmdlets.Archive.Tests.ps1 +++ /dev/null @@ -1,1258 +0,0 @@ -<############################################################################################ - # File: Pester.Commands.Cmdlets.ArchiveTests.ps1 - # Commands.Cmdlets.ArchiveTests suite contains Tests that are - # used for validating Microsoft.PowerShell.Archive module. - ############################################################################################> -$script:TestSourceRoot = $PSScriptRoot -$DS = [System.IO.Path]::DirectorySeparatorChar -if ($IsWindows -eq $null) { - $IsWindows = $PSVersionTable.PSEdition -eq "Desktop" -} -Describe "Test suite for Microsoft.PowerShell.Archive module" -Tags "BVT" { - - BeforeAll { - $originalProgressPref = $ProgressPreference - $ProgressPreference = "SilentlyContinue" - $originalPSModulePath = $env:PSModulePath - # make sure we use the one in this repo - $env:PSModulePath = "$($script:TestSourceRoot)\..;$($env:PSModulePath)" - - New-Item $TestDrive$($DS)SourceDir -Type Directory | Out-Null - New-Item $TestDrive$($DS)SourceDir$($DS)ChildDir-1 -Type Directory | Out-Null - New-Item $TestDrive$($DS)SourceDir$($DS)ChildDir-2 -Type Directory | Out-Null - New-Item $TestDrive$($DS)SourceDir$($DS)ChildEmptyDir -Type Directory | Out-Null - - $content = "Some Data" - $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)Sample-1.txt - $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)Sample-2.txt - $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-3.txt - $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-4.txt - $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)ChildDir-2$($DS)Sample-5.txt - $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)ChildDir-2$($DS)Sample-6.txt - - "Some Text" > $TestDrive$($DS)Sample.unzip - "Some Text" > $TestDrive$($DS)Sample.cab - - $preCreatedArchivePath = Join-Path $script:TestSourceRoot "SamplePreCreatedArchive.archive" - Copy-Item $preCreatedArchivePath $TestDrive$($DS)SamplePreCreatedArchive.zip -Force - - $preCreatedArchivePath = Join-Path $script:TestSourceRoot "TrailingSpacer.archive" - Copy-Item $preCreatedArchivePath $TestDrive$($DS)TrailingSpacer.zip -Force - } - - AfterAll { - $global:ProgressPreference = $originalProgressPref - $env:PSModulePath = $originalPSModulePath - } - - function Add-CompressionAssemblies { - Add-Type -AssemblyName System.IO.Compression - if ($psedition -eq "Core") - { - Add-Type -AssemblyName System.IO.Compression.ZipFile - } - else - { - Add-Type -AssemblyName System.IO.Compression.FileSystem - } - } - - function CompressArchivePathParameterSetValidator { - param - ( - [string[]] $path, - [string] $destinationPath, - [string] $compressionLevel = "Optimal" - ) - - try - { - Compress-Archive -Path $path -DestinationPath $destinationPath -CompressionLevel $compressionLevel - throw "ValidateNotNullOrEmpty attribute is missing on one of parameters belonging to Path parameterset." - } - catch - { - $_.FullyQualifiedErrorId | Should Be "ParameterArgumentValidationError,Compress-Archive" - } - } - - function CompressArchiveLiteralPathParameterSetValidator { - param - ( - [string[]] $literalPath, - [string] $destinationPath, - [string] $compressionLevel = "Optimal" - ) - - try - { - Compress-Archive -LiteralPath $literalPath -DestinationPath $destinationPath -CompressionLevel $compressionLevel - throw "ValidateNotNullOrEmpty attribute is missing on one of parameters belonging to LiteralPath parameterset." - } - catch - { - $_.FullyQualifiedErrorId | Should Be "ParameterArgumentValidationError,Compress-Archive" - } - } - - - function CompressArchiveInValidPathValidator { - param - ( - [string[]] $path, - [string] $destinationPath, - [string] $invalidPath, - [string] $expectedFullyQualifiedErrorId - ) - - try - { - Compress-Archive -Path $path -DestinationPath $destinationPath - throw "Failed to validate that an invalid Path $invalidPath was supplied as input to Compress-Archive cmdlet." - } - catch - { - $_.FullyQualifiedErrorId | Should Be $expectedFullyQualifiedErrorId - } - } - - function CompressArchiveInValidArchiveFileExtensionValidator { - param - ( - [string[]] $path, - [string] $destinationPath, - [string] $invalidArchiveFileExtension - ) - - try - { - Compress-Archive -Path $path -DestinationPath $destinationPath - throw "Failed to validate that an invalid archive file format $invalidArchiveFileExtension was supplied as input to Compress-Archive cmdlet." - } - catch - { - $_.FullyQualifiedErrorId | Should Be "NotSupportedArchiveFileExtension,Compress-Archive" - } - } - - function Validate-ArchiveEntryCount { - param - ( - [string] $path, - [int] $expectedEntryCount - ) - - Add-CompressionAssemblies - try - { - $archiveFileStreamArgs = @($path, [System.IO.FileMode]::Open) - $archiveFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList $archiveFileStreamArgs - - $zipArchiveArgs = @($archiveFileStream, [System.IO.Compression.ZipArchiveMode]::Read, $false) - $zipArchive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $zipArchiveArgs - - $actualEntryCount = $zipArchive.Entries.Count - $actualEntryCount | Should Be $expectedEntryCount - } - finally - { - if ($null -ne $zipArchive) { $zipArchive.Dispose()} - if ($null -ne $archiveFileStream) { $archiveFileStream.Dispose() } - } - } - - function ArchiveFileEntryContentValidator { - param - ( - [string] $path, - [string] $entryFileName, - [string] $expectedEntryFileContent - ) - - Add-CompressionAssemblies - try - { - $destFile = "$TestDrive$($DS)ExpandedFile"+([System.Guid]::NewGuid().ToString())+".txt" - - $archiveFileStreamArgs = @($path, [System.IO.FileMode]::Open) - $archiveFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList $archiveFileStreamArgs - - $zipArchiveArgs = @($archiveFileStream, [System.IO.Compression.ZipArchiveMode]::Read, $false) - $zipArchive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $zipArchiveArgs - - $entryToBeUpdated = $zipArchive.Entries | ? {$_.FullName -eq $entryFileName.replace([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar)} - - if($entryToBeUpdated -ne $null) - { - $srcStream = $entryToBeUpdated.Open() - $destStream = New-Object "System.IO.FileStream" -ArgumentList( $destFile, [System.IO.FileMode]::Create ) - $srcStream.CopyTo( $destStream ) - $destStream.Dispose() - $srcStream.Dispose() - Get-Content $destFile | Should Be $expectedEntryFileContent - } - else - { - throw "Failed to find the file $entryFileName in the archive file $path" - } - } - finally - { - if ($zipArchive) - { - $zipArchive.Dispose() - } - if ($archiveFileStream) - { - $archiveFileStream.Dispose() - } - } - } - - function ArchiveFileEntrySeparatorValidator { - param - ( - [string] $path - ) - - Add-CompressionAssemblies - try - { - $destFile = "$TestDrive$($DS)ExpandedFile"+([System.Guid]::NewGuid().ToString())+".txt" - - $archiveFileStreamArgs = @($path, [System.IO.FileMode]::Open) - $archiveFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList $archiveFileStreamArgs - - $zipArchiveArgs = @($archiveFileStream, [System.IO.Compression.ZipArchiveMode]::Read, $false) - $zipArchive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $zipArchiveArgs - - $badEntries = $zipArchive.Entries | Where-Object {$_.FullName.Contains('\')} - - $badEntries.Count | Should Be 0 - } - finally - { - if ($zipArchive) - { - $zipArchive.Dispose() - } - if ($archiveFileStream) - { - $archiveFileStream.Dispose() - } - } - } - - function ExpandArchiveInvalidParameterValidator { - param - ( - [boolean] $isLiteralPathParameterSet, - [string[]] $path, - [string] $destinationPath, - [string] $expectedFullyQualifiedErrorId - ) - - try - { - if($isLiteralPathParameterSet) - { - Expand-Archive -LiteralPath $literalPath -DestinationPath $destinationPath - } - else - { - Expand-Archive -Path $path -DestinationPath $destinationPath - } - - throw "Expand-Archive did NOT throw expected error" - } - catch - { - $_.FullyQualifiedErrorId | Should Be $expectedFullyQualifiedErrorId - } - } - - Context "Compress-Archive - Parameter validation test cases" { - - It "Validate errors from Compress-Archive with NULL & EMPTY values for Path, LiteralPath, DestinationPath, CompressionLevel parameters" { - $sourcePath = "$TestDrive$($DS)SourceDir" - $destinationPath = "$TestDrive$($DS)SampleSingleFile.zip" - - CompressArchivePathParameterSetValidator $null $destinationPath - CompressArchivePathParameterSetValidator $sourcePath $null - CompressArchivePathParameterSetValidator $null $null - - CompressArchivePathParameterSetValidator "" $destinationPath - CompressArchivePathParameterSetValidator $sourcePath "" - CompressArchivePathParameterSetValidator "" "" - - CompressArchivePathParameterSetValidator $null $null "NoCompression" - - CompressArchiveLiteralPathParameterSetValidator $null $destinationPath - CompressArchiveLiteralPathParameterSetValidator $sourcePath $null - CompressArchiveLiteralPathParameterSetValidator $null $null - - CompressArchiveLiteralPathParameterSetValidator "" $destinationPath - CompressArchiveLiteralPathParameterSetValidator $sourcePath "" - CompressArchiveLiteralPathParameterSetValidator "" "" - - CompressArchiveLiteralPathParameterSetValidator $null $null "NoCompression" - - CompressArchiveLiteralPathParameterSetValidator $sourcePath $destinationPath $null - CompressArchiveLiteralPathParameterSetValidator $sourcePath $destinationPath "" - } - - It "Validate errors from Compress-Archive when invalid path (non-existing path / non-filesystem path) is supplied for Path or LiteralPath parameters" { - CompressArchiveInValidPathValidator "$TestDrive$($DS)InvalidPath" $TestDrive "$TestDrive$($DS)InvalidPath" "ArchiveCmdletPathNotFound,Compress-Archive" - CompressArchiveInValidPathValidator "$TestDrive" "$TestDrive$($DS)NonExistingDirectory$($DS)sample.zip" "$TestDrive$($DS)NonExistingDirectory$($DS)sample.zip" "ArchiveCmdletPathNotFound,Compress-Archive" - - $path = @("$TestDrive", "$TestDrive$($DS)InvalidPath") - CompressArchiveInValidPathValidator $path $TestDrive "$TestDrive$($DS)InvalidPath" "ArchiveCmdletPathNotFound,Compress-Archive" - - # The tests below are no longer valid. You can have zip files with non-zip extensions. Different archive - # formats should be added in a separate pull request, with a parameter to identify the archive format, and - # default formats associated with specific extensions. Until then, as long as these cmdlets only support - # Zip files, any file extension is supported. - - #$invalidUnZipFileFormat = "$TestDrive$($DS)Sample.unzip" - #CompressArchiveInValidArchiveFileExtensionValidator $TestDrive "$invalidUnZipFileFormat" ".unzip" - - #$invalidcabZipFileFormat = "$TestDrive$($DS)Sample.cab" - #CompressArchiveInValidArchiveFileExtensionValidator $TestDrive "$invalidcabZipFileFormat" ".cab" - } - - It "Validate error from Compress-Archive when archive file already exists and -Update parameter is not specified" { - $sourcePath = "$TestDrive$($DS)SourceDir" - $destinationPath = "$TestDrive$($DS)ValidateErrorWhenUpdateNotSpecified.zip" - - try - { - "Some Data" > $destinationPath - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - throw "Failed to validate that an archive file format $destinationPath already exists and -Update switch parameter is not specified while running Compress-Archive command." - } - catch - { - $_.FullyQualifiedErrorId | Should Be "ArchiveFileExists,Compress-Archive" - } - } - - It "Validate error from Compress-Archive when duplicate paths are supplied as input to Path parameter" { - $sourcePath = @( - "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt", - "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt") - $destinationPath = "$TestDrive$($DS)DuplicatePaths.zip" - - try - { - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - throw "Failed to detect that duplicate Path $sourcePath is supplied as input to Path parameter." - } - catch - { - $_.FullyQualifiedErrorId | Should Be "DuplicatePathFound,Compress-Archive" - } - } - - It "Validate error from Compress-Archive when duplicate paths are supplied as input to LiteralPath parameter" { - $sourcePath = @( - "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt", - "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt") - $destinationPath = "$TestDrive$($DS)DuplicatePaths.zip" - - try - { - Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath - throw "Failed to detect that duplicate Path $sourcePath is supplied as input to LiteralPath parameter." - } - catch - { - $_.FullyQualifiedErrorId | Should Be "DuplicatePathFound,Compress-Archive" - } - } - } - - Context "Compress-Archive - functional test cases" { - It "Validate that a single file can be compressed using Compress-Archive cmdlet" { - $sourcePath = "$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-3.txt" - $destinationPath = "$TestDrive$($DS)SampleSingleFile.zip" - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - $destinationPath | Should Exist - } - # This test requires a fix in PS5 to support reading paths with square bracket - It "Validate that Compress-Archive cmdlet can accept LiteralPath parameter with Special Characters" -skip:(($PSVersionTable.psversion.Major -lt 5) -and ($PSVersionTable.psversion.Minor -lt 0)) { - $sourcePath = "$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample[]File.txt" - "Some Random Content" | Out-File -LiteralPath $sourcePath - $destinationPath = "$TestDrive$($DS)SampleSingleFileWithSpecialCharacters.zip" - try - { - Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath - $destinationPath | Should Exist - } - finally - { - Remove-Item -LiteralPath $sourcePath -Force - } - } - It "Validate that Compress-Archive cmdlet errors out when DestinationPath resolves to multiple locations" { - - New-Item $TestDrive$($DS)SampleDir$($DS)Child-1 -Type Directory -Force | Out-Null - New-Item $TestDrive$($DS)SampleDir$($DS)Child-2 -Type Directory -Force | Out-Null - New-Item $TestDrive$($DS)SampleDir$($DS)Test.txt -Type File -Force | Out-Null - - $destinationPath = "$TestDrive$($DS)SampleDir$($DS)Child-*$($DS)SampleChidArchive.zip" - $sourcePath = "$TestDrive$($DS)SampleDir$($DS)Test.txt" - try - { - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - throw "Failed to detect that destination $destinationPath can resolve to multiple paths" - } - catch - { - $_.FullyQualifiedErrorId | Should Be "InvalidArchiveFilePath,Compress-Archive" - } - finally - { - Remove-Item -LiteralPath $TestDrive$($DS)SampleDir -Force -Recurse - } - } - It "Validate that Compress-Archive cmdlet works when DestinationPath has wild card pattern and resolves to a single valid path" { - - New-Item $TestDrive$($DS)SampleDir$($DS)Child-1 -Type Directory -Force | Out-Null - New-Item $TestDrive$($DS)SampleDir$($DS)Test.txt -Type File -Force | Out-Null - - $destinationPath = "$TestDrive$($DS)SampleDir$($DS)Child-*$($DS)SampleChidArchive.zip" - $sourcePath = "$TestDrive$($DS)SampleDir$($DS)Test.txt" - try - { - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - $destinationPath | Should Exist - } - finally - { - Remove-Item -LiteralPath $TestDrive$($DS)SampleDir -Force -Recurse - } - } - It "Validate that Compress-Archive cmdlet works when it ecounters LastWriteTimeValues earlier than 1980" { - New-Item $TestDrive$($DS)SampleDir$($DS)Child-1 -Type Directory -Force | Out-Null - $file = New-Item $TestDrive$($DS)SampleDir$($DS)Test.txt -Type File -Force - $destinationPath = "$TestDrive$($DS)SampleDir$($DS)Child-*$($DS)SampleChidArchive.zip" - $sourcePath = "$TestDrive$($DS)SampleDir$($DS)Test.txt" - - $file.LastWriteTime = [DateTime]::Parse('1967-03-04T06:00:00') - try - { - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WarningAction SilentlyContinue - $destinationPath | Should Exist - } - finally - { - Remove-Item -LiteralPath $TestDrive$($DS)SampleDir -Force -Recurse - } - } - It "Validate that Compress-Archive cmdlet warns when updating the LastWriteTime for files earlier than 1980" { - New-Item $TestDrive$($DS)SampleDir$($DS)Child-1 -Type Directory -Force | Out-Null - $file = New-Item $TestDrive$($DS)SampleDir$($DS)Test.txt -Type File -Force - $destinationPath = "$TestDrive$($DS)SampleDir$($DS)Child-*$($DS)SampleChidArchive.zip" - $sourcePath = "$TestDrive$($DS)SampleDir$($DS)Test.txt" - - $file.LastWriteTime = [DateTime]::Parse('1967-03-04T06:00:00') - try - { - $ps=[PowerShell]::Create() - $ps.Streams.Warning.Clear() - $script = "Import-Module Microsoft.PowerShell.Archive; Compress-Archive -Path $sourcePath -DestinationPath `"$destinationPath`" -CompressionLevel Fastest -Verbose" - $ps.AddScript($script) - $ps.Invoke() - - $ps.Streams.Warning.Count -gt 0 | Should Be $True - } - finally - { - Remove-Item -LiteralPath $TestDrive$($DS)SampleDir -Force -Recurse - } - } - - # This test requires a fix in PS5 to support reading paths with square bracket - It "Validate that Compress-Archive cmdlet can accept LiteralPath parameter for a directory with Special Characters in the directory name" -skip:(($PSVersionTable.psversion.Major -lt 5) -and ($PSVersionTable.psversion.Minor -lt 0)) { - $sourcePath = "$TestDrive$($DS)Source[]Dir$($DS)ChildDir[]-1" - New-Item $sourcePath -Type Directory | Out-Null - "Some Random Content" | Out-File -LiteralPath "$sourcePath$($DS)Sample[]File.txt" - $destinationPath = "$TestDrive$($DS)SampleDirWithSpecialCharacters.zip" - try - { - Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath - $destinationPath | Should Exist - } - finally - { - Remove-Item -LiteralPath $sourcePath -Force -Recurse - } - } - It "Validate that Compress-Archive cmdlet can accept DestinationPath parameter with Special Characters" { - $sourcePath = "$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-3.txt" - $destinationPath = "$TestDrive$($DS)Sample[]SingleFile.zip" - try - { - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - Test-Path -LiteralPath $destinationPath | Should Be $true - } - finally - { - Remove-Item -LiteralPath $destinationPath -Force - } - } - It "Validate that Source Path can be at SystemDrive location" -skip:(!$IsWindows) { - $sourcePath = "$env:SystemDrive$($DS)SourceDir" - $destinationPath = "$TestDrive$($DS)SampleFromSystemDrive.zip" - New-Item $sourcePath -Type Directory | Out-Null # not enough permissions to write to drive root on Linux - "Some Data" | Out-File -FilePath $sourcePath$($DS)SampleSourceFileForArchive.txt - try - { - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - Test-Path $destinationPath | Should Be $true - } - finally - { - del "$sourcePath" -Force -Recurse -ErrorAction SilentlyContinue - } - } - It "Validate that multiple files can be compressed using Compress-Archive cmdlet" { - $sourcePath = @( - "$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-3.txt", - "$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-4.txt", - "$TestDrive$($DS)SourceDir$($DS)ChildDir-2$($DS)Sample-5.txt", - "$TestDrive$($DS)SourceDir$($DS)ChildDir-2$($DS)Sample-6.txt") - $destinationPath = "$TestDrive$($DS)SampleMultipleFiles.zip" - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - Test-Path $destinationPath | Should Be $true - } - It "Validate that multiple files and directories can be compressed using Compress-Archive cmdlet" { - $sourcePath = @( - "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt", - "$TestDrive$($DS)SourceDir$($DS)Sample-2.txt", - "$TestDrive$($DS)SourceDir$($DS)ChildDir-1", - "$TestDrive$($DS)SourceDir$($DS)ChildDir-2") - $destinationPath = "$TestDrive$($DS)SampleMultipleFilesAndDirs.zip" - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - Test-Path $destinationPath | Should Be $true - } - It "Validate that a single directory can be compressed using Compress-Archive cmdlet" { - $sourcePath = @("$TestDrive$($DS)SourceDir$($DS)ChildDir-1") - $destinationPath = "$TestDrive$($DS)SampleSingleDir.zip" - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - Test-Path $destinationPath | Should Be $true - } - It "Validate that a single directory with multiple files and subdirectories can be compressed using Compress-Archive cmdlet" { - $sourcePath = @("$TestDrive$($DS)SourceDir") - $destinationPath = "$TestDrive$($DS)SampleSubTree.zip" - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - Test-Path $destinationPath | Should Be $true - } - It "Validate that a single directory & multiple files can be compressed using Compress-Archive cmdlet" { - $sourcePath = @( - "$TestDrive$($DS)SourceDir$($DS)ChildDir-1", - "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt", - "$TestDrive$($DS)SourceDir$($DS)Sample-2.txt") - $destinationPath = "$TestDrive$($DS)SampleMultipleFilesAndSingleDir.zip" - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - Test-Path $destinationPath | Should Be $true - } - - It "Validate that if .zip extension is not supplied as input to DestinationPath parameter, then .zip extension is appended" { - $sourcePath = @("$TestDrive$($DS)SourceDir") - $destinationPath = "$TestDrive$($DS)SampleNoExtension.zip" - $destinationWithoutExtensionPath = "$TestDrive$($DS)SampleNoExtension" - Compress-Archive -Path $sourcePath -DestinationPath $destinationWithoutExtensionPath - Test-Path $destinationPath | Should Be $true - } - It "Validate that relative path can be specified as Path parameter of Compress-Archive cmdlet" { - $sourcePath = ".$($DS)SourceDir" - $destinationPath = "RelativePathForPathParameter.zip" - try - { - Push-Location $TestDrive - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - Test-Path $destinationPath | Should Be $true - } - finally - { - Pop-Location - } - } - It "Validate that relative path can be specified as LiteralPath parameter of Compress-Archive cmdlet" { - $sourcePath = ".$($DS)SourceDir" - $destinationPath = "RelativePathForLiteralPathParameter.zip" - try - { - Push-Location $TestDrive - Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath - Test-Path $destinationPath | Should Be $true - } - finally - { - Pop-Location - } - } - It "Validate that relative path can be specified as DestinationPath parameter of Compress-Archive cmdlet" { - $sourcePath = "$TestDrive$($DS)SourceDir" - $destinationPath = ".$($DS)RelativePathForDestinationPathParameter.zip" - try - { - Push-Location $TestDrive - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - Test-Path $destinationPath | Should Be $true - } - finally - { - Pop-Location - } - } - It "Validate that -Update parameter makes Compress-Archive to not throw an error if archive file already exists" { - $sourcePath = @("$TestDrive$($DS)SourceDir") - $destinationPath = "$TestDrive$($DS)SampleUpdateTest.zip" - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - Test-Path $destinationPath | Should Be $true - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Update - Test-Path $destinationPath | Should Be $true - } - It "Validate -Update parameter by adding a new file to an existing archive file" { - $sourcePath = @("$TestDrive$($DS)SourceDir$($DS)ChildDir-1") - $destinationPath = "$TestDrive$($DS)SampleUpdateAdd1File.zip" - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - Test-Path $destinationPath | Should Be $true - New-Item $TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-AddedNewFile.txt -Type File | Out-Null - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Update - Test-Path $destinationPath | Should Be $true - Validate-ArchiveEntryCount -path $destinationPath -expectedEntryCount 3 - } - - It "Validate that all CompressionLevel values can be used with Compress-Archive cmdlet" { - $sourcePath = "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt" - - $destinationPath = "$TestDrive$($DS)FastestCompressionLevel.zip" - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -CompressionLevel Fastest - Test-Path $destinationPath | Should Be $true - - $destinationPath = "$TestDrive$($DS)OptimalCompressionLevel.zip" - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -CompressionLevel Optimal - Test-Path $destinationPath | Should Be $true - - $destinationPath = "$TestDrive$($DS)NoCompressionCompressionLevel.zip" - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -CompressionLevel NoCompression - Test-Path $destinationPath | Should Be $true - } - - It "Validate that -Update parameter is modifying a file that already exists in the archive file" { - $filePath = "$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-3.txt" - - $initialContent = "Initial Content" - $modifiedContent = "Modified Content" - - $initialContent | Set-Content $filePath - - $sourcePath = "$TestDrive$($DS)SourceDir" - $destinationPath = "$TestDrive$($DS)UpdatingModifiedFile.zip" - - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - Test-Path $destinationPath | Should Be $True - - $modifiedContent | Set-Content $filePath - - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Update - Test-Path $destinationPath | Should Be $True - - ArchiveFileEntryContentValidator "$destinationPath" "SourceDir$($DS)ChildDir-1$($DS)Sample-3.txt" $modifiedContent - } - - It "Validate that only / separators are used as archive directory separators" { - $filePath = "$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-3.txt" - - $initialContent = "Initial Content" - $modifiedContent = "Modified Content" - - $initialContent | Set-Content $filePath - - $sourcePath = "$TestDrive$($DS)SourceDir" - $destinationPath = "$TestDrive$($DS)VerifyingSeparators.zip" - - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - Test-Path $destinationPath | Should Be $True - - ArchiveFileEntrySeparatorValidator "$destinationPath" - } - - It "Validate Compress-Archive cmdlet in pipleline scenario" { - $destinationPath = "$TestDrive$($DS)CompressArchiveFromPipeline.zip" - - # Piping a single file path to Compress-Archive - dir -Path $TestDrive$($DS)SourceDir$($DS)Sample-1.txt | Compress-Archive -DestinationPath $destinationPath - Test-Path $destinationPath | Should Be $True - - # Piping a string directory path to Compress-Archive - "$TestDrive$($DS)SourceDir$($DS)ChildDir-2" | Compress-Archive -DestinationPath $destinationPath -Update - Test-Path $destinationPath | Should Be $True - - # Piping the output of Get-ChildItem to Compress-Archive - dir "$TestDrive$($DS)SourceDir" -Recurse | Compress-Archive -DestinationPath $destinationPath -Update - Test-Path $destinationPath | Should Be $True - } - - It "Validate that Compress-Archive works on ReadOnly files" { - $sourcePath = "$TestDrive$($DS)ReadOnlyFile.txt" - $destinationPath = "$TestDrive$($DS)TestForReadOnlyFile.zip" - - "Some Content" | Out-File -FilePath $sourcePath - $createdItem = Get-Item $sourcePath - $createdItem.Attributes = 'ReadOnly' - - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - Test-Path $destinationPath | Should Be $true - } - - It "Validate that Compress-Archive generates Verbose messages" { - $sourcePath = "$TestDrive$($DS)SourceDir" - $destinationPath = "$TestDrive$($DS)Compress-Archive generates VerboseMessages.zip" - - try - { - $ps=[PowerShell]::Create() - $ps.Streams.Error.Clear() - $ps.Streams.Verbose.Clear() - $script = "Import-Module Microsoft.PowerShell.Archive; Compress-Archive -Path $sourcePath -DestinationPath `"$destinationPath`" -CompressionLevel Fastest -Verbose" - $ps.AddScript($script) - $ps.Invoke() - - $ps.Streams.Verbose.Count -gt 0 | Should Be $True - $ps.Streams.Error.Count | Should Be 0 - } - finally - { - $ps.Dispose() - } - } - - It "Validate that Compress-Archive returns nothing when -PassThru is not used" { - $sourcePath = @("$TestDrive$($DS)SourceDir") - $destinationPath = "$TestDrive$($DS)NoPassThruTest.zip" - $archive = Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - $archive | Should Be $null - } - - It "Validate that Compress-Archive returns nothing when -PassThru is used with a value of $false" { - $sourcePath = @("$TestDrive$($DS)SourceDir") - $destinationPath = "$TestDrive$($DS)FalsePassThruTest.zip" - $archive = Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -PassThru:$false - $archive | Should Be $null - } - - It "Validate that Compress-Archive returns the archive when invoked with -PassThru" { - $sourcePath = @("$TestDrive$($DS)SourceDir") - $destinationPath = "$TestDrive$($DS)PassThruTest.zip" - $archive = Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -PassThru - $archive.FullName | Should Be $destinationPath - } - - It "Validate that Compress-Archive can create a zip archive that has a different extension" { - $sourcePath = "$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-3.txt" - $destinationPath = "$TestDrive$($DS)DifferentZipExtension.dat" - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - $destinationPath | Should Exist - } - - It "Validate that Compress-Archive can create a zip archive when the source is in use" { - $sourcePath = "$TestDrive$($DS)InUseFile.txt" - $destinationPath = "$TestDrive$($DS)TestForinUseFile.zip" - - "Some Content" | Out-File -FilePath $sourcePath - Get-Content $sourcePath - $TestFile = [System.IO.File]::Open($sourcePath, 'Open', 'Read', 'Read') - - try { - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - Test-Path $destinationPath | Should Be $true - } - finally { - $TestFile.Close() - } - } - } - - Context "Expand-Archive - Parameter validation test cases" { - It "Validate non existing archive -Path trows expected error message" { - $sourcePath = "$TestDrive$($DS)SourceDir" - $destinationPath = "$TestDrive$($DS)ExpandedArchive" - try - { - Expand-Archive -Path $sourcePath -DestinationPath $destinationPath - throw "Expand-Archive succeeded for non existing archive path" - } - catch - { - $_.FullyQualifiedErrorId | Should Be "PathNotFound,Expand-Archive" - } - } - - It "Validate errors from Expand-Archive with NULL & EMPTY values for Path, LiteralPath, DestinationPath parameters" { - ExpandArchiveInvalidParameterValidator $false $null "$TestDrive$($DS)SourceDir" "ParameterArgumentValidationError,Expand-Archive" - ExpandArchiveInvalidParameterValidator $false $null $null "ParameterArgumentValidationError,Expand-Archive" - - ExpandArchiveInvalidParameterValidator $false "$TestDrive$($DS)SourceDir" $null "ParameterArgumentTransformationError,Expand-Archive" - ExpandArchiveInvalidParameterValidator $false "" "$TestDrive$($DS)SourceDir" "ParameterArgumentTransformationError,Expand-Archive" - ExpandArchiveInvalidParameterValidator $false "$TestDrive$($DS)SourceDir" "" "ParameterArgumentTransformationError,Expand-Archive" - ExpandArchiveInvalidParameterValidator $false "" "" "ParameterArgumentTransformationError,Expand-Archive" - - ExpandArchiveInvalidParameterValidator $true $null "$TestDrive$($DS)SourceDir" "ParameterArgumentValidationError,Expand-Archive" - ExpandArchiveInvalidParameterValidator $true $null $null "ParameterArgumentValidationError,Expand-Archive" - - ExpandArchiveInvalidParameterValidator $true "$TestDrive$($DS)SourceDir" $null "ParameterArgumentValidationError,Expand-Archive" - ExpandArchiveInvalidParameterValidator $true "" "$TestDrive$($DS)SourceDir" "ParameterArgumentValidationError,Expand-Archive" - ExpandArchiveInvalidParameterValidator $true "$TestDrive$($DS)SourceDir" "" "ParameterArgumentValidationError,Expand-Archive" - ExpandArchiveInvalidParameterValidator $true "" "" "ParameterArgumentValidationError,Expand-Archive" - - ExpandArchiveInvalidParameterValidator $true $null "$TestDrive$($DS)SourceDir" "ParameterArgumentValidationError,Expand-Archive" - ExpandArchiveInvalidParameterValidator $true $null $null "ParameterArgumentValidationError,Expand-Archive" - - ExpandArchiveInvalidParameterValidator $true "$TestDrive$($DS)SourceDir" $null "ParameterArgumentValidationError,Expand-Archive" - ExpandArchiveInvalidParameterValidator $true "" "$TestDrive$($DS)SourceDir" "ParameterArgumentValidationError,Expand-Archive" - ExpandArchiveInvalidParameterValidator $true "$TestDrive$($DS)SourceDir" "" "ParameterArgumentValidationError,Expand-Archive" - ExpandArchiveInvalidParameterValidator $true "" "" "ParameterArgumentValidationError,Expand-Archive" - } - - It "Validate errors from Expand-Archive when invalid path (non-existing path / non-filesystem path) is supplied for Path or LiteralPath parameters" { - try { Expand-Archive -Path "$TestDrive$($DS)NonExistingArchive" -DestinationPath "$TestDrive$($DS)SourceDir"; throw "Expand-Archive did NOT throw expected error" } - catch { $_.FullyQualifiedErrorId | Should Be "ArchiveCmdletPathNotFound,Expand-Archive" } - - try { Expand-Archive -LiteralPath "$TestDrive$($DS)NonExistingArchive" -DestinationPath "$TestDrive$($DS)SourceDir"; throw "Expand-Archive did NOT throw expected error" } - catch { $_.FullyQualifiedErrorId | Should Be "ArchiveCmdletPathNotFound,Expand-Archive" } - } - - It "Validate error from Expand-Archive when invalid path (non-existing path / non-filesystem path) is supplied for DestinationPath parameter" { - $sourcePath = "$TestDrive$($DS)SamplePreCreatedArchive.zip" - $destinationPath = "HKLM:\SOFTWARE" - if ($IsWindows) { - $expectedError = "InvalidDirectoryPath,Expand-Archive" - } - else { - $expectedError = "DriveNotFound,Microsoft.PowerShell.Commands.NewItemCommand" - } - try { Expand-Archive -Path $sourcePath -DestinationPath $destinationPath; throw "Expand-Archive did NOT throw expected error" } - catch { $_.FullyQualifiedErrorId | Should Be $expectedError } - } - - It "Validate that you can compress an archive to a custom PSDrive using the Compress-Archive cmdlet" { - $sourcePath = "$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-3.txt" - $destinationDriveName = 'CompressArchivePesterTest' - $destinationDrive = New-PSDrive -Name $destinationDriveName -PSProvider FileSystem -Root $TestDrive -Scope Global - $destinationPath = "${destinationDriveName}:$($DS)CompressToPSDrive.zip" - try { - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - $destinationPath | Should Exist - } finally { - Remove-PSDrive -LiteralName $destinationDriveName - } - } - } - - Context "Expand-Archive - functional test cases" { - - It "Validate basic Expand-Archive scenario" { - $sourcePath = "$TestDrive$($DS)SamplePreCreatedArchive.zip" - $content = "Some Data" - $destinationPath = "$TestDrive$($DS)DestDirForBasicExpand" - $files = @("Sample-1.txt", "Sample-2.txt") - - # The files in "$TestDrive$($DS)SamplePreCreatedArchive.zip" are precreated. - $fileCreationTimeStamp = Get-Date -Year 2014 -Month 6 -Day 13 -Hour 15 -Minute 50 -Second 20 -Millisecond 0 - - Expand-Archive -Path $sourcePath -DestinationPath $destinationPath - foreach($currentFile in $files) - { - $expandedFile = Join-Path $destinationPath -ChildPath $currentFile - Test-Path $expandedFile | Should Be $True - - # We are validating to make sure that time stamps are preserved in the - # compressed archive are reflected back when the file is expanded. - (dir $expandedFile).LastWriteTime.CompareTo($fileCreationTimeStamp) | Should Be 0 - - Get-Content $expandedFile | Should Be $content - } - } - It "Validate that Expand-Archive cmdlet errors out when DestinationPath resolves to multiple locations" { - New-Item $TestDrive$($DS)SampleDir$($DS)Child-1 -Type Directory -Force | Out-Null - New-Item $TestDrive$($DS)SampleDir$($DS)Child-2 -Type Directory -Force | Out-Null - - $destinationPath = "$TestDrive$($DS)SampleDir$($DS)Child-*" - $sourcePath = "$TestDrive$($DS)SamplePreCreatedArchive.zip" - try - { - Expand-Archive -Path $sourcePath -DestinationPath $destinationPath - throw "Failed to detect that destination $destinationPath can resolve to multiple paths" - } - catch - { - $_.FullyQualifiedErrorId | Should Be "InvalidDestinationPath,Expand-Archive" - } - finally - { - Remove-Item -LiteralPath $TestDrive$($DS)SampleDir -Force -Recurse - } - } - It "Validate that Expand-Archive cmdlet works when DestinationPath resolves has wild card pattern and resolves to a single valid path" { - New-Item $TestDrive$($DS)SampleDir$($DS)Child-1 -Type Directory -Force | Out-Null - - $destinationPath = "$TestDrive$($DS)SampleDir$($DS)Child-*" - $sourcePath = "$TestDrive$($DS)SamplePreCreatedArchive.zip" - try - { - Expand-Archive -Path $sourcePath -DestinationPath $destinationPath - $expandedFiles = Get-ChildItem $destinationPath -Recurse - $expandedFiles.Length | Should BeGreaterThan 1 - } - finally - { - Remove-Item -LiteralPath $TestDrive$($DS)SampleDir -Force -Recurse - } - } - It "Validate Expand-Archive scenario where DestinationPath has Special Characters" { - $sourcePath = "$TestDrive$($DS)SamplePreCreatedArchive.zip" - $content = "Some Data" - $destinationPath = "$TestDrive$($DS)DestDir[]Expand" - $files = @("Sample-1.txt", "Sample-2.txt") - - # The files in "$TestDrive$($DS)SamplePreCreatedArchive.zip" are precreated. - $fileCreationTimeStamp = Get-Date -Year 2014 -Month 6 -Day 13 -Hour 15 -Minute 50 -Second 20 -Millisecond 0 - - Expand-Archive -Path $sourcePath -DestinationPath $destinationPath - foreach($currentFile in $files) - { - $expandedFile = Join-Path $destinationPath -ChildPath $currentFile - Test-Path -LiteralPath $expandedFile | Should Be $True - - # We are validating to make sure that time stamps are preserved in the - # compressed archive are reflected back when the file is expanded. - (dir -LiteralPath $expandedFile).LastWriteTime.CompareTo($fileCreationTimeStamp) | Should Be 0 - - Get-Content -LiteralPath $expandedFile | Should Be $content - } - } - It "Invoke Expand-Archive with relative path in Path parameter and -Force parameter" { - $sourcePath = ".$($DS)SamplePreCreatedArchive.zip" - $destinationPath = "$TestDrive$($DS)SomeOtherNonExistingDir$($DS)Path" - try - { - Push-Location $TestDrive - - Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -Force - $expandedFiles = Get-ChildItem $destinationPath -Recurse - $expandedFiles.Length | Should Be 2 - } - finally - { - Pop-Location - } - } - - It "Invoke Expand-Archive with relative path in LiteralPath parameter and -Force parameter" { - $sourcePath = ".$($DS)SamplePreCreatedArchive.zip" - $destinationPath = "$TestDrive$($DS)SomeOtherNonExistingDir$($DS)LiteralPath" - try - { - Push-Location $TestDrive - - Expand-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath -Force - $expandedFiles = Get-ChildItem $destinationPath -Recurse - $expandedFiles.Length | Should Be 2 - } - finally - { - Pop-Location - } - } - - It "Invoke Expand-Archive with non-existing relative directory in DestinationPath parameter and -Force parameter" { - $sourcePath = "$TestDrive$($DS)SamplePreCreatedArchive.zip" - $destinationPath = ".$($DS)SomeOtherNonExistingDir$($DS)DestinationPath" - try - { - Push-Location $TestDrive - - Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -Force - $expandedFiles = Get-ChildItem $destinationPath -Recurse - $expandedFiles.Length | Should Be 2 - } - finally - { - Pop-Location - } - } - - # The test below is no longer valid. You can have zip files with non-zip extensions. Different archive - # formats should be added in a separate pull request, with a parameter to identify the archive format, and - # default formats associated with specific extensions. Until then, as long as these cmdlets only support - # Zip files, any file extension is supported. - #It "Invoke Expand-Archive with unsupported archive format" { - # $sourcePath = "$TestDrive$($DS)Sample.cab" - # $destinationPath = "$TestDrive$($DS)UnsupportedArchiveFormatDir" - # try - # { - # Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -Force - # throw "Failed to detect unsupported archive format at $sourcePath" - # } - # catch - # { - # $_.FullyQualifiedErrorId | Should Be "NotSupportedArchiveFileExtension,Expand-Archive" - # } - #} - - It "Invoke Expand-Archive with archive file containing multiple files, directories with subdirectories and empty directories" { - $sourcePath = "$TestDrive$($DS)SourceDir" - $archivePath = "$TestDrive$($DS)FileAndDirTreeForExpand.zip" - $destinationPath = "$TestDrive$($DS)FileAndDirTree" - $sourceList = dir $sourcePath -Name - - Add-CompressionAssemblies - [System.IO.Compression.ZipFile]::CreateFromDirectory($sourcePath, $archivePath) - - Expand-Archive -Path $archivePath -DestinationPath $destinationPath - $extractedList = dir $destinationPath -Name - - Compare-Object -ReferenceObject $extractedList -DifferenceObject $sourceList -PassThru | Should Be $null - } - - It "Validate Expand-Archive cmdlet in pipleline scenario" { - $sourcePath = "$TestDrive$($DS)SamplePreCreated*.zip" - $destinationPath = "$TestDrive$($DS)PipeToExpandArchive" - - $content = "Some Data" - $files = @("Sample-1.txt", "Sample-2.txt") - - dir $sourcePath | Expand-Archive -DestinationPath $destinationPath - - foreach($currentFile in $files) - { - $expandedFile = Join-Path $destinationPath -ChildPath $currentFile - Test-Path $expandedFile | Should Be $True - Get-Content $expandedFile | Should Be $content - } - } - - It "Validate that Expand-Archive generates Verbose messages" { - $sourcePath = "$TestDrive$($DS)SamplePreCreatedArchive.zip" - $destinationPath = "$TestDrive$($DS)VerboseMessagesInExpandArchive" - - try - { - $ps=[PowerShell]::Create() - $ps.Streams.Error.Clear() - $ps.Streams.Verbose.Clear() - $script = "Import-Module Microsoft.PowerShell.Archive; Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -Verbose" - $ps.AddScript($script) - $ps.Invoke() - - $ps.Streams.Verbose.Count -gt 0 | Should Be $True - $ps.Streams.Error.Count | Should Be 0 - } - finally - { - $ps.Dispose() - } - } - - It "Validate that without -Force parameter Expand-Archive generates non-terminating errors without overwriting existing files" { - $sourcePath = "$TestDrive$($DS)SamplePreCreatedArchive.zip" - $destinationPath = "$TestDrive$($DS)NoForceParameterExpandArchive" - - try - { - $ps=[PowerShell]::Create() - $ps.Streams.Error.Clear() - $ps.Streams.Verbose.Clear() - $script = "Import-Module Microsoft.PowerShell.Archive; Expand-Archive -Path $sourcePath -DestinationPath $destinationPath; Expand-Archive -Path $sourcePath -DestinationPath $destinationPath" - $ps.AddScript($script) - $ps.Invoke() - - $ps.Streams.Error.Count -gt 0 | Should Be $True - } - finally - { - $ps.Dispose() - } - } - - It "Validate that without DestinationPath parameter Expand-Archive cmdlet succeeds in expanding the archive" { - $sourcePath = "$TestDrive$($DS)SamplePreCreatedArchive.zip" - $archivePath = "$TestDrive$($DS)NoDestinationPathParameter.zip" - $destinationPath = "$TestDrive$($DS)NoDestinationPathParameter" - copy $sourcePath $archivePath -Force - - try - { - Push-Location $TestDrive - - Expand-Archive -Path $archivePath - (dir $destinationPath).Count | Should Be 2 - } - finally - { - Pop-Location - } - } - - It "Validate that without DestinationPath parameter Expand-Archive cmdlet succeeds in expanding the archive when destination directory exists" { - $sourcePath = "$TestDrive$($DS)SamplePreCreatedArchive.zip" - $archivePath = "$TestDrive$($DS)NoDestinationPathParameterDirExists.zip" - $destinationPath = "$TestDrive$($DS)NoDestinationPathParameterDirExists" - copy $sourcePath $archivePath -Force - New-Item -Path $destinationPath -ItemType Directory | Out-Null - - try - { - Push-Location $TestDrive - - Expand-Archive -Path $archivePath - (dir $destinationPath).Count | Should Be 2 - } - finally - { - Pop-Location - } - } - - It "Validate that Expand-Archive returns nothing when -PassThru is not used" { - $sourcePath = "$TestDrive$($DS)SourceDir" - $archivePath = "$TestDrive$($DS)NoPassThruTestForExpand.zip" - $destinationPath = "$TestDrive$($DS)NoPassThruTest" - - $sourceList = dir $sourcePath -Name - - Add-CompressionAssemblies - [System.IO.Compression.ZipFile]::CreateFromDirectory($sourcePath, $archivePath) - - $contents = Expand-Archive -Path $archivePath -DestinationPath $destinationPath - - $contents | Should Be $null - } - - It "Validate that Expand-Archive returns nothing when -PassThru is used with a value of $false" { - $sourcePath = "$TestDrive$($DS)SourceDir" - $archivePath = "$TestDrive$($DS)FalsePassThruTestForExpand.zip" - $destinationPath = "$TestDrive$($DS)FalsePassThruTest" - $sourceList = dir $sourcePath -Name - - Add-CompressionAssemblies - [System.IO.Compression.ZipFile]::CreateFromDirectory($sourcePath, $archivePath) - - $contents = Expand-Archive -Path $archivePath -DestinationPath $destinationPath -PassThru:$false - - $contents | Should Be $null - } - - It "Validate that Expand-Archive returns the contents of the archive -PassThru" { - $sourcePath = "$TestDrive$($DS)SourceDir" - $archivePath = "$TestDrive$($DS)PassThruTestForExpand.zip" - $destinationPath = "$TestDrive$($DS)PassThruTest" - $sourceList = dir $sourcePath -Name - - Add-CompressionAssemblies - [System.IO.Compression.ZipFile]::CreateFromDirectory($sourcePath, $archivePath) - - $contents = Expand-Archive -Path $archivePath -DestinationPath $destinationPath -PassThru | Sort-Object -Property PSParentPath,PSIsDirectory,Name - # We pipe Get-ChildItem to Get-Item here because the ToString results are different between the two, and we - # need to compare with other Get-Item results - $extractedList = Get-ChildItem -Recurse -LiteralPath $destinationPath | Get-Item - - Compare-Object -ReferenceObject $extractedList -DifferenceObject $contents -PassThru | Should Be $null - } - - It "Validate Expand-Archive works with zip files that have non-zip file extensions" { - $sourcePath = "$TestDrive$($DS)SourceDir" - $archivePath = "$TestDrive$($DS)NonZipFileExtension.dat" - $destinationPath = "$TestDrive$($DS)NonZipFileExtension" - $sourceList = dir $sourcePath -Name - - Add-CompressionAssemblies - [System.IO.Compression.ZipFile]::CreateFromDirectory($sourcePath, $archivePath) - - Expand-Archive -Path $archivePath -DestinationPath $destinationPath - $extractedList = dir $destinationPath -Name - - Compare-Object -ReferenceObject $extractedList -DifferenceObject $sourceList -PassThru | Should Be $null - } - - # trailing spaces give this error on Linux: Exception calling "[System.IO.Compression.ZipFileExtensions]::ExtractToFile" with "3" argument(s): "Could not find a part of the path '/tmp/02132f1d-5b0c-4a99-b5bf-707cef7681a6/TrailingSpacer/Inner/TrailingSpace/test.txt'." - It "Validate Expand-Archive works with zip files where the contents contain trailing whitespace" -skip:(!$IsWindows) { - $archivePath = "$TestDrive$($DS)TrailingSpacer.zip" - $destinationPath = "$TestDrive$($DS)TrailingSpacer" - # we can't just compare the output and the results as you only get one DirectoryInfo for directories that only contain directories - $expectedPaths = "$TestDrive$($DS)TrailingSpacer$($DS)Inner$($DS)TrailingSpace","$TestDrive$($DS)TrailingSpacer$($DS)Inner$($DS)TrailingSpace$($DS)test.txt" - - $contents = Expand-Archive -Path $archivePath -DestinationPath $destinationPath -PassThru - - $contents.Count | Should Be $expectedPaths.Count - - for ($i = 0; $i -lt $expectedPaths.Count; $i++) { - $contents[$i].FullName | Should Be $expectedPaths[$i] - } - } - - It "Validate that Compress-Archive/Expand-Archive work with backslashes and forward slashes in paths" { - $sourcePath = "$TestDrive\SourceDir/ChildDir-2" - $archivePath = "$TestDrive\MixedSlashesDir1/MixedSlashesDir2/SampleMixedslashFile.zip" - $expandPath = "$TestDrive\MixedSlashesExpandDir/DirA\DirB/DirC" - - New-Item -Path (Split-Path $archivePath) -Type Directory | Out-Null - Compress-Archive -Path $sourcePath -DestinationPath $archivePath - $archivePath | Should Exist - - $content = "Some Data" - $files = @("ChildDir-2$($DS)Sample-5.txt", "ChildDir-2$($DS)Sample-6.txt") - Expand-Archive -Path $archivePath -DestinationPath $expandPath - foreach($currentFile in $files) - { - $expandedFile = Join-Path $expandPath -ChildPath $currentFile - Test-Path $expandedFile | Should Be $True - Get-Content $expandedFile | Should Be $content - } - } - - It "Validate that Compress-Archive/Expand-Archive work with dates earlier than 1980" { - $file1 = New-Item $TestDrive$($DS)SourceDir$($DS)EarlierThan1980.txt -Type File -Force - $file1.LastWriteTime = [DateTime]::Parse('1974-10-03T04:30:00') - $file2 = Get-Item "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt" - $expandPath = "$TestDrive$($DS)EarlyYearDir" - $expectedFile1 = "$expandPath$($DS)EarlierThan1980.txt" - $expectedFile2 = "$expandPath$($DS)Sample-1.txt" - $archivePath = "$TestDrive$($DS)EarlyYear.zip" - - try - { - Compress-Archive -Path @($file1, $file2) -DestinationPath $archivePath -WarningAction SilentlyContinue - $archivePath | Should Exist - - Expand-Archive -Path $archivePath -DestinationPath $expandPath - - $expectedFile1 | Should Exist - (Get-Item $expectedFile1).LastWriteTime | Should Be $([DateTime]::Parse('1980-01-01T00:00')) - $expectedFile2 | Should Exist - (Get-Item $expectedFile2).LastWriteTime | Should Not Be $([DateTime]::Parse('1980-01-01T00:00')) - - } - finally - { - Remove-Item -LiteralPath $archivePath -Force -Recurse - } - } - - # test is currently blocked by https://github.com/dotnet/corefx/issues/24832 - It "Validate module can be imported when current language is not en-US" -Pending { - $currentCulture = [System.Threading.Thread]::CurrentThread.CurrentUICulture - try { - [System.Threading.Thread]::CurrentThread.CurrentCulture = [CultureInfo]::new("he-IL") - { Import-Module Microsoft.PowerShell.Archive -Force -ErrorAction Stop } | Should Not Throw - } - finally { - [System.Threading.Thread]::CurrentThread.CurrentCulture = $currentCulture - } - } - } -} \ No newline at end of file diff --git a/Tests/SamplePreCreatedArchive.archive b/Tests/SamplePreCreatedArchive.archive deleted file mode 100644 index c2aecb417985e0ae3b2fff30c1cfd41f82be5932..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 270 zcmWIWW@Zs#U|`^2@TxoOvPh-fTmr}gVICk3PRuRHN!2yfE2${?{a=5E;(W#Fic=jM z54I{!T*%T3{gTw;61|d&k^pZ; dCJ_eQnjz*WK#e0{6e}A@9U~BW18HlpCICN18x;Tm diff --git a/TravisCI.ps1 b/TravisCI.ps1 deleted file mode 100644 index a605497..0000000 --- a/TravisCI.ps1 +++ /dev/null @@ -1,6 +0,0 @@ -$testResultsFile = "./ArchiveTestResults.xml" -Import-Module "./Microsoft.PowerShell.Archive/Microsoft.PowerShell.Archive.psd1" -Force -$testResults = Invoke-Pester -Script "./Tests" -OutputFormat NUnitXml -OutputFile $testResultsFile -PassThru -if ($testResults.FailedCount -gt 0) { - throw "$($testResults.FailedCount) tests failed." -} diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index ac70956..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,22 +0,0 @@ -# Image with WMF5.0 RTM -os: "WMF 5" - -# clone directory -clone_folder: c:\projects\Archive-Module - -# Install Pester -install: - - cinst -y pester - -build: false - -# Run Pester tests and store the results -test_script: - - ps: | - $testResultsFile = ".\ArchiveTestResults.xml" - Import-Module "C:\projects\Archive-Module\Microsoft.PowerShell.Archive" -Force - $testResults = Invoke-Pester -Script "C:\projects\Archive-Module\Tests" -OutputFormat NUnitXml -OutputFile $testResultsFile -PassThru - (New-Object 'System.Net.WebClient').UploadFile("https://ci.appveyor.com/api/testresults/nunit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path $testResultsFile)) - if ($testResults.FailedCount -gt 0) { - throw "$($testResults.FailedCount) tests failed." - } diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml deleted file mode 100644 index 9585892..0000000 --- a/azure-pipelines-release.yml +++ /dev/null @@ -1,109 +0,0 @@ -name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr) - -trigger: none - -resources: - repositories: - - repository: ComplianceRepo - type: github - endpoint: ComplianceGHRepo - name: PowerShell/compliance - -variables: - - name: PackageName - value: 'Microsoft.PowerShell.Archive' - - name: PackageVersion - value: '' - - name: BuildOutDir - value: '' - -stages: -- stage: Build - displayName: Build module - pool: - name: 1ES - demands: - - ImageOverride -equals PSMMS2019-Secure - jobs: - - job: BuildPkg - displayName: Build module - variables: - - group: ESRP - steps: - - - task: UseDotNet@2 - displayName: 'Get .NET 7.0 SDK' - inputs: - packageType: sdk - version: 7.0 - includePreviewVersions: true - - - pwsh: | - & $(Build.SourcesDirectory)\SimpleBuild.ps1 - displayName: Build Microsoft.PowerShell.Archive module - condition: succeededOrFailed() - - - pwsh: | - dir "$(BuildOutDir)\*" -Recurse - displayName: Show BuildOutDirectory - - - template: Sbom.yml@ComplianceRepo - parameters: - BuildDropPath: "$(BuildOutDir)" - Build_Repository_Uri: 'https://github.com/PowerShell/Microsoft.PowerShell.Archive' - PackageName: $(PackageName) - PackageVersion: $(PackageVersion) - - - pwsh: | - dir "$(BuildOutDir)\*" -Recurse - displayName: Show BuildOutDirectory - - - pwsh: | - $signSrcPath = "$(BuildOutDir)" - # Set signing src path variable - $vstsCommandString = "vso[task.setvariable variable=signSrcPath]${signSrcPath}" - Write-Host "sending " + $vstsCommandString - Write-Host "##$vstsCommandString" - $signOutPath = "$(Build.SourcesDirectory)\signed\Microsoft.PowerShell.Archive" - $null = New-Item -ItemType Directory -Path $signOutPath - # Set signing out path variable - $vstsCommandString = "vso[task.setvariable variable=signOutPath]${signOutPath}" - Write-Host "sending " + $vstsCommandString - Write-Host "##$vstsCommandString" - # Set path variable for guardian codesign validation - $vstsCommandString = "vso[task.setvariable variable=GDN_CODESIGN_TARGETDIRECTORY]${signOutPath}" - Write-Host "sending " + $vstsCommandString - Write-Host "##$vstsCommandString" - displayName: Setup variables for signing - - - template: EsrpSign.yml@ComplianceRepo - parameters: - # the folder which contains the binaries to sign - buildOutputPath: $(signSrcPath) - # the location to put the signed output - signOutputPath: $(signOutPath) - # the certificate ID to use - certificateId: "CP-230012" - # the file pattern to use, comma separated - pattern: '*.psd1,*.psm1' - - - pwsh: | - Compress-Archive -Path "$(signOutPath)\*" -DestinationPath "$(System.ArtifactsDirectory)\Microsoft.PowerShell.Archive.zip" - displayName: Create Microsoft.PowerShell.Archive.zip - - - publish: $(System.ArtifactsDirectory)\Microsoft.PowerShell.Archive.zip - artifact: SignedModule - - - template: script-module-compliance.yml@ComplianceRepo - parameters: - # component-governance - sourceScanPath: '$(signOutPath)' - # credscan - suppressionsFile: '' - # TermCheck - optionsRulesDBPath: '' - optionsFTPath: '' - # tsa-upload - codeBaseName: 'Microsoft_PowerShell_Archive_2_15_2022' - # selections - APIScan: false # set to false when not using Windows APIs. diff --git a/src/ArchiveAddition.cs b/src/ArchiveAddition.cs index 4c59a7f..c19e32f 100644 --- a/src/ArchiveAddition.cs +++ b/src/ArchiveAddition.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; using System.Collections.Generic; using System.Text; diff --git a/src/ArchiveFactory.cs b/src/ArchiveFactory.cs index 35b557e..fd0e636 100644 --- a/src/ArchiveFactory.cs +++ b/src/ArchiveFactory.cs @@ -1,8 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Management.Automation; -using System.Net; -using System.Text; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; namespace Microsoft.PowerShell.Archive { @@ -12,9 +12,9 @@ internal static IArchive GetArchive(ArchiveFormat format, string archivePath, Ar { System.IO.FileStream archiveFileStream = archiveMode switch { - ArchiveMode.Create => new System.IO.FileStream(archivePath, mode: System.IO.FileMode.CreateNew, access: System.IO.FileAccess.Write, share: System.IO.FileShare.None), - ArchiveMode.Update => new System.IO.FileStream(archivePath, mode: System.IO.FileMode.Open, access: System.IO.FileAccess.ReadWrite, share: System.IO.FileShare.None), - ArchiveMode.Extract => new System.IO.FileStream(archivePath, mode: System.IO.FileMode.Open, access: System.IO.FileAccess.Read, share: System.IO.FileShare.Read), + ArchiveMode.Create => new FileStream(archivePath, mode: System.IO.FileMode.CreateNew, access: System.IO.FileAccess.Write, share: System.IO.FileShare.None), + ArchiveMode.Update => new FileStream(archivePath, mode: System.IO.FileMode.Open, access: System.IO.FileAccess.ReadWrite, share: System.IO.FileShare.None), + ArchiveMode.Extract => new FileStream(archivePath, mode: System.IO.FileMode.Open, access: System.IO.FileAccess.Read, share: System.IO.FileShare.Read), _ => throw new ArgumentOutOfRangeException(nameof(archiveMode)) }; @@ -29,7 +29,7 @@ internal static IArchive GetArchive(ArchiveFormat format, string archivePath, Ar internal static bool TryGetArchiveFormatFromExtension(string path, out ArchiveFormat? archiveFormat) { - archiveFormat = System.IO.Path.GetExtension(path).ToLowerInvariant() switch + archiveFormat = Path.GetExtension(path).ToLowerInvariant() switch { ".zip" => ArchiveFormat.Zip, /* Disable support for tar and tar.gz for preview1 release diff --git a/src/ArchiveFormat.cs b/src/ArchiveFormat.cs index 0f0d1a0..5ee6fef 100644 --- a/src/ArchiveFormat.cs +++ b/src/ArchiveFormat.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; using System.Collections.Generic; using System.Text; diff --git a/src/ArchiveMode.cs b/src/ArchiveMode.cs index 6f32220..2c07b85 100644 --- a/src/ArchiveMode.cs +++ b/src/ArchiveMode.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; using System.Collections.Generic; using System.Text; diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index fa2ef80..ce30438 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -41,7 +44,7 @@ private enum ParameterSet /// [Parameter(Mandatory = true, ParameterSetName = nameof(ParameterSet.LiteralPath), ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] - [Alias("PSPath")] + [Alias("PSPath", "LP")] public string[]? LiteralPath { get; set; } /// @@ -92,9 +95,6 @@ protected override void BeginProcessing() _destinationPathInfo = _pathHelper.ResolveToSingleFullyQualifiedPath(DestinationPath); DestinationPath = _destinationPathInfo.FullName; ValidateDestinationPath(); - - // Determine archive format based on DestinationPath - DetermineArchiveFormat(); } protected override void ProcessRecord() @@ -178,11 +178,16 @@ protected override void EndProcessing() long numberOfAdditions = archiveAdditions.Count; long numberOfAddedItems = 0; - var progressRecord = new ProgressRecord(activityId: 1, activity: "Compress-Archive", statusDescription: string.Format(Messages.ProgressDisplay, "0.0")); - WriteProgress(progressRecord); + // Messages.ProgressDisplay does not need to be formatted here because progressRecord.StautsDescription will be updated in the for-loop + var progressRecord = new ProgressRecord(activityId: 1, activity: "Compress-Archive", statusDescription: Messages.ProgressDisplay); foreach (ArchiveAddition entry in archiveAdditions) { + // Update progress + var percentComplete = numberOfAddedItems / (float)numberOfAdditions * 100f; + progressRecord.StatusDescription = string.Format(Messages.ProgressDisplay, "{percentComplete:0.0}"); + WriteProgress(progressRecord); + if (ShouldProcess(target: entry.FileSystemInfo.FullName, action: Messages.Add)) { archive?.AddFileSystemEntry(entry); @@ -190,20 +195,14 @@ protected override void EndProcessing() var addedItemMessage = string.Format(Messages.AddedItemToArchiveVerboseMessage, entry.FileSystemInfo.FullName); WriteVerbose(addedItemMessage); } - - // Keep track of number of items added to the archive and use that to update progress + // Keep track of number of items added to the archive numberOfAddedItems++; - var percentComplete = numberOfAddedItems / (float)numberOfAdditions * 100f; - progressRecord.StatusDescription = string.Format(Messages.ProgressDisplay, "{percentComplete:0.0}"); - WriteProgress(progressRecord); } - // If there were no items to add, show progress as 100% - if (numberOfAdditions == 0) - { - progressRecord.StatusDescription = string.Format(Messages.ProgressDisplay, "100.0"); - WriteProgress(progressRecord); - } + // Once all items in the archive are processed, show progress as 100% + // This code is here and not in the loop because we want it to run even if there are no items to add to the archive + progressRecord.StatusDescription = string.Format(Messages.ProgressDisplay, "100.0"); + WriteProgress(progressRecord); } finally { @@ -227,59 +226,58 @@ protected override void StopProcessing() } /// - /// Validate DestinationPath parameter + /// Validate DestinationPath parameter and determine the archive format based on the extension of DestinationPath /// private void ValidateDestinationPath() { Debug.Assert(_destinationPathInfo is not null); ErrorCode? errorCode = null; - // In this case, DestinationPath does not exist - if (!_destinationPathInfo.Exists) + if (_destinationPathInfo.Exists) { - // Throw an error if DestinationPath does not exist and cmdlet is in Update mode - if (WriteMode == WriteMode.Update) - { - errorCode = ErrorCode.ArchiveDoesNotExist; - } - } - // Check if DestinationPath is an existing directory - else if (_destinationPathInfo.Attributes.HasFlag(FileAttributes.Directory)) - { - // Throw an error if DestinationPath exists and the cmdlet is not in Update mode or Overwrite is not specified - if (WriteMode == WriteMode.Create) - { - errorCode = ErrorCode.ArchiveExistsAsDirectory; - } - // Throw an error if the DestinationPath is a directory and the cmdlet is in Update mode - else if (WriteMode == WriteMode.Update) + // Check if DestinationPath is an existing directory + if (_destinationPathInfo.Attributes.HasFlag(FileAttributes.Directory)) { - errorCode = ErrorCode.ArchiveExistsAsDirectory; - } - // Throw an error if the DestinationPath is the current working directory and the cmdlet is in Overwrite mode - else if (WriteMode == WriteMode.Overwrite && _destinationPathInfo.FullName == SessionState.Path.CurrentFileSystemLocation.ProviderPath) - { - errorCode = ErrorCode.CannotOverwriteWorkingDirectory; + // Throw an error if DestinationPath exists and the cmdlet is not in Update mode or Overwrite is not specified + if (WriteMode == WriteMode.Create) + { + errorCode = ErrorCode.ArchiveExistsAsDirectory; + } + // Throw an error if the DestinationPath is a directory and the cmdlet is in Update mode + else if (WriteMode == WriteMode.Update) + { + errorCode = ErrorCode.ArchiveExistsAsDirectory; + } + // Throw an error if the DestinationPath is the current working directory and the cmdlet is in Overwrite mode + else if (WriteMode == WriteMode.Overwrite && _destinationPathInfo.FullName == SessionState.Path.CurrentFileSystemLocation.ProviderPath) + { + errorCode = ErrorCode.CannotOverwriteWorkingDirectory; + } + // Throw an error if the DestinationPath is a directory with at 1 least item and the cmdlet is in Overwrite mode + else if (WriteMode == WriteMode.Overwrite && _destinationPathInfo is DirectoryInfo directory && directory.GetFileSystemInfos().Length > 0) + { + errorCode = ErrorCode.ArchiveIsNonEmptyDirectory; + } } - // Throw an error if the DestinationPath is a directory with at 1 least item and the cmdlet is in Overwrite mode - else if (WriteMode == WriteMode.Overwrite && _destinationPathInfo is DirectoryInfo directory && directory.GetFileSystemInfos().Length > 0) + // If DestinationPath is an existing file + else { - errorCode = ErrorCode.ArchiveIsNonEmptyDirectory; + // Throw an error if DestinationPath exists and the cmdlet is not in Update mode or Overwrite is not specified + if (WriteMode == WriteMode.Create) + { + errorCode = ErrorCode.ArchiveExists; + } + // Throw an error if the cmdlet is in Update mode but the archive is read only + else if (WriteMode == WriteMode.Update && _destinationPathInfo.Attributes.HasFlag(FileAttributes.ReadOnly)) + { + errorCode = ErrorCode.ArchiveReadOnly; + } } } - // If DestinationPath is an existing file - else + // Throw an error if DestinationPath does not exist and cmdlet is in Update mode + else if (WriteMode == WriteMode.Update) { - // Throw an error if DestinationPath exists and the cmdlet is not in Update mode or Overwrite is not specified - if (WriteMode == WriteMode.Create) - { - errorCode = ErrorCode.ArchiveExists; - } - // Throw an error if the cmdlet is in Update mode but the archive is read only - else if (WriteMode == WriteMode.Update && _destinationPathInfo.Attributes.HasFlag(FileAttributes.ReadOnly)) - { - errorCode = ErrorCode.ArchiveReadOnly; - } + errorCode = ErrorCode.ArchiveDoesNotExist; } if (errorCode is not null) @@ -288,6 +286,9 @@ private void ValidateDestinationPath() var errorRecord = ErrorMessages.GetErrorRecord(errorCode: errorCode.Value, errorItem: _destinationPathInfo.FullName); ThrowTerminatingError(errorRecord); } + + // Determine archive format based on the extension of DestinationPath + DetermineArchiveFormat(); } private void DeleteDestinationPathIfExists() diff --git a/src/ErrorMessages.cs b/src/ErrorMessages.cs index 2040b92..4cb7cd9 100644 --- a/src/ErrorMessages.cs +++ b/src/ErrorMessages.cs @@ -1,4 +1,7 @@ -using Microsoft.PowerShell.Archive.Localized; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.PowerShell.Archive.Localized; using System; using System.Management.Automation; diff --git a/src/IArchive.cs b/src/IArchive.cs index 5903ee5..a785a5f 100644 --- a/src/IArchive.cs +++ b/src/IArchive.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; using System.Collections.Generic; using System.Text; diff --git a/src/Microsoft.PowerShell.Archive.psd1 b/src/Microsoft.PowerShell.Archive.psd1 index 373ce80..a047d5a 100644 --- a/src/Microsoft.PowerShell.Archive.psd1 +++ b/src/Microsoft.PowerShell.Archive.psd1 @@ -1,5 +1,4 @@ @{ -RootModule = '' ModuleVersion = '2.0.1' GUID = '06a335eb-dd10-4d25-b753-4f6a80163516' Author = 'Microsoft' @@ -8,10 +7,7 @@ Copyright = '(c) Microsoft. All rights reserved.' Description = 'PowerShell module for creating and expanding archives.' PowerShellVersion = '7.2.5' NestedModules = @('Microsoft.PowerShell.Archive.dll') -FunctionsToExport = '' CmdletsToExport = @('Compress-Archive') -VariablesToExport = '' -AliasesToExport = '' PrivateData = @{ PSData = @{ Tags = @('Archive', 'Zip', 'Compress') diff --git a/src/PathHelper.cs b/src/PathHelper.cs index c9ece93..3bf5c3d 100644 --- a/src/PathHelper.cs +++ b/src/PathHelper.cs @@ -1,11 +1,12 @@ -using Microsoft.PowerShell.Archive.Localized; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.PowerShell.Archive.Localized; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Management.Automation; -using System.Net.NetworkInformation; -using System.Text; namespace Microsoft.PowerShell.Archive { diff --git a/src/TarArchive.cs b/src/TarArchive.cs index 0dcb148..da25815 100644 --- a/src/TarArchive.cs +++ b/src/TarArchive.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; using System.Collections.Generic; using System.Formats.Tar; using System.IO; diff --git a/src/WriteMode.cs b/src/WriteMode.cs index 88566b1..00efad3 100644 --- a/src/WriteMode.cs +++ b/src/WriteMode.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; using System.Collections.Generic; using System.Text; diff --git a/src/ZipArchive.cs b/src/ZipArchive.cs index 0a95b2c..1cdadad 100644 --- a/src/ZipArchive.cs +++ b/src/ZipArchive.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; using System.Collections.Generic; using System.IO.Compression; using System.Text; From f7c2af4b0910765a50b09933aae53f100d24a640 Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Wed, 3 Aug 2022 17:00:58 -0700 Subject: [PATCH 38/42] updated .gitignore, added explanation for why ArchiveAddition.EntryName is not necessarily equal to FileSystemInfo.Name --- .azdevops/CI.yml | 4 +- .gitignore | 439 ++++++++--------------------------------- src/ArchiveAddition.cs | 9 + 3 files changed, 89 insertions(+), 363 deletions(-) diff --git a/.azdevops/CI.yml b/.azdevops/CI.yml index cb955ad..8c2b168 100644 --- a/.azdevops/CI.yml +++ b/.azdevops/CI.yml @@ -35,7 +35,7 @@ stages: includePreviewVersions: true - pwsh: | - & "$(Build.SourcesDirectory)\SimpleBuild.ps1" + & "$(Build.SourcesDirectory)\Build.ps1" displayName: Build Microsoft.PowerShell.Archive module - task: CopyFiles@2 @@ -45,8 +45,6 @@ stages: contents: '**' targetFolder: '$(Build.ArtifactStagingDirectory)/Microsoft.PowerShell.Archive' - - pwsh: | - dir "$(Build.ArtifactStagingDirectory)/*" -Recurse - publish: '$(Build.ArtifactStagingDirectory)/Microsoft.PowerShell.Archive' displayName: 'Publish module build' artifact: ModuleBuild diff --git a/.gitignore b/.gitignore index f9af6f6..6fbb448 100644 --- a/.gitignore +++ b/.gitignore @@ -1,372 +1,91 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -# VSCode -.vscode - -# Auto-generated resx code -src/Localized/Messages.Designer.cs - -# Visual Studio Launch Settings -src/Properties +bin/ +obj/ +.ionide/ +project.lock.json +*-tests.xml +/debug/ +/staging/ +/Packages/ +*.nuget.props -# User-specific files -*.rsuser +# VS auto-generated solution files for project.json solutions +*.xproj +*.xproj.user *.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Oo]ut/ -[Ll]og/ -[Ll]ogs/ +# VS auto-generated files for csproj files +*.csproj.user -# Visual Studio 2015/2017 cache/options directory +# Visual Studio IDE directory .vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml +# Visual Studio Configuration Settings +src/Properties/ -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c +# VSCode directories that are not at the repository root +/**/.vscode/ -# Benchmark Results -BenchmarkDotNet.Artifacts/ +# Project Rider IDE files +.idea.powershell/ -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# ASP.NET Scaffolding -ScaffoldingReadMe.txt - -# StyleCop -StyleCopReport.xml +# Ignore executables +*.exe +*.msi +*.appx +*.msix -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch +# Ignore binaries and symbols *.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Coverlet is a free, cross platform Code Coverage Tool -coverage*.json -coverage*.xml -coverage*.info - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages +*.dll +*.wixpdb + +# Ignore packages +*.deb +*.tar.gz +*.zip +*.rpm +*.pkg *.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ - -# Fody - auto-generated XML schema -FodyWeavers.xsd +*.AppImage + +# default location for produced nuget packages +/nuget-artifacts + +# resgen output +gen + +# Per repo profile +.profile.ps1 + +# macOS +.DS_Store +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk +.AppleDouble +.LSOverride + +# TestsResults +TestsResults*.xml +ParallelXUnitResults.xml +xUnitResults.xml + +# Resharper settings +PowerShell.sln.DotSettings.user +*.msp +StyleCop.Cache + +# Ignore SelfSignedCertificate autogenerated files +test/tools/Modules/SelfSignedCertificate/ + +# BenchmarkDotNet artifacts +test/perf/BenchmarkDotNet.Artifacts/ \ No newline at end of file diff --git a/src/ArchiveAddition.cs b/src/ArchiveAddition.cs index c19e32f..d0e8337 100644 --- a/src/ArchiveAddition.cs +++ b/src/ArchiveAddition.cs @@ -16,6 +16,15 @@ internal class ArchiveAddition /// /// The name of the file or directory in the archive. /// This is a path of the file or directory in the archive (e.g., 'file1.txt` means the file is a top-level file in the archive). + /// + /// Does EntryName == FileSystemInfo.Name? This is not always true because EntryName can contain ancestor directories due to path directory structure preservation or due to the user + /// archiving parent directories. + /// For example, supoose we have the following directory + /// grandparent + /// |---parent + /// |---file.txt + /// If we want to add or update grandparent to/in the archive, grandparent would be recursed for its descendents. This means the EntryName of file.txt would become + /// `grandparent/parent/file.txt` so that when expanding the archive, file.txt is put in the correct location (directly under parent and under grandparent). /// internal string EntryName { get; set; } From f7cdfd21645d53178305e29a5237bdf61642a08c Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Thu, 4 Aug 2022 13:38:38 -0700 Subject: [PATCH 39/42] updated Compress-Archive cmdlet to resolve a path one at a time rather than collecting all paths first --- src/ArchiveMode.cs | 4 - src/CompressArchiveCommand.cs | 158 ++++++----- src/ErrorMessages.cs | 7 + src/Localized/Messages.resx | 4 +- src/PathHelper.cs | 495 +++++++++++++++------------------- 5 files changed, 321 insertions(+), 347 deletions(-) diff --git a/src/ArchiveMode.cs b/src/ArchiveMode.cs index 2c07b85..1f99a37 100644 --- a/src/ArchiveMode.cs +++ b/src/ArchiveMode.cs @@ -1,10 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; -using System.Collections.Generic; -using System.Text; - namespace Microsoft.PowerShell.Archive { internal enum ArchiveMode diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index ce30438..126085f 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -7,8 +7,8 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.IO.Compression; -using System.Linq; using System.Management.Automation; +using System.Runtime.InteropServices; using Microsoft.PowerShell.Archive.Localized; @@ -68,85 +68,105 @@ private enum ParameterSet [Parameter()] public ArchiveFormat? Format { get; set; } = null; - // Stores paths from -Path parameter - private List? _literalPaths; + private readonly PathHelper _pathHelper; - // Stores paths from -LiteralPath parameter - private List? _nonliteralPaths; + private bool _didCreateNewArchive; - private readonly PathHelper _pathHelper; + // Stores paths + private HashSet? _paths; - private FileSystemInfo? _destinationPathInfo; + // This is used so the cmdlet can show all nonexistent paths at once to the user + private HashSet _nonexistentPaths; - private bool _didCreateNewArchive; + // Keeps track of duplicate paths so the cmdlet can show them all at once to the user + private HashSet _duplicatePaths; + + // Keeps track of whether any source path is equal to the destination path + // Since we are already checking for duplicates, only a bool is necessary and not a List or a HashSet + // Only 1 path could be equal to the destination path after filtering for duplicates + private bool _isSourcePathEqualToDestinationPath; public CompressArchiveCommand() { - _literalPaths = new List(); - _nonliteralPaths = new List(); _pathHelper = new PathHelper(this); Messages.Culture = new System.Globalization.CultureInfo("en-US"); _didCreateNewArchive = false; - _destinationPathInfo = null; + _paths = new HashSet( RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase); + _nonexistentPaths = new HashSet( RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase); + _duplicatePaths = new HashSet( RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase); } protected override void BeginProcessing() { - _destinationPathInfo = _pathHelper.ResolveToSingleFullyQualifiedPath(DestinationPath); - DestinationPath = _destinationPathInfo.FullName; + // This resolves the path to a fully qualified path and handles provider exceptions + DestinationPath = _pathHelper.GetUnresolvedPathFromPSProviderPath(DestinationPath); ValidateDestinationPath(); } protected override void ProcessRecord() { - // Add each path from -Path or -LiteralPath to _nonliteralPaths or _literalPaths because they can get lost when the next item in the pipeline is sent if (ParameterSetName == nameof(ParameterSet.Path)) { Debug.Assert(Path is not null); - _nonliteralPaths?.AddRange(Path); + foreach (var path in Path) { + var resolvedPaths = _pathHelper.GetResolvedPathFromPSProviderPath(path, _nonexistentPaths); + if (resolvedPaths is not null) { + foreach (var resolvedPath in resolvedPaths) { + // Add resolvedPath to _path + AddPathToPaths(pathToAdd: resolvedPath); + } + } + } + } else { Debug.Assert(LiteralPath is not null); - _literalPaths?.AddRange(LiteralPath); + foreach (var path in LiteralPath) { + var unresolvedPath = _pathHelper.GetUnresolvedPathFromPSProviderPath(path, _nonexistentPaths); + if (unresolvedPath is not null) { + // Add unresolvedPath to _path + AddPathToPaths(pathToAdd: unresolvedPath); + } + } } } protected override void EndProcessing() { - Debug.Assert(_destinationPathInfo is not null); - Debug.Assert(_literalPaths is not null); - Debug.Assert(_nonliteralPaths is not null); + // If there are non-existent paths, throw a terminating error + if (_nonexistentPaths.Count > 0) { + // Get a comma-seperated string containg the non-existent paths + string commaSeperatedNonExistentPaths = string.Join(',', _nonexistentPaths); + var errorRecord = ErrorMessages.GetErrorRecord(ErrorCode.InvalidPath, commaSeperatedNonExistentPaths); + ThrowTerminatingError(errorRecord); + } - // Get archive entries, validation is performed by PathHelper - // _literalPaths should not be null at this stage, but if it is, prevent a NullReferenceException by doing the following - List archiveAdditions = _pathHelper.GetArchiveAdditionsForPath(paths: _literalPaths.ToArray(), literalPath: true); + // If there are duplicate paths, throw a terminating error + if (_duplicatePaths.Count > 0) { + // Get a comma-seperated string containg the non-existent paths + string commaSeperatedDuplicatePaths = string.Join(',', _nonexistentPaths); + var errorRecord = ErrorMessages.GetErrorRecord(ErrorCode.DuplicatePaths, commaSeperatedDuplicatePaths); + ThrowTerminatingError(errorRecord); + } - // Do the same as above for _nonliteralPaths - List? nonliteralArchiveAdditions = _pathHelper.GetArchiveAdditionsForPath(paths: _nonliteralPaths.ToArray(), literalPath: false); + // If a source path is the same as the destination path, throw a terminating error + // We don't want to overwrite the file or directory that we want to add to the archive. + if (_isSourcePathEqualToDestinationPath) { + var errorCode = ParameterSetName == nameof(ParameterSet.Path) ? ErrorCode.SamePathAndDestinationPath : ErrorCode.SameLiteralPathAndDestinationPath; + var errorRecord = ErrorMessages.GetErrorRecord(errorCode); + ThrowTerminatingError(errorRecord); + } - // Add nonliteralArchiveAdditions to archive additions, so we can keep track of one list only - archiveAdditions.AddRange(nonliteralArchiveAdditions); + // Get archive entries + // If a path causes an exception (e.g., SecurityException), _pathHelper should handle it + List archiveAdditions = _pathHelper.GetArchiveAdditions(_paths); - // Remove references to _sourcePaths, Path, and LiteralPath to free up memory + // Remove references to _paths, Path, and LiteralPath to free up memory // The user could have supplied a lot of paths, so we should do this Path = null; LiteralPath = null; - _literalPaths = null; - _nonliteralPaths = null; - // Remove reference to nonliteralArchiveAdditions since we do not use it any more - nonliteralArchiveAdditions = null; - - // Throw a terminating error if there is a source path as same as DestinationPath. - // We don't want to overwrite the file or directory that we want to add to the archive. - var additionsWithSamePathAsDestination = archiveAdditions.Where(addition => PathHelper.ArePathsSame(addition.FileSystemInfo, _destinationPathInfo)).ToList(); - if (additionsWithSamePathAsDestination.Count > 0) - { - // Since duplicate checking is performed earlier, there must a single ArchiveAddition such that ArchiveAddition.FullPath == DestinationPath - var errorCode = ParameterSetName == nameof(ParameterSet.Path) ? ErrorCode.SamePathAndDestinationPath : ErrorCode.SameLiteralPathAndDestinationPath; - var errorRecord = ErrorMessages.GetErrorRecord(errorCode, errorItem: additionsWithSamePathAsDestination[0].FileSystemInfo.FullName); - ThrowTerminatingError(errorRecord); - } + _paths = null; // Warn the user if there are no items to add for some reason (e.g., no items matched the filter) if (archiveAdditions.Count == 0) @@ -161,20 +181,18 @@ protected override void EndProcessing() IArchive? archive = null; try { - if (ShouldProcess(target: _destinationPathInfo.FullName, action: Messages.Create)) + if (ShouldProcess(target: DestinationPath, action: Messages.Create)) { // If the WriteMode is overwrite, delete the existing archive if (WriteMode == WriteMode.Overwrite) { DeleteDestinationPathIfExists(); - _destinationPathInfo = new FileInfo(_destinationPathInfo.FullName); } // Create an archive -- this is where we will switch between different types of archives archive = ArchiveFactory.GetArchive(format: Format ?? ArchiveFormat.Zip, archivePath: DestinationPath, archiveMode: archiveMode, compressionLevel: CompressionLevel); - _didCreateNewArchive = archiveMode == ArchiveMode.Update; + _didCreateNewArchive = archiveMode != ArchiveMode.Update; } - long numberOfAdditions = archiveAdditions.Count; long numberOfAddedItems = 0; @@ -212,7 +230,7 @@ protected override void EndProcessing() // If -PassThru is specified, write a System.IO.FileInfo object if (PassThru) { - WriteObject(_destinationPathInfo); + WriteObject(new FileInfo(DestinationPath)); } } @@ -221,7 +239,7 @@ protected override void StopProcessing() // If a new output archive was created, delete it (this does not delete an archive if -WriteMode Update is specified) if (_didCreateNewArchive) { - _destinationPathInfo?.Delete(); + DeleteDestinationPathIfExists(); } } @@ -230,13 +248,12 @@ protected override void StopProcessing() /// private void ValidateDestinationPath() { - Debug.Assert(_destinationPathInfo is not null); ErrorCode? errorCode = null; - if (_destinationPathInfo.Exists) + if (System.IO.Path.Exists(DestinationPath)) { // Check if DestinationPath is an existing directory - if (_destinationPathInfo.Attributes.HasFlag(FileAttributes.Directory)) + if (Directory.Exists(DestinationPath)) { // Throw an error if DestinationPath exists and the cmdlet is not in Update mode or Overwrite is not specified if (WriteMode == WriteMode.Create) @@ -249,12 +266,12 @@ private void ValidateDestinationPath() errorCode = ErrorCode.ArchiveExistsAsDirectory; } // Throw an error if the DestinationPath is the current working directory and the cmdlet is in Overwrite mode - else if (WriteMode == WriteMode.Overwrite && _destinationPathInfo.FullName == SessionState.Path.CurrentFileSystemLocation.ProviderPath) + else if (WriteMode == WriteMode.Overwrite && DestinationPath == SessionState.Path.CurrentFileSystemLocation.ProviderPath) { errorCode = ErrorCode.CannotOverwriteWorkingDirectory; } // Throw an error if the DestinationPath is a directory with at 1 least item and the cmdlet is in Overwrite mode - else if (WriteMode == WriteMode.Overwrite && _destinationPathInfo is DirectoryInfo directory && directory.GetFileSystemInfos().Length > 0) + else if (WriteMode == WriteMode.Overwrite && Directory.GetFileSystemEntries(DestinationPath).Length > 0) { errorCode = ErrorCode.ArchiveIsNonEmptyDirectory; } @@ -268,7 +285,7 @@ private void ValidateDestinationPath() errorCode = ErrorCode.ArchiveExists; } // Throw an error if the cmdlet is in Update mode but the archive is read only - else if (WriteMode == WriteMode.Update && _destinationPathInfo.Attributes.HasFlag(FileAttributes.ReadOnly)) + else if (WriteMode == WriteMode.Update && File.GetAttributes(DestinationPath).HasFlag(FileAttributes.ReadOnly)) { errorCode = ErrorCode.ArchiveReadOnly; } @@ -283,7 +300,7 @@ private void ValidateDestinationPath() if (errorCode is not null) { // Throw an error -- since we are validating DestinationPath, the problem is with DestinationPath - var errorRecord = ErrorMessages.GetErrorRecord(errorCode: errorCode.Value, errorItem: _destinationPathInfo.FullName); + var errorRecord = ErrorMessages.GetErrorRecord(errorCode: errorCode.Value, errorItem: DestinationPath); ThrowTerminatingError(errorRecord); } @@ -293,37 +310,39 @@ private void ValidateDestinationPath() private void DeleteDestinationPathIfExists() { - Debug.Assert(_destinationPathInfo is not null); try { // No need to ensure DestinationPath has no children when deleting it // because ValidateDestinationPath should have already done this - if (_destinationPathInfo.Exists) + if (File.Exists(DestinationPath)) { - _destinationPathInfo.Delete(); + File.Delete(DestinationPath); + } + else if (Directory.Exists(DestinationPath)) + { + Directory.Delete(DestinationPath); } } // Throw a terminating error if an IOException occurs catch (IOException ioException) { var errorRecord = new ErrorRecord(ioException, errorId: nameof(ErrorCode.OverwriteDestinationPathFailed), - errorCategory: ErrorCategory.InvalidOperation, targetObject: _destinationPathInfo.FullName); + errorCategory: ErrorCategory.InvalidOperation, targetObject: DestinationPath); ThrowTerminatingError(errorRecord); } // Throw a terminating error if an UnauthorizedAccessException occurs catch (System.UnauthorizedAccessException unauthorizedAccessException) { var errorRecord = new ErrorRecord(unauthorizedAccessException, errorId: nameof(ErrorCode.InsufficientPermissionsToAccessPath), - errorCategory: ErrorCategory.PermissionDenied, targetObject: _destinationPathInfo.FullName); + errorCategory: ErrorCategory.PermissionDenied, targetObject: DestinationPath); ThrowTerminatingError(errorRecord); } } private void DetermineArchiveFormat() { - Debug.Assert(_destinationPathInfo is not null); // Check if cmdlet is able to determine the format of the archive based on the extension of DestinationPath - bool ableToDetermineArchiveFormat = ArchiveFactory.TryGetArchiveFormatFromExtension(path: _destinationPathInfo.FullName, archiveFormat: out var archiveFormat); + bool ableToDetermineArchiveFormat = ArchiveFactory.TryGetArchiveFormatFromExtension(path: DestinationPath, archiveFormat: out var archiveFormat); // If the user did not specify which archive format to use, try to determine it automatically if (Format is null) { @@ -334,7 +353,7 @@ private void DetermineArchiveFormat() else { // If the archive format could not be determined, use zip by default and emit a warning - var warningMsg = string.Format(Messages.ArchiveFormatCouldNotBeDeterminedWarning, _destinationPathInfo.FullName); + var warningMsg = string.Format(Messages.ArchiveFormatCouldNotBeDeterminedWarning, DestinationPath); WriteWarning(warningMsg); Format = ArchiveFormat.Zip; } @@ -347,10 +366,21 @@ private void DetermineArchiveFormat() { if (archiveFormat is null || archiveFormat.Value != Format.Value) { - var warningMsg = string.Format(Messages.ArchiveExtensionDoesNotMatchArchiveFormatWarning, _destinationPathInfo.FullName); + var warningMsg = string.Format(Messages.ArchiveExtensionDoesNotMatchArchiveFormatWarning, DestinationPath); WriteWarning(warningMsg); } } } + + // Adds a path to _paths variable + // If the path being added is a duplicate, it adds it _duplicatePaths (if it is not already there) + // If the path is the same as the destination path, it sets _isSourcePathEqualToDestinationPath to true + private void AddPathToPaths(string pathToAdd) { + if (!_paths.Add(pathToAdd)) { + _duplicatePaths.Add(pathToAdd); + } else if (!_isSourcePathEqualToDestinationPath && pathToAdd == DestinationPath) { + _isSourcePathEqualToDestinationPath = true; + } + } } } diff --git a/src/ErrorMessages.cs b/src/ErrorMessages.cs index 4cb7cd9..80ec0c5 100644 --- a/src/ErrorMessages.cs +++ b/src/ErrorMessages.cs @@ -16,6 +16,13 @@ internal static ErrorRecord GetErrorRecord(ErrorCode errorCode, string errorItem return new ErrorRecord(exception, errorCode.ToString(), ErrorCategory.InvalidArgument, errorItem); } + internal static ErrorRecord GetErrorRecord(ErrorCode errorCode) + { + var errorMsg = GetErrorMessage(errorCode: errorCode); + var exception = new ArgumentException(errorMsg); + return new ErrorRecord(exception, errorCode.ToString(), ErrorCategory.InvalidArgument, null); + } + internal static string GetErrorMessage(ErrorCode errorCode) { return errorCode switch diff --git a/src/Localized/Messages.resx b/src/Localized/Messages.resx index c5535d6..e230be9 100644 --- a/src/Localized/Messages.resx +++ b/src/Localized/Messages.resx @@ -175,9 +175,9 @@ {0}% complete - A path {0} supplied to -LiteralPath is the same as the path supplied to -DestinationPath. + A path supplied to -LiteralPath is the same as the path supplied to -DestinationPath. - A path {0} supplied to -Path is the same as the path supplied to -DestinationPath. + A path supplied to -Path is the same as the path supplied to -DestinationPath. \ No newline at end of file diff --git a/src/PathHelper.cs b/src/PathHelper.cs index 3bf5c3d..dbcf487 100644 --- a/src/PathHelper.cs +++ b/src/PathHelper.cs @@ -4,6 +4,7 @@ using Microsoft.PowerShell.Archive.Localized; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Management.Automation; @@ -21,184 +22,17 @@ internal PathHelper(PSCmdlet cmdlet) _cmdlet = cmdlet; } - /// - /// Get a list of ArchiveAddition objects from an array of paths depending on whether we want to use the path literally or not. - /// - /// An array of paths, relative or absolute -- they do not necessarily have to be fully qualifed paths. - /// If true, wildcard characters in each path are expanded. If false, wildcard characters are not expanded. - /// - internal List GetArchiveAdditionsForPath(string[] paths, bool literalPath) + internal List GetArchiveAdditions(HashSet fullyQualifiedPaths) { - List additions = new List(); - - // Used to keep track of non-filesystem paths - HashSet nonfilesystemPaths = new HashSet(); - - foreach (var path in paths) - { - // Based on the value of literalPath, call the appropriate method - if (literalPath) - { - AddArchiveAdditionForUserEnteredLiteralPath(path: path, archiveAdditions: additions, nonfilesystemPaths: nonfilesystemPaths); - } else - { - AddArchiveAdditionForUserEnteredNonLiteralPath(path: path, archiveAdditions: additions, nonfilesystemPaths: nonfilesystemPaths); - } - } - - // If there is at least 1 non-filesystem path, throw an invalid path error - if (nonfilesystemPaths.Count > 0) - { - // Get an error record and throw it - var commaSperatedPaths = string.Join(separator: ',', values: nonfilesystemPaths); - var errorRecord = ErrorMessages.GetErrorRecord(errorCode: ErrorCode.InvalidPath, errorItem: commaSperatedPaths); - _cmdlet.ThrowTerminatingError(errorRecord: errorRecord); - } - - // If there are duplicate paths, throw an error - var duplicates = GetDuplicatePaths(additions); - if (duplicates.Any()) - { - // Get an error record and throw it - var commaSperatedPaths = string.Join(separator: ',', values: duplicates); - var errorRecord = ErrorMessages.GetErrorRecord(errorCode: ErrorCode.DuplicatePaths, errorItem: commaSperatedPaths); - _cmdlet.ThrowTerminatingError(errorRecord: errorRecord); - } - - return additions; - } - - /// - /// Resolves a user-entered path while expanding wildcards, creates an ArchiveAddition object for it, and its to the list of ArchiveAddition objects - /// - /// - /// - /// - private void AddArchiveAdditionForUserEnteredNonLiteralPath(string path, List archiveAdditions, HashSet nonfilesystemPaths) - { - // Keep the exception at the top, then when an error occurs, use the exception to create an ErrorRecord - Exception? exception = null; - try - { - // Resolve the path -- I don't think we need to handle exceptions here as no special behavior occurs when an exception occurs - var resolvedPaths = _cmdlet.SessionState.Path.GetResolvedProviderPathFromPSPath(path: path, provider: out var providerInfo); - - // Check if the path if from the filesystem - if (providerInfo?.Name != FileSystemProviderName) - { - // If not, add the path to the set of non-filesystem paths. We will throw an error later so we can show the user all invalid paths at once - foreach (var resolvedPath in resolvedPaths) - { - nonfilesystemPaths.Add(resolvedPath); - } - return; - } - - // Check if the cmdlet can preserve paths based on path variable - bool shouldPreservePathStructure = CanPreservePathStructure(path); - - // Go through each resolved path and add an ArchiveAddition for it to additions - for (int i = 0; i < resolvedPaths.Count; i++) - { - var resolvedPath = resolvedPaths[i]; - AddAdditionForFullyQualifiedPath(path: resolvedPath, additions: archiveAdditions, shouldPreservePathStructure: shouldPreservePathStructure); - } - } - catch (System.Management.Automation.ProviderNotFoundException providerNotFoundException) + List archiveAdditions = new List(fullyQualifiedPaths.Count); + foreach (var path in fullyQualifiedPaths) { - exception = providerNotFoundException; - } - catch (System.Management.Automation.DriveNotFoundException driveNotFoundException) - { - exception = driveNotFoundException; - } - catch (System.Management.Automation.ProviderInvocationException providerInvocationException) - { - exception = providerInvocationException; - } - catch (System.Management.Automation.PSNotSupportedException notSupportedException) - { - exception = notSupportedException; - } - catch (System.Management.Automation.PSInvalidOperationException invalidOperationException) - { - exception = invalidOperationException; - } - // Throw a terminating error if the path could not be found - catch (System.Management.Automation.ItemNotFoundException notFoundException) - { - var errorRecord = new ErrorRecord(exception: notFoundException, errorId: nameof(ErrorCode.PathNotFound), errorCategory: ErrorCategory.InvalidArgument, - targetObject: path); - _cmdlet.ThrowTerminatingError(errorRecord); - } - - // If an exception (besides ItemNotFoundException) was caught, write a non-terminating error - if (exception is not null) - { - var errorRecord = new ErrorRecord(exception: exception, errorId: nameof(ErrorCode.InvalidPath), errorCategory: ErrorCategory.InvalidArgument, - targetObject: path); - _cmdlet.WriteError(errorRecord); - } - } - - /// - /// Resolves a user-entered path without expanding wildcards, creates an ArchiveAddition object for it, and its to the list of ArchiveAddition objects - /// - /// - /// - /// - private void AddArchiveAdditionForUserEnteredLiteralPath(string path, List archiveAdditions, HashSet nonfilesystemPaths) - { - // Keep the exception at the top, then when an error occurs, use the exception to create an ErrorRecord - Exception? exception = null; - try - { - // Resolve the path -- gets the fully qualified path - string fullyQualifiedPath = _cmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath(path, out var providerInfo, out var psDriveInfo); - - // Check if the path is from the filesystem - if (providerInfo.Name != FileSystemProviderName) - { - nonfilesystemPaths.Add(path); - return; - } - - // We do not need to check if the path exists here because it is done in AddAdditionForFullyQualifiedMethod call - - // Check if we can preserve the path structure -- this is based on the original path the user entered (not fully qualified) - bool canPreservePathStructure = CanPreservePathStructure(path: path); - - // Add an ArchiveAddition for the path to the list of additions - AddAdditionForFullyQualifiedPath(path: fullyQualifiedPath, additions: archiveAdditions, shouldPreservePathStructure: canPreservePathStructure); - } - catch (System.Management.Automation.ProviderNotFoundException providerNotFoundException) - { - exception = providerNotFoundException; - } - catch (System.Management.Automation.DriveNotFoundException driveNotFoundException) - { - exception = driveNotFoundException; - } - catch (System.Management.Automation.ProviderInvocationException providerInvocationException) - { - exception = providerInvocationException; - } - catch (System.Management.Automation.PSNotSupportedException notSupportedException) - { - exception = notSupportedException; - } - catch (System.Management.Automation.PSInvalidOperationException invalidOperationException) - { - exception = invalidOperationException; - } - - // If an exception was caught, write a non-terminating error - if (exception is not null) - { - var errorRecord = new ErrorRecord(exception: exception, errorId: nameof(ErrorCode.InvalidPath), errorCategory: ErrorCategory.InvalidArgument, - targetObject: path); - _cmdlet.WriteError(errorRecord); + // Assume each path is valid, fully qualified, and existing + Debug.Assert(Path.Exists(path)); + Debug.Assert(Path.IsPathFullyQualified(path)); + AddAdditionForFullyQualifiedPath(path, archiveAdditions); } + return archiveAdditions; } /// @@ -207,28 +41,35 @@ private void AddArchiveAdditionForUserEnteredLiteralPath(string path, ListThe fully qualified path /// The list where to add the ArchiveAddition object for the path /// If true, relative path structure will be preserved. If false, relative path structure will NOT be preserved. - private void AddAdditionForFullyQualifiedPath(string path, List additions, bool shouldPreservePathStructure) + private void AddAdditionForFullyQualifiedPath(string path, List additions) { - System.IO.FileSystemInfo fileSystemInfo = new System.IO.FileInfo(path); - if (System.IO.Directory.Exists(path)) - { - // Add directory seperator to end if it does not already have it - if (!path.EndsWith(System.IO.Path.DirectorySeparatorChar)) path += System.IO.Path.DirectorySeparatorChar; - // Recurse through the child items and add them to additions - var directoryInfo = new System.IO.DirectoryInfo(path); - AddDescendentEntries(directoryInfo: directoryInfo, additions: additions, shouldPreservePathStructure: shouldPreservePathStructure); - fileSystemInfo = directoryInfo; + Debug.Assert(Path.Exists(path)); + FileSystemInfo fileSystemInfo; + + if (Directory.Exists(path)) + { + // If the path is a directory, ensure it does not have a trailing DirectorySeperatorChar and if it does, remove it + // This will make path comparisons easier because the cmdlet won't have to consider whether or not a path has a DirectorySeperatorChar at the end + // i.e., we will avoid the scenario where 1 path has a trailing DirectorySeperatorChar and the other does not (this is not a big deal, but makes life easier) + if (path.EndsWith(Path.DirectorySeparatorChar)) { + path = path.Substring(0, path.Length - 1); + } + fileSystemInfo = new DirectoryInfo(path); } - else if (!System.IO.File.Exists(path)) + else { - // Throw an error if the path does not exist - var errorRecord = ErrorMessages.GetErrorRecord(errorCode: ErrorCode.PathNotFound, errorItem: path); - _cmdlet.ThrowTerminatingError(errorRecord: errorRecord); + fileSystemInfo = new FileInfo(path); } - // Add an entry for the item - var entryName = GetEntryName(fileSystemInfo: fileSystemInfo, shouldPreservePathStructure: shouldPreservePathStructure); + // Get the entry name of the file or directory in the archive + // The cmdlet will preserve the directory structure as long as the path is relative to the working directory + var entryName = GetEntryName(fileSystemInfo, out bool doesPreservePathStructure); additions.Add(new ArchiveAddition(entryName: entryName, fileSystemInfo: fileSystemInfo)); + + // Recurse through the child items and add them to additions + if (fileSystemInfo.Attributes.HasFlag(FileAttributes.Directory) && fileSystemInfo is DirectoryInfo directoryInfo) { + AddDescendentEntries(directoryInfo: directoryInfo, additions: additions, shouldPreservePathStructure: doesPreservePathStructure); + } } /// @@ -243,11 +84,22 @@ private void AddDescendentEntries(System.IO.DirectoryInfo directoryInfo, ListA fully qualified path /// /// - private string GetEntryName(System.IO.FileSystemInfo fileSystemInfo, bool shouldPreservePathStructure) + private string GetEntryName(FileSystemInfo fileSystemInfo, out bool doesPreservePathStructure) { + string entryName; + doesPreservePathStructure = false; // If the path is relative to the current working directory, return the relative path as name - if (shouldPreservePathStructure && TryGetPathRelativeToCurrentWorkingDirectory(path: fileSystemInfo.FullName, out var relativePath)) + if (TryGetPathRelativeToCurrentWorkingDirectory(path: fileSystemInfo.FullName, out var relativePath)) { - return relativePath; + Debug.Assert(relativePath is not null); + doesPreservePathStructure = true; + entryName = relativePath; } // Otherwise, return the name of the directory or file - var entryName = fileSystemInfo.Name; - if (fileSystemInfo.Attributes.HasFlag(FileAttributes.Directory) && !entryName.EndsWith(System.IO.Path.DirectorySeparatorChar)) + else + { + entryName = fileSystemInfo.Name; + } + + if (fileSystemInfo.Attributes.HasFlag(FileAttributes.Directory) && !entryName.EndsWith(Path.DirectorySeparatorChar)) { entryName += System.IO.Path.DirectorySeparatorChar; } @@ -289,7 +149,7 @@ private string GetEntryName(System.IO.FileSystemInfo fileSystemInfo, bool should /// /// /// - private static string GetEntryName(string path, string prefix) + private static string GetEntryNameUsingPrefix(string path, string prefix) { if (prefix == string.Empty) return path; @@ -330,107 +190,188 @@ private static string GetPrefixForPath(System.IO.DirectoryInfo directoryInfo) } return prefix; } - - /// - /// Get the duplicate fully qualified paths from a list of ArchiveAdditions - /// - /// - /// - private static IEnumerable GetDuplicatePaths(List additions) - { - return additions.GroupBy(x => x.FileSystemInfo.FullName) - .Where(group => group.Count() > 1) - .Select(x => x.Key); - } - + /// - /// Resolve a path that may contain wildcard characters and could be a literal or non-literal path to a single fully qualified path. + /// Tries to get a path relative to the current working directory as long as the relative path does not contain ".." /// /// + /// /// - /// - internal System.IO.FileSystemInfo ResolveToSingleFullyQualifiedPath(string path) + private bool TryGetPathRelativeToCurrentWorkingDirectory(string path, out string? relativePathToWorkingDirectory) { - // Currently, all this function does is return the literal fully qualified path of a path + string relativePath = System.IO.Path.GetRelativePath(_cmdlet.SessionState.Path.CurrentFileSystemLocation.Path, path); + relativePathToWorkingDirectory = relativePath.Contains("..") ? null : relativePath; + return relativePathToWorkingDirectory is not null; + } - // First, get non-literal path - string fullyQualifiedPath = _cmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath(path, out var providerInfo, out var psDriveInfo); + internal string[]? GetResolvedPathFromPSProviderPath(string path, HashSet nonexistentPaths) { + // Keep the exception at the top, then when an error occurs, use the exception to create an ErrorRecord + Exception? exception = null; + string[]? fullyQualifiedPaths = null; + try + { + // Resolve path + var resolvedPaths = _cmdlet.SessionState.Path.GetResolvedProviderPathFromPSPath(path, out var providerInfo); - // If the path is not from the filesystem, throw an error - if (providerInfo.Name != FileSystemProviderName) + // If the path is from the filesystem, set it to fullyQualifiedPaths so it can be returned + // Otherwise, create an exception so an error will be written + if (providerInfo.Name != FileSystemProviderName) + { + var exceptionMsg = ErrorMessages.GetErrorMessage(ErrorCode.InvalidPath); + exception = new ArgumentException(exceptionMsg); + } else { + fullyQualifiedPaths = resolvedPaths.ToArray(); + } + } + catch (System.Management.Automation.ProviderNotFoundException providerNotFoundException) { - var errorRecord = ErrorMessages.GetErrorRecord(errorCode: ErrorCode.InvalidPath, errorItem: path); - _cmdlet.ThrowTerminatingError(errorRecord); + exception = providerNotFoundException; + } + catch (System.Management.Automation.DriveNotFoundException driveNotFoundException) + { + exception = driveNotFoundException; + } + catch (System.Management.Automation.ProviderInvocationException providerInvocationException) + { + exception = providerInvocationException; + } + catch (System.Management.Automation.PSNotSupportedException notSupportedException) + { + exception = notSupportedException; + } + catch (System.Management.Automation.PSInvalidOperationException invalidOperationException) + { + exception = invalidOperationException; + } + // If a path can't be found, write an error + catch (System.Management.Automation.ItemNotFoundException) + { + nonexistentPaths.Add(path); } - // Return filesystem info - - return GetFilesystemInfoForPath(fullyQualifiedPath); - } + // If an exception was caught, write a non-terminating error + if (exception is not null) + { + var errorRecord = new ErrorRecord(exception: exception, errorId: nameof(ErrorCode.InvalidPath), errorCategory: ErrorCategory.InvalidArgument, + targetObject: path); + _cmdlet.WriteError(errorRecord); + } - /// - /// Determines if the relative path structure can be preserved - /// - /// - /// - private static bool CanPreservePathStructure(string path) - { - return !System.IO.Path.IsPathRooted(path); + return fullyQualifiedPaths; } - /// - /// Tries to get a path relative to the current working directory as long as the relative path does not contain ".." - /// - /// - /// - /// - private bool TryGetPathRelativeToCurrentWorkingDirectory(string path, out string relativePath) - { - relativePath = System.IO.Path.GetRelativePath(_cmdlet.SessionState.Path.CurrentFileSystemLocation.Path, path); - return !relativePath.Contains(".."); - } + // Resolves a literal path. Does not check if the path exists. + // If an exception occurs with a provider, it throws a terminating error + internal string? GetUnresolvedPathFromPSProviderPath(string path) { + // Keep the exception at the top, then when an error occurs, use the exception to create an ErrorRecord + Exception? exception = null; + string? fullyQualifiedPath = null; + try + { + // Resolve path + var resolvedPath = _cmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath(path, out var providerInfo, out var psDriveInfo); - /// - /// Determines if two paths are the same - /// - /// - /// - /// - internal static bool ArePathsSame(System.IO.FileSystemInfo fileSystemInfo1, System.IO.FileSystemInfo fileSystemInfo2) - { - // If one is a file and the other is a directory, return false - if ((fileSystemInfo1.Attributes.HasFlag(FileAttributes.Directory) && !fileSystemInfo2.Attributes.HasFlag(FileAttributes.Directory)) || - (!fileSystemInfo1.Attributes.HasFlag(FileAttributes.Directory) && fileSystemInfo2.Attributes.HasFlag(FileAttributes.Directory))) + // If the path is from the filesystem, set fullyQualifiedPath to resolvedPath so it can be returned + // Otherwise, create an exception so an error will be written + if (providerInfo.Name != FileSystemProviderName) + { + var exceptionMsg = ErrorMessages.GetErrorMessage(ErrorCode.InvalidPath); + exception = new ArgumentException(exceptionMsg); + } + else + { + fullyQualifiedPath = resolvedPath; + } + } + catch (System.Management.Automation.ProviderNotFoundException providerNotFoundException) { - return false; + exception = providerNotFoundException; + } + catch (System.Management.Automation.DriveNotFoundException driveNotFoundException) + { + exception = driveNotFoundException; + } + catch (System.Management.Automation.ProviderInvocationException providerInvocationException) + { + exception = providerInvocationException; + } + catch (System.Management.Automation.PSNotSupportedException notSupportedException) + { + exception = notSupportedException; + } + catch (System.Management.Automation.PSInvalidOperationException invalidOperationException) + { + exception = invalidOperationException; } - string fullPath1 = fileSystemInfo1.FullName; - string fullPath2 = fileSystemInfo2.FullName; - - // If both are directories, compare their paths - if (fileSystemInfo1.Attributes.HasFlag(FileAttributes.Directory) && fileSystemInfo2.Attributes.HasFlag(FileAttributes.Directory)) + // If an exception was caught, write a non-terminating error of throwError == false. Otherwise, throw a terminating errror + if (exception is not null) { - if (!System.IO.Path.EndsInDirectorySeparator(fullPath1)) fullPath1 += System.IO.Path.DirectorySeparatorChar; - if (!System.IO.Path.EndsInDirectorySeparator(fullPath2)) fullPath2 += System.IO.Path.DirectorySeparatorChar; + var errorRecord = new ErrorRecord(exception: exception, errorId: nameof(ErrorCode.InvalidPath), errorCategory: ErrorCategory.InvalidArgument, + targetObject: path); + _cmdlet.ThrowTerminatingError(errorRecord); } - - return fullPath1 == fullPath2; + return fullyQualifiedPath; } - internal static System.IO.FileSystemInfo GetFilesystemInfoForPath(string path) - { - // Check if path exists - if (System.IO.File.Exists(path)) + // Resolves a literal path. If it does not exist, it adds the path to nonexistentPaths. + // If an exception occurs with a provider, it writes a non-terminating error + internal string? GetUnresolvedPathFromPSProviderPath(string path, HashSet nonexistentPaths) { + // Keep the exception at the top, then when an error occurs, use the exception to create an ErrorRecord + Exception? exception = null; + string? fullyQualifiedPath = null; + try { - return new System.IO.FileInfo(path); + // Resolve path + var resolvedPath = _cmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath(path, out var providerInfo, out var psDriveInfo); + + // If the path is from the filesystem, set fullyQualifiedPath to resolvedPath so it can be returned + // Otherwise, create an exception so an error will be written + if (providerInfo.Name != FileSystemProviderName) + { + var exceptionMsg = ErrorMessages.GetErrorMessage(ErrorCode.InvalidPath); + exception = new ArgumentException(exceptionMsg); + } + // If the path does not exist, create an exception + else if (!Path.Exists(resolvedPath)) { + nonexistentPaths.Add(resolvedPath); + } + else + { + fullyQualifiedPath = resolvedPath; + } + } + catch (System.Management.Automation.ProviderNotFoundException providerNotFoundException) + { + exception = providerNotFoundException; + } + catch (System.Management.Automation.DriveNotFoundException driveNotFoundException) + { + exception = driveNotFoundException; + } + catch (System.Management.Automation.ProviderInvocationException providerInvocationException) + { + exception = providerInvocationException; + } + catch (System.Management.Automation.PSNotSupportedException notSupportedException) + { + exception = notSupportedException; + } + catch (System.Management.Automation.PSInvalidOperationException invalidOperationException) + { + exception = invalidOperationException; } - if (System.IO.Directory.Exists(path)) + + // If an exception was caught, write a non-terminating error + if (exception is not null) { - return new System.IO.DirectoryInfo(path); + var errorRecord = new ErrorRecord(exception: exception, errorId: nameof(ErrorCode.InvalidPath), errorCategory: ErrorCategory.InvalidArgument, + targetObject: path); + _cmdlet.WriteError(errorRecord); } - return path.EndsWith(System.IO.Path.DirectorySeparatorChar) ? new System.IO.DirectoryInfo(path) : new System.IO.FileInfo(path); + + return fullyQualifiedPath; } } } From b5e23c2b07418e4c58e5538dfa03a22f8683c8cf Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Thu, 4 Aug 2022 16:23:40 -0700 Subject: [PATCH 40/42] fixed a bug where PathNotFound error was not thrown, fixed a bug when testing invalid paths --- Tests/Compress-Archive.Tests.ps1 | 59 ++++++++++++++++++++------------ src/CompressArchiveCommand.cs | 2 +- src/PathHelper.cs | 7 ++-- 3 files changed, 42 insertions(+), 26 deletions(-) diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index 20c8979..01f1524 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -136,31 +136,48 @@ } } - It "Validate errors from Compress-Archive when invalid path (non-existing path / non-filesystem path) is supplied for Path or LiteralPath parameters" -ForEach @( - @{ Path = "TestDrive:/InvalidPath" } - @{ Path = @("TestDrive:/", "TestDrive:/InvalidPath") } - ) { + It "Validate errors from Compress-Archive when invalid path is supplied for Path or LiteralPath parameters" -ForEach @( + @{ Path = "Env:/Path" } + @{ Path = @("TestDrive:/", "Env:/Path") } + ) -Tag this1 { $DestinationPath = "TestDrive:/archive2.zip" + Compress-Archive -Path $Path -DestinationPath $DestinationPath -ErrorAction SilentlyContinue -ErrorVariable error + $error.Count | Should -Be 1 + $error[0].FullyQualifiedErrorId | Should -Be "InvalidPath,Microsoft.PowerShell.Archive.CompressArchiveCommand" + Remove-Item -Path $DestinationPath + + Compress-Archive -LiteralPath $Path -DestinationPath $DestinationPath -ErrorAction SilentlyContinue -ErrorVariable error + $error.Count | Should -Be 1 + $error[0].FullyQualifiedErrorId | Should -Be "InvalidPath,Microsoft.PowerShell.Archive.CompressArchiveCommand" + Remove-Item -Path $DestinationPath + } + + It "Throws terminating error when non-existing path is supplied for Path or LiteralPath parameters" -ForEach @( + @{ Path = "TestDrive:/DoesNotExist" } + @{ Path = @("TestDrive:/", "TestDrive:/DoesNotExist") } + ) -Tag this2 { + $DestinationPath = "TestDrive:/archive3.zip" + try - { - Compress-Archive -Path $Path -DestinationPath $DestinationPath - throw "Failed to validate that an invalid Path was supplied as input to Compress-Archive cmdlet." - } - catch - { - $_.FullyQualifiedErrorId | Should -Be "PathNotFound,Microsoft.PowerShell.Archive.CompressArchiveCommand" - } + { + Compress-Archive -Path $Path -DestinationPath $DestinationPath + throw "Failed to validate that an invalid Path was supplied as input to Compress-Archive cmdlet." + } + catch + { + $_.FullyQualifiedErrorId | Should -Be "PathNotFound,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } - try - { - Compress-Archive -LiteralPath $Path -DestinationPath $DestinationPath - throw "Failed to validate that an invalid LiteralPath was supplied as input to Compress-Archive cmdlet." - } - catch - { - $_.FullyQualifiedErrorId | Should -Be "PathNotFound,Microsoft.PowerShell.Archive.CompressArchiveCommand" - } + try + { + Compress-Archive -LiteralPath $Path -DestinationPath $DestinationPath + throw "Failed to validate that an invalid LiteralPath was supplied as input to Compress-Archive cmdlet." + } + catch + { + $_.FullyQualifiedErrorId | Should -Be "PathNotFound,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } } It "Validate error from Compress-Archive when duplicate paths are supplied as input to Path parameter" { diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index 126085f..de521d9 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -138,7 +138,7 @@ protected override void EndProcessing() if (_nonexistentPaths.Count > 0) { // Get a comma-seperated string containg the non-existent paths string commaSeperatedNonExistentPaths = string.Join(',', _nonexistentPaths); - var errorRecord = ErrorMessages.GetErrorRecord(ErrorCode.InvalidPath, commaSeperatedNonExistentPaths); + var errorRecord = ErrorMessages.GetErrorRecord(ErrorCode.PathNotFound, commaSeperatedNonExistentPaths); ThrowTerminatingError(errorRecord); } diff --git a/src/PathHelper.cs b/src/PathHelper.cs index dbcf487..ce58bc4 100644 --- a/src/PathHelper.cs +++ b/src/PathHelper.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Linq; using System.Management.Automation; namespace Microsoft.PowerShell.Archive @@ -204,10 +203,10 @@ private bool TryGetPathRelativeToCurrentWorkingDirectory(string path, out string return relativePathToWorkingDirectory is not null; } - internal string[]? GetResolvedPathFromPSProviderPath(string path, HashSet nonexistentPaths) { + internal System.Collections.ObjectModel.Collection? GetResolvedPathFromPSProviderPath(string path, HashSet nonexistentPaths) { // Keep the exception at the top, then when an error occurs, use the exception to create an ErrorRecord Exception? exception = null; - string[]? fullyQualifiedPaths = null; + System.Collections.ObjectModel.Collection? fullyQualifiedPaths = null; try { // Resolve path @@ -220,7 +219,7 @@ private bool TryGetPathRelativeToCurrentWorkingDirectory(string path, out string var exceptionMsg = ErrorMessages.GetErrorMessage(ErrorCode.InvalidPath); exception = new ArgumentException(exceptionMsg); } else { - fullyQualifiedPaths = resolvedPaths.ToArray(); + fullyQualifiedPaths = resolvedPaths; } } catch (System.Management.Automation.ProviderNotFoundException providerNotFoundException) From fcaaa8ed1fb19e300990a529b9092c2dceed8855 Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Fri, 5 Aug 2022 17:06:54 -0700 Subject: [PATCH 41/42] added custom assertions for testing zip archives --- .../Should-BeZipArchiveOnlyContaining.psm1 | 144 ++++++++++++++++++ Tests/Compress-Archive.Tests.ps1 | 136 ++++------------- Tests/Install-7Zip.ps1 | 13 ++ src/ZipArchive.cs | 4 +- 4 files changed, 190 insertions(+), 107 deletions(-) create mode 100644 Tests/Assertions/Should-BeZipArchiveOnlyContaining.psm1 create mode 100644 Tests/Install-7Zip.ps1 diff --git a/Tests/Assertions/Should-BeZipArchiveOnlyContaining.psm1 b/Tests/Assertions/Should-BeZipArchiveOnlyContaining.psm1 new file mode 100644 index 0000000..2f3ad8e --- /dev/null +++ b/Tests/Assertions/Should-BeZipArchiveOnlyContaining.psm1 @@ -0,0 +1,144 @@ +function Should-BeZipArchiveOnlyContaining { + <# + .SYNOPSIS + Checks if a zip archive contains the entries $ExpectedValue + .EXAMPLE + "C:\Users\\archive.zip" | Should -BeZipArchiveContaining @("file1.txt") + + Checks if archive.zip only contains file1.txt + #> + + [CmdletBinding()] + Param ( + [string] $ActualValue, + [string[]] $ExpectedValue, + [switch] $Negate, + [string] $Because, + [switch] $LiteralPath, + $CallerSessionState + ) + + # ActualValue is supposed to be a path to an archive + # It could be a path to a custom PSDrive, so it needes to be converted + if ($LiteralPath) { + $ActualValue = Convert-Path -LiteralPath $ActualValue + } else { + $ActualValue = Convert-Path -Path $ActualValue + } + + + # Ensure ActualValue is a valid path + if ($LiteralPath) { + $testPathResult = Test-Path -LiteralPath $ActualValue + } else { + $testPathResult = Test-Path -Path $ActualValue + } + + # Don't continue processing if ActualValue is not an actual path + # Determine if the assertion succeeded or failed and then return + if (-not $testPathResult) { + $succeeded = $Negate + if (-not $succeeded) { + $failureMessage = "The path ${ActualValue} does not exist" + } + return [pscustomobject]@{ + Succeeded = $succeeded + FailureMessage = $failureMessage + } + } + + # Get 7-zip to list the contents of the archive + $output = 7z.exe l $ActualValue -ba + + # Check if the output is null + if ($null -eq $output) { + if ($null -eq $ExpectedValue -or $ExpectedValue.Length -eq 0) { + $succeeded = -not $Negate + } else { + $succeeded = $Negate + } + + if (-not $succeeded) { + $failureMessage = "Archive {0} contains nothing, but it was expected to contain something" + } + + return [pscustomobject]@{ + Succeeded = $succeeded + FailureMessage = $failureMessage + } + } + + # Filter the output line by line + $lines = $output -split [System.Environment]::NewLine + + # Stores the entry names + $entryNames = @() + + # Go through each line and split it by whitespace + foreach ($line in $lines) { + $lineComponents = $line -split " +" + + # Example of some lines: + #2022-08-05 15:54:04 D.... 0 0 SourceDir + #2022-08-05 15:54:04 ..... 11 11 SourceDir/Sample-1.txt + + # First component is date + # 2nd component is time + # 3rd componnent is attributes + # 4th component is size + # 5th component is compressed size + # 6th component is entry name + + $entryName = $lineComponents[$lineComponents.Length - 1] + + # Since 7zip does not show trailing forwardslash for directories, we need to check the attributes to see if it starts with 'D' + # If so, it means the entry is a directory and we should append a forwardslash to the entry name + + if ($lineComponents[2].StartsWith('D')) { + $entryName += '/' + } + + # Replace backslashes to forwardslashes + $dirSeperatorChar = [System.IO.Path]::DirectorySeparatorChar + $entryName = $entryName.Replace($dirSeperatorChar, "/") + + $entryNames += $entryName + } + + $itemsNotInArchive = @() + + # Go through each item in ExpectedValue and ensure it is in entryNames + foreach ($expectedItem in $ExpectedValue) { + if ($entryNames -notcontains $expectedItem) { + $itemsNotInArchive += $expectedItem + } + } + + if ($itemsNotInArchive.Length -gt 0 -and -not $Negate) { + # Create a comma-seperated string from $itemsNotInEnryName + $commaSeperatedItemsNotInArchive = $itemsNotInArchive -join "," + $failureMessage = "'$ActualValue' does not contain $commaSeperatedItemsNotInArchive $(if($Because) { "because $Because"})." + $succeeded = $false + } + + # Ensure the length of $entryNames is equal to that of $ExpectedValue + if ($null -eq $succeeded -and $entryNames.Length -ne $ExpectedValue.Length -and -not $Negate) { + $failureMessage = "${ActualValue} does not contain the same number of items as ${ExpectedValue -join ""} (expected ${ExpectedValue.Length} entries but found ${entryNames.Length}) $(if($Because) { "because $Because"})." + $succeeded = $false + } + + if ($null -eq $succeeded) { + $succeeded = -not $Negate + if (-not $succeeded) { + $failureMessage = "Expected ${ActualValue} to not contain the entries ${ExpectedValue -join ""} only $(if($Because) { "because $Because"})." + } + } + + $ObjProperties = @{ + Succeeded = $succeeded + FailureMessage = $failureMessage + } + return New-Object PSObject -Property $ObjProperties +} + +Add-ShouldOperator -Name BeZipArchiveOnlyContaining -InternalName 'Should-BeZipArchiveOnlyContaining' -Test ${function:Should-BeZipArchiveOnlyContaining} \ No newline at end of file diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index 01f1524..049e6f1 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -1,96 +1,17 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +BeforeDiscovery { + # Loads and registers custom assertion. Ignores usage of unapproved verb with -DisableNameChecking + Import-Module "$PSScriptRoot/Assertions/Should-BeZipArchiveOnlyContaining.psm1" -DisableNameChecking +} + Describe("Microsoft.PowerShell.Archive tests") { BeforeAll { $originalProgressPref = $ProgressPreference $ProgressPreference = "SilentlyContinue" $originalPSModulePath = $env:PSModulePath - - # Add compression assemblies - function Add-CompressionAssemblies { - Add-Type -AssemblyName System.IO.Compression - if ($psedition -eq "Core") - { - Add-Type -AssemblyName System.IO.Compression.ZipFile - } - else - { - Add-Type -AssemblyName System.IO.Compression.FileSystem - } - } - - Add-CompressionAssemblies - - # Used for validating an archive's contents - function Test-ZipArchive { - param - ( - [string] $archivePath, - [string[]] $expectedEntries, - [switch] $Literal - ) - - try - { - if ($Literal) { - $archivePath = Convert-Path -LiteralPath $archivePath - } else { - $archivePath = Convert-Path -Path $archivePath - } - - - $archiveFileStreamArgs = @($archivePath, [System.IO.FileMode]::Open) - $archiveFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList $archiveFileStreamArgs - - $zipArchiveArgs = @($archiveFileStream, [System.IO.Compression.ZipArchiveMode]::Read, $false) - $zipArchive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $zipArchiveArgs - - $actualEntryCount = $zipArchive.Entries.Count - $actualEntryCount | Should -Be $expectedEntries.Length - - # Get a list of entry names in the zip archive - $archiveEntries = @() - ForEach ($archiveEntry in $zipArchive.Entries) { - $archiveEntries += $archiveEntry.FullName - } - - # Ensure each entry in the archive is in the list of expected entries - ForEach ($expectedEntry in $expectedEntries) { - $expectedEntry | Should -BeIn $archiveEntries - } - - } - finally - { - if ($null -ne $zipArchive) { $zipArchive.Dispose()} - if ($null -ne $archiveFileStream) { $archiveFileStream.Dispose() } - } - } - - # This function gets a list of a directories descendants formatted as archive entries - function Get-Descendants { - param ( - [string] $Path - ) - - - # Get the folder name - $folderName = Split-Path -Path $Path -Leaf - - # Get descendents - $descendants = Get-ChildItem -Path $Path -Recurse -Name - - $output = @() - - # Prefix each descendant name with folder name - foreach ($name in $descendants) { - $output += ($folderName + '/' + $name).Replace([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar) - } - - return $output - } } AfterAll { @@ -325,12 +246,11 @@ } } - It "-WriteMode Create works" -Tag this2 { + It "-WriteMode Create works" -Tag td1 { $sourcePath = "TestDrive:/SourceDir" $destinationPath = "TestDrive:/archive1.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - Test-Path $destinationPath - Test-ZipArchive $destinationPath @('SourceDir/', 'SourceDir/Sample-1.txt') + $destinationPath | Should -BeZipArchiveOnlyContaining @('SourceDir/', 'SourceDir/Sample-1.txt') } } @@ -354,26 +274,21 @@ $sourcePath = "TestDrive:/SourceDir/ChildDir-1/Sample-2.txt" $destinationPath = "TestDrive:/archive1.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - $destinationPath | Should -Exist - Test-ZipArchive $destinationPath @('Sample-2.txt') + $destinationPath | Should -BeZipArchiveOnlyContaining @('Sample-2.txt') } It "Validate that an empty folder can be compressed" { $sourcePath = "TestDrive:/EmptyDir" $destinationPath = "TestDrive:/archive2.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - $destinationPath | Should -Exist - Test-ZipArchive $destinationPath @('EmptyDir/') + $destinationPath | Should -BeZipArchiveOnlyContaining @('EmptyDir/') } It "Validate a folder containing files, non-empty folders, and empty folders can be compressed" { $sourcePath = "TestDrive:/SourceDir" $destinationPath = "TestDrive:/archive3.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - $destinationPath | Should -Exist - $contents = Get-Descendants -Path $sourcePath - $contents += "SourceDir/" - Test-ZipArchive $destinationPath $contents + $destinationPath | Should -BeZipArchiveOnlyContaining @('SourceDir/', 'SourceDir/ChildDir-1/', 'SourceDir/ChildDir-2/', 'SourceDir/ChildEmptyDir/', 'SourceDir/Sample-1.txt', 'SourceDir/ChildDir-1/Sample-2.txt', 'SourceDir/ChildDir-2/Sample-3.txt') } } @@ -515,27 +430,27 @@ $sourcePath = "TestDrive:/SourceDir" $destinationPath = "TestDrive:/EmptyDirectory" - (Get-Item $destinationPath) -is [System.IO.DirectoryInfo] | Should -Be $true + # Ensure $destinationPath is a directory + Test-Path $destinationPath -PathType Container | Should -Be $true + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite - # Ensure $destiationPath is now a file - $destinationPathInfo = Get-Item $destinationPath - $destinationPathInfo -is [System.IO.DirectoryInfo] | Should -Be $false - $destinationPathInfo -is [System.IO.FileInfo] | Should -Be $true + # Ensure $destinationPath is now a file + Test-Path $destinationPath -PathType Leaf | Should -Be $true } It "Overwrites an archive that already exists" { $destinationPath = "TestDrive:/archive.zip" - # Get the entries of the original zip archive - Test-ZipArchive $destinationPath @("Sample-1.txt") + # Ensure the original archive contains Sample-1.txt + $destinationPath | Should -BeZipArchiveOnlyContaining @("Sample-1.txt") # Overwrite the archive $sourcePath = "TestDrive:/Sample-2.txt" Compress-Archive -Path $sourcePath -DestinationPath "TestDrive:/archive.zip" -WriteMode Overwrite # Ensure the original entries and different than the new entries - Test-ZipArchive $destinationPath @("Sample-2.txt") + $destinationPath | Should -BeZipArchiveOnlyContaining @("Sample-2.txt") } } @@ -620,9 +535,20 @@ $destinationPath = "TestDrive:/archive[2.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - Test-Path -LiteralPath $destinationPath | Should -Be $true - Test-ZipArchive $destinationPath @("SourceDir/", "SourceDir/Sample-1.txt") -Literal + $destinationPath | Should -BeZipArchiveOnlyContaining @("SourceDir/", "SourceDir/Sample-1.txt") -LiteralPath Remove-Item -LiteralPath $destinationPath } } + + Context "test" -Tag lol { + BeforeAll { + $content = "Some Data" + $content | Out-File -FilePath TestDrive:/Sample-1.txt + Compress-Archive -Path TestDrive:/Sample-1.txt -DestinationPath TestDrive:/archive1.zip + } + + It "test custom assetion" { + "${TestDrive}/archive1.zip" | Should -BeZipArchiveOnlyContaining @("Sample-1.txt") + } + } } diff --git a/Tests/Install-7Zip.ps1 b/Tests/Install-7Zip.ps1 new file mode 100644 index 0000000..4a5edc7 --- /dev/null +++ b/Tests/Install-7Zip.ps1 @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# This script will download and install 7-zip + +function Install-7zip { + + Param ( + [string] OS + ) + + +} \ No newline at end of file diff --git a/src/ZipArchive.cs b/src/ZipArchive.cs index 1cdadad..4c74147 100644 --- a/src/ZipArchive.cs +++ b/src/ZipArchive.cs @@ -63,9 +63,9 @@ void IArchive.AddFileSystemEntry(ArchiveAddition addition) if (entryInArchive == null) { // Ensure addition.entryName has '/' at the end - if (!addition.EntryName.EndsWith(ZipArchiveDirectoryPathTerminator)) + if (!entryName.EndsWith(ZipArchiveDirectoryPathTerminator)) { - addition.EntryName += ZipArchiveDirectoryPathTerminator; + entryName += ZipArchiveDirectoryPathTerminator; } _zipArchive.CreateEntry(entryName); From dc557a5f0f6f62b8ebd95e13a2e0c78e58d53f38 Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Mon, 8 Aug 2022 13:38:43 -0700 Subject: [PATCH 42/42] updated CI to run tests using new assertion, updated README with Azure CI status, fixed a bug where a path is determined to be relative to the working directory if the working directory is on a different drive than the path --- .azdevops/RunTests.ps1 | 30 +++----- .azdevops/TestsTemplate.yml | 69 +++++++++++-------- README.md | 7 +- .../Should-BeZipArchiveOnlyContaining.psm1 | 6 +- Tests/Compress-Archive.Tests.ps1 | 14 ++-- Tests/Install-7Zip.ps1 | 13 ---- src/PathHelper.cs | 11 ++- 7 files changed, 78 insertions(+), 72 deletions(-) delete mode 100644 Tests/Install-7Zip.ps1 diff --git a/.azdevops/RunTests.ps1 b/.azdevops/RunTests.ps1 index 44198cf..0361d4f 100644 --- a/.azdevops/RunTests.ps1 +++ b/.azdevops/RunTests.ps1 @@ -1,36 +1,22 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Load the module -$module = Get-Module -Name "Microsoft.PowerShell.Archive" -if ($null -ne $module) -{ - Remove-Module $module -} - # Import the built module Import-Module "$env:PIPELINE_WORKSPACE/ModuleBuild/Microsoft.PowerShell.Archive.psd1" -$pesterRequiredVersion = "5.3" +Get-ChildItem "$env:PIPELINE_WORKSPACE/ModuleBuild" | Write-Verbose -Verbose -# If Pester 5.3.3 is not installed, install it -$shouldInstallPester = $true - -if ($pesterModules = Get-Module -Name "Pester" -ListAvailable) { - foreach ($module in $pesterModules) { - if ($module.Version.ToString() -eq $pesterRequiredVersion) { - $shouldInstallPester = $false - break - } - } -} +$pesterMinVersion = "5.3.0" +$pesterMaxVersion = "5.3.9" -if ($shouldInstallPester) { - Install-Module -Name "Pester" -RequiredVersion $pesterRequiredVersion -Force +# If Pester 5.3.x is not installed, install it +$pesterModule = Get-InstalledModule -Name "Pester" -MinimumVersion $pesterMinVersion -MaximumVersion $pesterMaxVersion +if ($null -eq $pesterModule) { + Install-Module -Name "Pester" -MinimumVersion $pesterMinVersion -MaximumVersion $pesterMaxVersion -Force } # Load Pester -Import-Module -Name "Pester" -RequiredVersion $pesterRequiredVersion +Import-Module -Name "Pester" -MinimumVersion $pesterMinVersion -MaximumVersion $pesterMaxVersion # Run tests $OutputFile = "$PWD/build-unit-tests.xml" diff --git a/.azdevops/TestsTemplate.yml b/.azdevops/TestsTemplate.yml index 604ba77..0876709 100644 --- a/.azdevops/TestsTemplate.yml +++ b/.azdevops/TestsTemplate.yml @@ -22,50 +22,65 @@ jobs: - pwsh: | Write-Output ${{ parameters.vmImageName }} - $url = "https://github.com/PowerShell/PowerShell/releases/download/v7.3.0-preview.6/PowerShell-7.3.0-preview.6-win-x64.zip" - $isTar = $false + + if ("${{ parameters.vmImageName }}" -like 'windows-*') + { + $url = "https://github.com/PowerShell/PowerShell/releases/download/v7.3.0-preview.6/PowerShell-7.3.0-preview.6-win-x64.zip" + $downloadFilename = "pwsh_download.msi" + } + if ("${{ parameters.vmImageName }}" -like 'macos-*') { - $url = "https://github.com/PowerShell/PowerShell/releases/download/v7.3.0-preview.6/powershell-7.3.0-preview.6-osx-x64.tar.gz" - $isTar = $true - Write-Output "Choose macOS" + $url = "https://github.com/PowerShell/PowerShell/releases/download/v7.3.0-preview.6/powershell-7.3.0-preview.6-osx-x64.pkg" + $downloadFilename = "pwsh_download.pkg" } if ("${{ parameters.vmImageName }}" -like 'ubuntu-*') { $url = "https://github.com/PowerShell/PowerShell/releases/download/v7.3.0-preview.6/powershell-7.3.0-preview.6-linux-x64.tar.gz" - $isTar = $true - Write-Output "Choose Ubuntu" + $downloadFilename = "pwsh_download.tar.gz" } - $destination = "powershell-preview-archive" - if ($isTar) { - $destination += ".tar.gz" - } else { - $destination += ".zip" + + $downloadDestination = Join-Path $pwd $downloadFilename + Invoke-WebRequest -Uri $url -OutFile $downloadDestination + + # Installation steps for windows + if ("${{ parameters.vmImageName }}" -like 'windows-*') { + Expand-Archive -Path $downloadDestination -DestinationPath "pwsh-preview" + $powerShellPreview = Join-Path $pwd "pwsh-preview" "pwsh.exe" } - Invoke-WebRequest -Uri $url -OutFile $destination - ## unpack the downloaded file - $powershellPreview = Join-Path $pwd "powershell-preview" - mkdir $powershellPreview - if ($isTar) + if ("${{ parameters.vmImageName }}" -like 'ubuntu-*') { - gunzip -d $destination - $destination = $destination.Replace(".gz", "") - tar -x -f $destination -C $powershellPreview - } else { - Expand-Archive -Path $destination -DestinationPath $powershellPreview + gunzip -d $downloadDestination + $downloadDestination = $downloadDestination.Replace(".gz", "") + mkdir "pwsh-preview" + tar -x -f $downloadDestination -C "pwsh-preview" + $powerShellPreview = Join-Path $pwd "pwsh-preview" "pwsh" } - # Print contents of $powershellPreview - #Get-ChildItem $powershellPreview | Format-Table "Name" - $powershellPreview = Join-Path $powershellPreview "pwsh" - if ("${{ parameters.vmImageName }}" -like 'windows-*') + if ("${{ parameters.vmImageName }}" -like 'macos-*') { - $powerShellPreview += ".exe" + sudo xattr -rd com.apple.quarantine "${downloadDestination}" + sudo installer -pkg "${downloadDestination}" -target / + $powerShellPreview = "pwsh-preview" } # Write the location of PowerShell Preview Write-Host "##vso[task.setvariable variable=PowerShellPreviewExecutablePath;]$powershellPreview" displayName: Download and Install PowerShell Preview - pwsh: | + $destination = Join-Path $pwd "7z.exe" + $installUrl = "https://www.7-zip.org/a/7z2201-x64.exe" + Invoke-WebRequest -Uri $installUrl -OutFile $destination + # Run the installer in silent mode + .$destination /S /D="C:\Program Files\7-Zip" + displayName: Install 7-zip + condition: and(succeeded(), startswith('${{ parameters.vmImageName }}', 'windows')) + + - pwsh: | + if ("${{ parameters.vmImageName }}" -like 'windows-*') + { + # Add 7-zip to PATH on Windows + [System.Environment]::SetEnvironmentVariable('PATH',$Env:PATH+';C:\Program Files\7-zip') + } "$(PowerShellPreviewExecutablePath) .azdevops/RunTests.ps1" | Invoke-Expression displayName: Run Tests diff --git a/README.md b/README.md index e3c7b8e..53a6e45 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,9 @@ # Microsoft.PowerShell.Archive Module [Microsoft.PowerShell.Archive module](https://technet.microsoft.com/en-us/library/dn818910.aspx) contains cmdlets that let you create and extract ZIP archives. -|AppVeyor (Windows) | Travis CI (Linux) | -|:-------------------:|:------------------:| -|[![Build status](https://ci.appveyor.com/api/projects/status/npvhboe2nbdbtteg/branch/master?svg=true)](https://ci.appveyor.com/project/PowerShell/microsoft-powershell-archive/branch/master)|[![Build Status](https://travis-ci.org/PowerShell/Microsoft.PowerShell.Archive.svg?branch=master)](https://travis-ci.org/PowerShell/Microsoft.PowerShell.Archive)| - +| Azure CI | +|:-------------------:| +|[![Build Status](https://dev.azure.com/powershell/Archive/_apis/build/status/PowerShell.Microsoft.PowerShell.Archive?repoName=PowerShell%2FMicrosoft.PowerShell.Archive&branchName=refs%2Fpull%2F131%2Fmerge)](https://dev.azure.com/powershell/Archive/_build/latest?definitionId=130&repoName=PowerShell%2FMicrosoft.PowerShell.Archive&branchName=refs%2Fpull%2F131%2Fmerge)| ## [Compress-Archive](https://technet.microsoft.com/library/dn841358.aspx) examples 1. Create an archive from an entire folder including subdirectories: `Compress-Archive -Path C:\Reference -DestinationPath C:\Archives\Draft.zip` 2. Update an existing archive file: `Compress-Archive -Path C:\Reference\* -DestinationPath C:\Archives\Draft.zip -Update` diff --git a/Tests/Assertions/Should-BeZipArchiveOnlyContaining.psm1 b/Tests/Assertions/Should-BeZipArchiveOnlyContaining.psm1 index 2f3ad8e..833fc6c 100644 --- a/Tests/Assertions/Should-BeZipArchiveOnlyContaining.psm1 +++ b/Tests/Assertions/Should-BeZipArchiveOnlyContaining.psm1 @@ -48,7 +48,11 @@ function Should-BeZipArchiveOnlyContaining { } # Get 7-zip to list the contents of the archive - $output = 7z.exe l $ActualValue -ba + if ($IsWindows) { + $output = 7z.exe l $ActualValue -ba + } else { + $output = 7z l $ActualValue -ba + } # Check if the output is null if ($null -eq $output) { diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index 049e6f1..cfa3ad9 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -58,9 +58,9 @@ BeforeDiscovery { } It "Validate errors from Compress-Archive when invalid path is supplied for Path or LiteralPath parameters" -ForEach @( - @{ Path = "Env:/Path" } - @{ Path = @("TestDrive:/", "Env:/Path") } - ) -Tag this1 { + @{ Path = "Variable:/PWD" } + @{ Path = @("TestDrive:/", "Variable:/PWD") } + ) { $DestinationPath = "TestDrive:/archive2.zip" Compress-Archive -Path $Path -DestinationPath $DestinationPath -ErrorAction SilentlyContinue -ErrorVariable error @@ -249,8 +249,14 @@ BeforeDiscovery { It "-WriteMode Create works" -Tag td1 { $sourcePath = "TestDrive:/SourceDir" $destinationPath = "TestDrive:/archive1.zip" - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Verbose + if ($IsWindows) { + $t = Convert-Path $destinationPath + 7z l "${t}" | Write-Verbose -Verbose + } $destinationPath | Should -BeZipArchiveOnlyContaining @('SourceDir/', 'SourceDir/Sample-1.txt') + + } } diff --git a/Tests/Install-7Zip.ps1 b/Tests/Install-7Zip.ps1 deleted file mode 100644 index 4a5edc7..0000000 --- a/Tests/Install-7Zip.ps1 +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -# This script will download and install 7-zip - -function Install-7zip { - - Param ( - [string] OS - ) - - -} \ No newline at end of file diff --git a/src/PathHelper.cs b/src/PathHelper.cs index ce58bc4..b5262ed 100644 --- a/src/PathHelper.cs +++ b/src/PathHelper.cs @@ -137,6 +137,8 @@ private string GetEntryName(FileSystemInfo fileSystemInfo, out bool doesPreserve { entryName += System.IO.Path.DirectorySeparatorChar; } + + return entryName; } @@ -198,7 +200,14 @@ private static string GetPrefixForPath(System.IO.DirectoryInfo directoryInfo) /// private bool TryGetPathRelativeToCurrentWorkingDirectory(string path, out string? relativePathToWorkingDirectory) { - string relativePath = System.IO.Path.GetRelativePath(_cmdlet.SessionState.Path.CurrentFileSystemLocation.Path, path); + Debug.Assert(!string.IsNullOrEmpty(path)); + string? workingDirectoryRoot = Path.GetPathRoot(_cmdlet.SessionState.Path.CurrentFileSystemLocation.Path); + string? pathRoot = Path.GetPathRoot(path); + if (workingDirectoryRoot != pathRoot) { + relativePathToWorkingDirectory = null; + return false; + } + string relativePath = Path.GetRelativePath(_cmdlet.SessionState.Path.CurrentFileSystemLocation.Path, path); relativePathToWorkingDirectory = relativePath.Contains("..") ? null : relativePath; return relativePathToWorkingDirectory is not null; }