From ca2d328da7dd34cca7ce083bf656344fa72d2929 Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Tue, 26 Jul 2022 17:44:56 -0700 Subject: [PATCH 01/34] added and updated tests for basic functionality --- Tests/Compress-Archive.Tests.ps1 | 112 +++++++++++++++++++++++++++++-- 1 file changed, 105 insertions(+), 7 deletions(-) diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index cfa3ad9..c0c2cc5 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -276,26 +276,94 @@ BeforeDiscovery { $content | Out-File -FilePath TestDrive:/SourceDir/ChildDir-2/Sample-3.txt } - It "Validate that a single file can be compressed" { - $sourcePath = "TestDrive:/SourceDir/ChildDir-1/Sample-2.txt" - $destinationPath = "TestDrive:/archive1.zip" + It "Compresses a single file" { + $sourcePath = "$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-2.txt" + $destinationPath = "$TestDrive$($DS)archive1.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath $destinationPath | Should -BeZipArchiveOnlyContaining @('Sample-2.txt') } - It "Validate that an empty folder can be compressed" { - $sourcePath = "TestDrive:/EmptyDir" - $destinationPath = "TestDrive:/archive2.zip" + It "Compresses a non-empty directory" { + $sourcePath = "$TestDrive$($DS)SourceDir$($DS)ChildDir-1" + $destinationPath = "$TestDrive$($DS)archive4.zip" + + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + $destinationPath | Should -Exist + Test-ZipArchive $destinationPath @('ChildDir-1/', 'ChildDir-1/Sample-2.txt') + } + + It "Compresses an empty directory" { + $sourcePath = "$TestDrive$($DS)EmptyDir" + $destinationPath = "$TestDrive$($DS)archive2.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath $destinationPath | Should -BeZipArchiveOnlyContaining @('EmptyDir/') } + It "Compresses multiple files" { + $sourcePath = @("$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-2.txt", "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt") + $destinationPath = "$TestDrive$($DS)archive2.zip" + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + $destinationPath | Should -Exist + Test-ZipArchive $destinationPath @('Sample-1.txt', 'Sample-2.txt') + } + + It "Compress multiple files and a single empty-directory" { + $sourcePath = @("$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-2.txt", "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt", + "$TestDrive$($DS)SourceDir$($DS)ChildEmptyDir") + + $destinationPath = "$TestDrive$($DS)archive3.zip" + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + $destinationPath | Should -Exist + Test-ZipArchive $destinationPath @('Sample-1.txt', 'Sample-2.txt', 'EmptyDir/') + } + + It "Compresses multiple files and a single non-empty directory" { + $sourcePath = @("$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-2.txt", "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt", + "$TestDrive$($DS)SourceDir$($DS)ChildDir-1") + + $destinationPath = "$TestDrive$($DS)archive3.zip" + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + $destinationPath | Should -Exist + Test-ZipArchive $destinationPath @('Sample-1.txt', 'Sample-2.txt', 'ChildDir-1/', 'ChildDir-1/Sample-2.txt') + } + + It "Compresses multiple files and non-empty directories" { + $sourcePath = @("$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-2.txt", "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt", + "$TestDrive$($DS)SourceDir$($DS)ChildDir-1", "$TestDrive$($DS)SourceDir$($DS)ChildDir-2") + + $destinationPath = "$TestDrive$($DS)archive3.zip" + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + $destinationPath | Should -Exist + Test-ZipArchive $destinationPath @('Sample-1.txt', 'Sample-2.txt', 'ChildDir-1/', 'ChildDir-2/', + 'ChildDir-1/Sample-2.txt', 'ChildDir-2/Sample-3.txt') + } + + It "Compresses multiple files, non-empty directories, and an empty directory" { + $sourcePath = @("$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-2.txt", "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt", + "$TestDrive$($DS)SourceDir$($DS)ChildDir-1", "$TestDrive$($DS)SourceDir$($DS)ChildDir-2", "$TestDrive$($DS)SourceDir$($DS)ChildEmptyDir") + + $destinationPath = "$TestDrive$($DS)archive3.zip" + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + $destinationPath | Should -Exist + Test-ZipArchive $destinationPath @('Sample-1.txt', 'Sample-2.txt', 'ChildDir-1/', 'ChildDir-2/', + 'ChildDir-1/Sample-2.txt', 'ChildDir-2/Sample-3.txt', "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 -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') } + + It "Compresses a zero-byte file" { + $sourcePath = "$TestDrive$($DS)EmptyFile" + $destinationPath = "$TestDrive$($DS)archive3.zip" + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + $destinationPath | Should -Exist + $contents = @('EmptyFile') + Test-ZipArchive $destinationPath $contents + } } Context "Update tests" -Skip { @@ -525,9 +593,10 @@ BeforeDiscovery { $content = "Some Data" $content | Out-File -FilePath TestDrive:/SourceDir/Sample-1.txt + New-Item -LiteralPath "$TestDrive$($DS)Source[]Dir" -Type Directory | Out-Null + $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)file1[].txt } - It "Accepts DestinationPath parameter with wildcard characters that resolves to one path" { $sourcePath = "TestDrive:/SourceDir/Sample-1.txt" $destinationPath = "TestDrive:/Sample[]SingleFile.zip" @@ -544,6 +613,35 @@ BeforeDiscovery { $destinationPath | Should -BeZipArchiveOnlyContaining @("SourceDir/", "SourceDir/Sample-1.txt") -LiteralPath Remove-Item -LiteralPath $destinationPath } + + It "Accepts 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" + "Some Random Content" | Out-File -LiteralPath "$sourcePath$($DS)Sample[]File.txt" + $destinationPath = "$TestDrive$($DS)archive1.zip" + try + { + Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath + $destinationPath | Should -Exist + } + finally + { + Remove-Item -LiteralPath $sourcePath -Force -Recurse + } + } + + It "Accepts LiteralPath parameter for a file with wildcards in the filename" { + $sourcePath = "$TestDrive$($DS)SourceDir($DS)file1[].txt" + $destinationPath = "$TestDrive$($DS)archive1.zip" + try + { + Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath + $destinationPath | Should -Exist + } + finally + { + Remove-Item -LiteralPath $sourcePath -Force -Recurse + } + } } Context "test" -Tag lol { From 39158b59781ce21b28a6564728b1b1d609649026 Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Wed, 27 Jul 2022 13:02:44 -0700 Subject: [PATCH 02/34] added and worked on Expand-Archive cmdlet but it is incomplete --- Tests/Compress-Archive.Tests.ps1 | 6 +++--- src/CompressArchiveCommand.cs | 7 +------ src/ErrorMessages.cs | 15 +++++++++------ src/Localized/Messages.resx | 14 ++++++++------ src/WriteMode.cs | 6 ++++++ 5 files changed, 27 insertions(+), 21 deletions(-) diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index c0c2cc5..4942ca6 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -405,7 +405,7 @@ BeforeDiscovery { } catch { - $_.FullyQualifiedErrorId | Should -Be "ArchiveExists,Microsoft.PowerShell.Archive.CompressArchiveCommand" + $_.FullyQualifiedErrorId | Should -Be "DestinationExists,Microsoft.PowerShell.Archive.CompressArchiveCommand" } } @@ -435,7 +435,7 @@ BeforeDiscovery { } catch { - $_.FullyQualifiedErrorId | Should -Be "ArchiveExistsAsDirectory,Microsoft.PowerShell.Archive.CompressArchiveCommand" + $_.FullyQualifiedErrorId | Should -Be "DestinationExistsAsDirectory,Microsoft.PowerShell.Archive.CompressArchiveCommand" } } @@ -450,7 +450,7 @@ BeforeDiscovery { } catch { - $_.FullyQualifiedErrorId | Should -Be "ArchiveExistsAsDirectory,Microsoft.PowerShell.Archive.CompressArchiveCommand" + $_.FullyQualifiedErrorId | Should -Be "DestinationExistsAsDirectory,Microsoft.PowerShell.Archive.CompressArchiveCommand" } } diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index de521d9..cadecd0 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -16,12 +16,7 @@ namespace Microsoft.PowerShell.Archive { [Cmdlet("Compress", "Archive", SupportsShouldProcess = true)] [OutputType(typeof(FileInfo))] - public sealed class CompressArchiveCommand : PSCmdlet - { - - // TODO: Add filter parameter - // TODO: Add flatten parameter - // TODO: Add comments to methods + public sealed class CompressArchiveCommand : PSCmdlet { // TODO: Add tar support private enum ParameterSet diff --git a/src/ErrorMessages.cs b/src/ErrorMessages.cs index 80ec0c5..0b68a9b 100644 --- a/src/ErrorMessages.cs +++ b/src/ErrorMessages.cs @@ -30,8 +30,8 @@ internal static string GetErrorMessage(ErrorCode errorCode) ErrorCode.PathNotFound => Messages.PathNotFoundMessage, ErrorCode.InvalidPath => Messages.InvalidPathMessage, ErrorCode.DuplicatePaths => Messages.DuplicatePathsMessage, - ErrorCode.ArchiveExists => Messages.ArchiveExistsMessage, - ErrorCode.ArchiveExistsAsDirectory => Messages.ArchiveExistsAsDirectoryMessage, + ErrorCode.DestinationExists => Messages.DestinationExistsMessage, + ErrorCode.DestinationExistsAsDirectory => Messages.DestinationExistsAsDirectoryMessage, ErrorCode.ArchiveReadOnly => Messages.ArchiveIsReadOnlyMessage, ErrorCode.ArchiveDoesNotExist => Messages.ArchiveDoesNotExistMessage, ErrorCode.ArchiveIsNonEmptyDirectory => Messages.ArchiveIsNonEmptyDirectory, @@ -53,10 +53,10 @@ internal enum ErrorCode 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 file (used in Compress-Archive & Expand-Archive) + DestinationExists, // Used when DestinationPath is an existing directory - ArchiveExistsAsDirectory, + DestinationExistsAsDirectory, // 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 @@ -72,6 +72,9 @@ internal enum ErrorCode // Used when the cmdlet could not overwrite DestinationPath OverwriteDestinationPathFailed, // Used when the user enters the working directory as DestinationPath and it is an existing folder and -WriteMode Overwrite is specified - CannotOverwriteWorkingDirectory + // Used in Compress-Archive, Expand-Archive + CannotOverwriteWorkingDirectory, + // Expand-Archive: used when a path resolved to multiple paths when only one was needed + PathResolvedToMultiplePaths, } } diff --git a/src/Localized/Messages.resx b/src/Localized/Messages.resx index e230be9..7f9ebfc 100644 --- a/src/Localized/Messages.resx +++ b/src/Localized/Messages.resx @@ -126,12 +126,6 @@ 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. @@ -152,6 +146,11 @@ Create + + The destination path {0} is a directory. + + + The destination path {0} already exists. The path(s) {0} have been specified more than once. @@ -174,6 +173,9 @@ {0}% complete + + The path {0} refers to multiple paths, but only one was expected. Perhaps this occured because multiple paths matched the wildcard (if applicable). + A path supplied to -LiteralPath is the same as the path supplied to -DestinationPath. diff --git a/src/WriteMode.cs b/src/WriteMode.cs index 00efad3..5a1ae75 100644 --- a/src/WriteMode.cs +++ b/src/WriteMode.cs @@ -13,4 +13,10 @@ public enum WriteMode Update, Overwrite } + + public enum ExpandArchiveWriteMode + { + Expand, + Overwrite + } } From 2f3da2dabf574fbbc5b4f8fc552c09d1dbe604aa Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Wed, 27 Jul 2022 17:31:32 -0700 Subject: [PATCH 03/34] worked on Expand-Archive, added IEntry class, added support for ShouldProcess in Expand-Archive --- Tests/Compress-Archive.Tests.ps1 | 104 ++++++++++--- src/ArchiveCommandBase.cs | 52 +++++++ src/ErrorMessages.cs | 4 +- src/ExpandArchiveCommand.cs | 251 +++++++++++++++++++++++++++++++ src/IArchive.cs | 2 + src/IEntry.cs | 15 ++ src/Localized/Messages.resx | 6 +- src/TarArchive.cs | 5 + src/ZipArchive.cs | 42 ++++++ 9 files changed, 453 insertions(+), 28 deletions(-) create mode 100644 src/ArchiveCommandBase.cs create mode 100644 src/ExpandArchiveCommand.cs create mode 100644 src/IEntry.cs diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index 4942ca6..4acaf64 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -274,6 +274,15 @@ BeforeDiscovery { $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 + + "Hello, World!" | Out-File -FilePath $TestDrive$($DS)HelloWorld.txt + + # Create a zero-byte file + New-Item $TestDrive$($DS)EmptyFile -Type File | Out-Null + + # Create a file whose last write time is before 1980 + $content | Out-File -FilePath $TestDrive$($DS)OldFile.txt + Set-ItemProperty -Path $TestDrive$($DS)OldFile.txt -Name LastWriteTime -Value '1974-01-16 14:44' } It "Compresses a single file" { @@ -285,7 +294,7 @@ BeforeDiscovery { It "Compresses a non-empty directory" { $sourcePath = "$TestDrive$($DS)SourceDir$($DS)ChildDir-1" - $destinationPath = "$TestDrive$($DS)archive4.zip" + $destinationPath = "$TestDrive$($DS)archive2.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath $destinationPath | Should -Exist @@ -294,76 +303,109 @@ BeforeDiscovery { It "Compresses an empty directory" { $sourcePath = "$TestDrive$($DS)EmptyDir" - $destinationPath = "$TestDrive$($DS)archive2.zip" + $destinationPath = "$TestDrive$($DS)archive3.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath $destinationPath | Should -BeZipArchiveOnlyContaining @('EmptyDir/') } It "Compresses multiple files" { $sourcePath = @("$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-2.txt", "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt") - $destinationPath = "$TestDrive$($DS)archive2.zip" + $destinationPath = "$TestDrive$($DS)archive4.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath $destinationPath | Should -Exist Test-ZipArchive $destinationPath @('Sample-1.txt', 'Sample-2.txt') } - It "Compress multiple files and a single empty-directory" { + It "Compresses multiple files and a single empty directory" { $sourcePath = @("$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-2.txt", "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt", "$TestDrive$($DS)SourceDir$($DS)ChildEmptyDir") - $destinationPath = "$TestDrive$($DS)archive3.zip" + $destinationPath = "$TestDrive$($DS)archive5.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath $destinationPath | Should -Exist - Test-ZipArchive $destinationPath @('Sample-1.txt', 'Sample-2.txt', 'EmptyDir/') + Test-ZipArchive $destinationPath @('Sample-1.txt', 'Sample-2.txt', 'ChildEmptyDir/') } It "Compresses multiple files and a single non-empty directory" { $sourcePath = @("$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-2.txt", "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt", - "$TestDrive$($DS)SourceDir$($DS)ChildDir-1") + "$TestDrive$($DS)SourceDir$($DS)ChildDir-2") - $destinationPath = "$TestDrive$($DS)archive3.zip" + $destinationPath = "$TestDrive$($DS)archive6.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath $destinationPath | Should -Exist - Test-ZipArchive $destinationPath @('Sample-1.txt', 'Sample-2.txt', 'ChildDir-1/', 'ChildDir-1/Sample-2.txt') + Test-ZipArchive $destinationPath @('Sample-1.txt', 'Sample-2.txt', 'ChildDir-2/', 'ChildDir-2/Sample-3.txt') } It "Compresses multiple files and non-empty directories" { - $sourcePath = @("$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-2.txt", "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt", + $sourcePath = @("$TestDrive$($DS)HelloWorld.txt", "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt", "$TestDrive$($DS)SourceDir$($DS)ChildDir-1", "$TestDrive$($DS)SourceDir$($DS)ChildDir-2") - $destinationPath = "$TestDrive$($DS)archive3.zip" + $destinationPath = "$TestDrive$($DS)archive7.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath $destinationPath | Should -Exist - Test-ZipArchive $destinationPath @('Sample-1.txt', 'Sample-2.txt', 'ChildDir-1/', 'ChildDir-2/', + Test-ZipArchive $destinationPath @('Sample-1.txt', 'HelloWorld.txt', 'ChildDir-1/', 'ChildDir-2/', 'ChildDir-1/Sample-2.txt', 'ChildDir-2/Sample-3.txt') } It "Compresses multiple files, non-empty directories, and an empty directory" { - $sourcePath = @("$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-2.txt", "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt", + $sourcePath = @("$TestDrive$($DS)HelloWorld.txt", "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt", "$TestDrive$($DS)SourceDir$($DS)ChildDir-1", "$TestDrive$($DS)SourceDir$($DS)ChildDir-2", "$TestDrive$($DS)SourceDir$($DS)ChildEmptyDir") - $destinationPath = "$TestDrive$($DS)archive3.zip" + $destinationPath = "$TestDrive$($DS)archive8.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath $destinationPath | Should -Exist - Test-ZipArchive $destinationPath @('Sample-1.txt', 'Sample-2.txt', 'ChildDir-1/', 'ChildDir-2/', - 'ChildDir-1/Sample-2.txt', 'ChildDir-2/Sample-3.txt', "EmptyDir/") + Test-ZipArchive $destinationPath @('Sample-1.txt', 'HelloWorld.txt', 'ChildDir-1/', 'ChildDir-2/', + 'ChildDir-1/Sample-2.txt', 'ChildDir-2/Sample-3.txt', "ChildEmptyDir/") } - It "Validate a folder containing files, non-empty folders, and empty folders can be compressed" { - $sourcePath = "TestDrive:/SourceDir" - $destinationPath = "TestDrive:/archive3.zip" + It "Compresses a directory containing files, non-empty directories, and an empty directory can be compressed" -Tag td4 { + $sourcePath = "$TestDrive$($DS)SourceDir" + $destinationPath = "$TestDrive$($DS)archive9.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath $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') } It "Compresses a zero-byte file" { $sourcePath = "$TestDrive$($DS)EmptyFile" - $destinationPath = "$TestDrive$($DS)archive3.zip" + $destinationPath = "$TestDrive$($DS)archive10.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath $destinationPath | Should -Exist $contents = @('EmptyFile') Test-ZipArchive $destinationPath $contents } + + It "Compresses a file whose last write time is before 1980" { + $sourcePath = "$TestDrive$($DS)OldFile.txt" + $destinationPath = "$TestDrive$($DS)archive11.zip" + + # Assert the last write time of the file is before 1980 + $dateProperty = Get-ItemProperty -Path $sourcePath -Name "LastWriteTime" + $dateProperty.Year | Should -BeLessThan 1980 + + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + $destinationPath | Should -Exist + Test-ZipArchive $destinationPath @('OldFile.txt') + + # Get the archive + $fileMode = [System.IO.FileMode]::Open + $archiveStream = New-Object -TypeName System.IO.FileStream -ArgumentList $destinationPath,$fileMode + $zipArchiveMode = [System.IO.Compression.ZipArchiveMode]::Read + $archive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $archiveStream,$zipArchiveMode + $entry = $archive.GetEntry("OldFile.txt") + $entry | Should -Not -BeNullOrEmpty + + $entry.LastWriteTime.Year | Should -BeExactly 1980 + $entry.LastWriteTime.Month| Should -BeExactly 1 + $entry.LastWriteTime.Day | Should -BeExactly 1 + $entry.LastWriteTime.Hour | Should -BeExactly 0 + $entry.LastWriteTime.Minute | Should -BeExactly 0 + $entry.LastWriteTime.Second | Should -BeExactly 0 + $entry.LastWriteTime.Millisecond | Should -BeExactly 0 + + + $archive.Dispose() + $archiveStream.Dispose() + } } Context "Update tests" -Skip { @@ -589,12 +631,22 @@ BeforeDiscovery { Context "Special and Wildcard Characters Tests" { BeforeAll { +<<<<<<< HEAD New-Item TestDrive:/SourceDir -Type Directory | Out-Null $content = "Some Data" $content | Out-File -FilePath TestDrive:/SourceDir/Sample-1.txt New-Item -LiteralPath "$TestDrive$($DS)Source[]Dir" -Type Directory | Out-Null $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)file1[].txt +======= + New-Item $TestDrive$($DS)SourceDir -Type Directory | Out-Null + + New-Item -Path "$TestDrive$($DS)Source`[`]Dir" -Type Directory | Out-Null + + $content = "Some Data" + $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)Sample-1.txt + $content | Out-File -LiteralPath $TestDrive$($DS)file1[].txt +>>>>>>> 8b3dcd5 (worked on Expand-Archive, added IEntry class, added support for ShouldProcess in Expand-Archive) } It "Accepts DestinationPath parameter with wildcard characters that resolves to one path" { @@ -610,14 +662,20 @@ BeforeDiscovery { $destinationPath = "TestDrive:/archive[2.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath +<<<<<<< HEAD $destinationPath | Should -BeZipArchiveOnlyContaining @("SourceDir/", "SourceDir/Sample-1.txt") -LiteralPath Remove-Item -LiteralPath $destinationPath +======= + Test-Path -LiteralPath $destinationPath | Should -Be $true + Test-ZipArchive $destinationPath @("SourceDir/", "SourceDir/Sample-1.txt") + Remove-Item -LiteralPath $destinationPath -Force +>>>>>>> 8b3dcd5 (worked on Expand-Archive, added IEntry class, added support for ShouldProcess in Expand-Archive) } It "Accepts 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" "Some Random Content" | Out-File -LiteralPath "$sourcePath$($DS)Sample[]File.txt" - $destinationPath = "$TestDrive$($DS)archive1.zip" + $destinationPath = "$TestDrive$($DS)archive3.zip" try { Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath @@ -630,8 +688,8 @@ BeforeDiscovery { } It "Accepts LiteralPath parameter for a file with wildcards in the filename" { - $sourcePath = "$TestDrive$($DS)SourceDir($DS)file1[].txt" - $destinationPath = "$TestDrive$($DS)archive1.zip" + $sourcePath = "$TestDrive$($DS)file1[].txt" + $destinationPath = "$TestDrive$($DS)archive4.zip" try { Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath diff --git a/src/ArchiveCommandBase.cs b/src/ArchiveCommandBase.cs new file mode 100644 index 0000000..4dab683 --- /dev/null +++ b/src/ArchiveCommandBase.cs @@ -0,0 +1,52 @@ +using Microsoft.PowerShell.Archive.Localized; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.Archive +{ + /// + /// This class is meant to be a base class for all cmdlets in the archive module + /// + public class ArchiveCommandBase : PSCmdlet + { + protected ArchiveFormat DetermineArchiveFormat(string destinationPath, ArchiveFormat? archiveFormat) + { + // 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 archiveFormatBasedOnExt); + // If the user did not specify which archive format to use, try to determine it automatically + if (archiveFormat is null) + { + if (ableToDetermineArchiveFormat) + { + archiveFormat = archiveFormatBasedOnExt; + } + 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); + archiveFormat = ArchiveFormat.zip; + } + // Write a verbose message saying that Format is not specified and a format was determined automatically + string verboseMessage = String.Format(Messages.ArchiveFormatDeterminedVerboseMessage, archiveFormat); + 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 != archiveFormat.Value) + { + var warningMsg = String.Format(Messages.ArchiveExtensionDoesNotMatchArchiveFormatWarning, destinationPath); + WriteWarning(warningMsg); + } + } + + // archiveFormat is never null at this point + return archiveFormat.Value; + } + } +} diff --git a/src/ErrorMessages.cs b/src/ErrorMessages.cs index 0b68a9b..805b74d 100644 --- a/src/ErrorMessages.cs +++ b/src/ErrorMessages.cs @@ -34,7 +34,7 @@ internal static string GetErrorMessage(ErrorCode errorCode) ErrorCode.DestinationExistsAsDirectory => Messages.DestinationExistsAsDirectoryMessage, ErrorCode.ArchiveReadOnly => Messages.ArchiveIsReadOnlyMessage, ErrorCode.ArchiveDoesNotExist => Messages.ArchiveDoesNotExistMessage, - ErrorCode.ArchiveIsNonEmptyDirectory => Messages.ArchiveIsNonEmptyDirectory, + ErrorCode.DestinationIsNonEmptyDirectory => Messages.DestinationIsNonEmptyDirectory, ErrorCode.SamePathAndDestinationPath => Messages.SamePathAndDestinationPathMessage, ErrorCode.SameLiteralPathAndDestinationPath => Messages.SameLiteralPathAndDestinationPathMessage, ErrorCode.InsufficientPermissionsToAccessPath => Messages.InsufficientPermssionsToAccessPathMessage, @@ -58,7 +58,7 @@ internal enum ErrorCode // Used when DestinationPath is an existing directory DestinationExistsAsDirectory, // Used when DestinationPath is a non-empty directory and Action Overwrite is specified - ArchiveIsNonEmptyDirectory, + DestinationIsNonEmptyDirectory, // Used when Compress-Archive cmdlet is in Update mode but the archive is read-only ArchiveReadOnly, // Used when DestinationPath does not exist and the Compress-Archive cmdlet is in Update mode diff --git a/src/ExpandArchiveCommand.cs b/src/ExpandArchiveCommand.cs new file mode 100644 index 0000000..21a6f0b --- /dev/null +++ b/src/ExpandArchiveCommand.cs @@ -0,0 +1,251 @@ +using Microsoft.PowerShell.Archive.Localized; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Management.Automation; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.Archive +{ + [Cmdlet("Expand", "Archive", SupportsShouldProcess = true)] + [OutputType(typeof(System.IO.FileSystemInfo))] + public class ExpandArchiveCommand: ArchiveCommandBase + { + [Parameter(Position=0, Mandatory = true, ParameterSetName = "Path", ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Path { get; set; } = String.Empty; + + [Parameter(Mandatory = true, ParameterSetName = "LiteralPath", ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string LiteralPath { get; set; } = String.Empty; + + [Parameter(Position = 2, Mandatory = true)] + public string DestinationPath { get; set; } = String.Empty; + + [Parameter] + public ExpandArchiveWriteMode WriteMode { get; set; } = ExpandArchiveWriteMode.Expand; + + [Parameter()] + public ArchiveFormat? Format { get; set; } = null; + + [Parameter] + public SwitchParameter PassThru { get; set; } + + #region PrivateMembers + + private PathHelper _pathHelper; + + private System.IO.FileSystemInfo? _destinationPathInfo; + + private bool _didCreateOutput; + + #endregion + + public ExpandArchiveCommand() + { + _didCreateOutput = false; + _pathHelper = new PathHelper(cmdlet: this); + _destinationPathInfo = null; + } + + protected override void BeginProcessing() + { + // Resolve DestinationPath + _destinationPathInfo = _pathHelper.ResolveToSingleFullyQualifiedPath(path: DestinationPath, hasWildcards: false); + DestinationPath = _destinationPathInfo.FullName; + + ValidateDestinationPath(); + } + + protected override void ProcessRecord() + { + + } + + protected override void EndProcessing() + { + // Resolve Path or LiteralPath + bool checkForWildcards = ParameterSetName.StartsWith("Path"); + string path = ParameterSetName.StartsWith("Path") ? Path : LiteralPath; + System.IO.FileSystemInfo sourcePath = _pathHelper.ResolveToSingleFullyQualifiedPath(path: path, hasWildcards: checkForWildcards); + + ValidateSourcePath(sourcePath); + + // Determine archive format based on sourcePath + Format = DetermineArchiveFormat(destinationPath: sourcePath.FullName, archiveFormat: Format); + + // Get an archive from source path -- this is where we will switch between different types of archives + using IArchive? archive = ArchiveFactory.GetArchive(format: Format ?? ArchiveFormat.zip, archivePath: sourcePath.FullName, archiveMode: ArchiveMode.Extract, compressionLevel: System.IO.Compression.CompressionLevel.NoCompression); + try + { + // If the destination path is a file that needs to be overwriten, delete it + if (_destinationPathInfo.Exists && !_destinationPathInfo.Attributes.HasFlag(FileAttributes.Directory) && WriteMode == ExpandArchiveWriteMode.Overwrite) + { + if (ShouldProcess(target: _destinationPathInfo.FullName, action: "Overwrite")) + { + _destinationPathInfo.Delete(); + System.IO.Directory.CreateDirectory(_destinationPathInfo.FullName); + _destinationPathInfo = new System.IO.DirectoryInfo(_destinationPathInfo.FullName); + } + } + + // If the destination path does not exist, create it + if (!_destinationPathInfo.Exists && ShouldProcess(target: _destinationPathInfo.FullName, action: "Create")) + { + System.IO.Directory.CreateDirectory(_destinationPathInfo.FullName); + _destinationPathInfo = new System.IO.DirectoryInfo(_destinationPathInfo.FullName); + } + + // Get the next entry in the archive + var nextEntry = archive.GetNextEntry(); + while (nextEntry != null) + { + // TODO: Refactor this part + + // The location of the entry post-expanding of the archive + string postExpandPath = GetPostExpansionPath(entryName: nextEntry.Name, destinationPath: _destinationPathInfo.FullName); + + // If the entry name is invalid, write a non-terminating error + if (IsPathInvalid(postExpandPath)) + { + var errorRecord = ErrorMessages.GetErrorRecord(ErrorCode.InvalidPath, postExpandPath); + WriteError(errorRecord); + continue; + } + + System.IO.FileSystemInfo postExpandPathInfo = new System.IO.FileInfo(postExpandPath); + + if (!postExpandPathInfo.Exists && System.IO.Directory.Exists(postExpandPath)) + { + var directoryInfo = new System.IO.DirectoryInfo(postExpandPath); + // If postExpandPath is an existing directory containing files and/or directories, then write an error + if (directoryInfo.GetFileSystemInfos().Length > 0) + { + var errorRecord = ErrorMessages.GetErrorRecord(ErrorCode.DestinationIsNonEmptyDirectory, postExpandPath); + WriteError(errorRecord); + continue; + } + postExpandPathInfo = directoryInfo; + } + + // Throw an error if the cmdlet is not in Overwrite mode but the postExpandPath exists + if (postExpandPathInfo.Exists && WriteMode != ExpandArchiveWriteMode.Overwrite) + { + var errorRecord = ErrorMessages.GetErrorRecord(ErrorCode.DestinationExists, postExpandPath); + WriteError(errorRecord); + continue; + } + + if (postExpandPathInfo.Exists && ShouldProcess(target: _destinationPathInfo.FullName, action: "Expand and Overwrite")) + { + postExpandPathInfo.Delete(); + nextEntry.ExpandTo(_destinationPathInfo.FullName); + } else if (ShouldProcess(target: _destinationPathInfo.FullName, action: "Expand")) + { + nextEntry.ExpandTo(_destinationPathInfo.FullName); + } + + + nextEntry = archive.GetNextEntry(); + } + + + } catch + { + + } + } + + protected override void StopProcessing() + { + // Do clean up if the user abruptly stops execution + } + + #region PrivateMethods + + private void ValidateDestinationPath() + { + ErrorCode? errorCode = null; + + // In this case, DestinationPath does not exist + if (!_destinationPathInfo.Exists) + { + // Do nothing + } + // Check if DestinationPath is an existing directory + else if (_destinationPathInfo.Attributes.HasFlag(FileAttributes.Directory)) + { + // Throw an error if the DestinationPath is the current working directory and the cmdlet is in Overwrite mode + if (WriteMode == ExpandArchiveWriteMode.Overwrite && _destinationPathInfo.FullName == SessionState.Path.CurrentFileSystemLocation.ProviderPath) + { + errorCode = ErrorCode.CannotOverwriteWorkingDirectory; + } + } + // If DestinationPath is an existing file + else + { + // Throw an error if DestinationPath exists and the cmdlet is not in Overwrite mode + if (WriteMode == ExpandArchiveWriteMode.Expand) + { + errorCode = ErrorCode.DestinationExists; + } + } + + if (errorCode != null) + { + // Throw an error -- since we are validating DestinationPath, the problem is with DestinationPath + var errorRecord = ErrorMessages.GetErrorRecord(errorCode: errorCode.Value, errorItem: _destinationPathInfo.FullName); + ThrowTerminatingError(errorRecord); + } + } + + private void ValidateSourcePath(System.IO.FileSystemInfo sourcePath) + { + // Throw a terminating error if sourcePath does not exist + if (!sourcePath.Exists) + { + var errorRecord = ErrorMessages.GetErrorRecord(errorCode: ErrorCode.PathNotFound, errorItem: sourcePath.FullName); + ThrowTerminatingError(errorRecord); + } + + // Throw a terminating error if sourcePath is a directory + if (sourcePath.Attributes.HasFlag(FileAttributes.Directory)) + { + var errorRecord = ErrorMessages.GetErrorRecord(errorCode: ErrorCode.DestinationExistsAsDirectory, errorItem: sourcePath.FullName); + ThrowTerminatingError(errorRecord); + } + + // Ensure sourcePath is not the same as the destination path when the cmdlet is in overwrite mode + // When the cmdlet is not in overwrite mode, other errors will be thrown when validating DestinationPath before it even gets to this line + if (PathHelper.ArePathsSame(sourcePath, _destinationPathInfo) && WriteMode == ExpandArchiveWriteMode.Overwrite) + { + var errorRecord = ErrorMessages.GetErrorRecord(errorCode: ErrorCode.SamePathAndDestinationPath, errorItem: sourcePath.FullName); + ThrowTerminatingError(errorRecord); + } + } + + private string GetPostExpansionPath(string entryName, string destinationPath) + { + // Normalize entry name - on Windows, replace forwardslash with backslash + string normalizedEntryName = entryName.Replace(System.IO.Path.AltDirectorySeparatorChar, System.IO.Path.DirectorySeparatorChar); + return System.IO.Path.Combine(destinationPath, normalizedEntryName); + } + + private bool IsPathInvalid(string path) + { + foreach (var invalidCharacter in System.IO.Path.GetInvalidPathChars()) + { + if (path.Contains(invalidCharacter)) + { + return true; + } + } + return false; + } + + #endregion + } +} diff --git a/src/IArchive.cs b/src/IArchive.cs index a785a5f..52d9aef 100644 --- a/src/IArchive.cs +++ b/src/IArchive.cs @@ -24,6 +24,8 @@ internal interface IArchive: IDisposable // Throws an exception if the archive is in create mode. internal string[] GetEntries(); + internal IEntry? GetNextEntry(); + // 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/IEntry.cs b/src/IEntry.cs new file mode 100644 index 0000000..c3dfde8 --- /dev/null +++ b/src/IEntry.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.Archive +{ + internal interface IEntry + { + public string Name { get; } + + public void ExpandTo(string destinationPath); + } +} diff --git a/src/Localized/Messages.resx b/src/Localized/Messages.resx index 7f9ebfc..b2fe38f 100644 --- a/src/Localized/Messages.resx +++ b/src/Localized/Messages.resx @@ -135,9 +135,6 @@ 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. @@ -152,6 +149,9 @@ The destination path {0} already exists. + + The destination {0} cannot be overwritten because it is a non-empty directory. + The path(s) {0} have been specified more than once. diff --git a/src/TarArchive.cs b/src/TarArchive.cs index da25815..5ae2e4d 100644 --- a/src/TarArchive.cs +++ b/src/TarArchive.cs @@ -43,6 +43,11 @@ string[] IArchive.GetEntries() throw new NotImplementedException(); } + IEntry? IArchive.GetNextEntry() + { + return null; + } + void IArchive.Expand(string destinationPath) { throw new NotImplementedException(); diff --git a/src/ZipArchive.cs b/src/ZipArchive.cs index 4c74147..8c58119 100644 --- a/src/ZipArchive.cs +++ b/src/ZipArchive.cs @@ -24,6 +24,8 @@ internal class ZipArchive : IArchive private const char ZipArchiveDirectoryPathTerminator = '/'; + private int _entryIndex; + ArchiveMode IArchive.Mode => _mode; string IArchive.Path => _archivePath; @@ -36,6 +38,7 @@ public ZipArchive(string archivePath, ArchiveMode mode, System.IO.FileStream arc _archiveStream = archiveStream; _zipArchive = new System.IO.Compression.ZipArchive(stream: archiveStream, mode: ConvertToZipArchiveMode(_mode), leaveOpen: true); _compressionLevel = compressionLevel; + _entryIndex = -1; } // If a file is added to the archive when it already contains a folder with the same name, @@ -89,6 +92,26 @@ string[] IArchive.GetEntries() throw new NotImplementedException(); } + IEntry? IArchive.GetNextEntry() + { + if (_entryIndex < 0 || _entryIndex >= _zipArchive.Entries.Count) + { + _entryIndex = 0; + } + + // If there are no entries, return null + if (_zipArchive.Entries.Count == 0) + { + return null; + } + + // Create an ZipArchive.ZipArchiveEntry object + var nextEntry = _zipArchive.Entries[_entryIndex]; + _entryIndex++; + + return new ZipArchiveEntry(nextEntry); + } + void IArchive.Expand(string destinationPath) { throw new NotImplementedException(); @@ -125,5 +148,24 @@ public void Dispose() Dispose(disposing: true); GC.SuppressFinalize(this); } + + internal class ZipArchiveEntry : IEntry + { + // Underlying object is System.IO.Compression.ZipArchiveEntry + + private System.IO.Compression.ZipArchiveEntry _entry; + + string IEntry.Name => _entry.FullName; + + void IEntry.ExpandTo(string destinationPath) + { + _entry.ExtractToFile(destinationPath); + } + + internal ZipArchiveEntry(System.IO.Compression.ZipArchiveEntry entry) + { + _entry = entry; + } + } } } From 9839283fa7ad5ac02270ba996a8b3439dafcb190 Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Thu, 28 Jul 2022 12:07:42 -0700 Subject: [PATCH 04/34] added Expand-Archive.Tests.ps1 and worked on parameter set validation tests --- Tests/Expand-Archive.Tests.ps1 | 348 ++++++++++++++++++++++++++ src/ExpandArchiveCommand.cs | 11 +- src/Microsoft.PowerShell.Archive.psd1 | 3 +- 3 files changed, 359 insertions(+), 3 deletions(-) create mode 100644 Tests/Expand-Archive.Tests.ps1 diff --git a/Tests/Expand-Archive.Tests.ps1 b/Tests/Expand-Archive.Tests.ps1 new file mode 100644 index 0000000..2a5be16 --- /dev/null +++ b/Tests/Expand-Archive.Tests.ps1 @@ -0,0 +1,348 @@ +# Tests for Expand-Archive + +Describe("Expand-Archive Tests") { + BeforeAll { + 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 + } + } + $CmdletClassName = "Microsoft.PowerShell.Archive.ExpandArchiveCommand" + $DS = [System.IO.Path]::DirectorySeparatorChar + Add-CompressionAssemblies + + # Progress perference + $originalProgressPref = $ProgressPreference + $ProgressPreference = "SilentlyContinue" + } + + AfterAll { + $global:ProgressPreference = $originalProgressPref + } + + Context "Parameter set validation tests" { + BeforeAll { + function ExpandArchivePathParameterSetValidator { + param + ( + [string] $path, + [string] $destinationPath + ) + + try + { + Expand-Archive -Path $path -DestinationPath $destinationPath + throw "ValidateNotNullOrEmpty attribute is missing on one of parameters belonging to Path parameterset." + } + catch + { + $_.FullyQualifiedErrorId | Should -Be "ParameterArgumentValidationError,$CmdletClassName" + } + } + + function ExpandArchiveLiteralPathParameterSetValidator { + param + ( + [string] $literalPath, + [string] $destinationPath + ) + + try + { + Expand-Archive -LiteralPath $literalPath -DestinationPath $destinationPath + throw "ValidateNotNullOrEmpty attribute is missing on one of parameters belonging to LiteralPath parameterset." + } + catch + { + $_.FullyQualifiedErrorId | Should -Be "ParameterArgumentValidationError,$CmdletClassName" + } + } + + # Set up files for tests + New-Item $TestDrive$($DS)SourceDir -Type Directory | Out-Null + $content = "Some Data" + $content | Out-File -FilePath $TestDrive$($DS)Sample-1.txt + + # Create archives called archive1.zip and archive2.zip + Compress-Archive -Path $TestDrive$($DS)Sample-1.txt -DestinationPath $TestDrive$($DS)archive1.zip + Compress-Archive -Path $TestDrive$($DS)Sample-1.txt -DestinationPath $TestDrive$($DS)archive2.zip + } + + + It "Validate errors with NULL & EMPTY values for Path, LiteralPath, and DestinationPath" { + $sourcePath = "$TestDrive$($DS)SourceDir" + $destinationPath = "$TestDrive$($DS)SampleSingleFile.zip" + + ExpandArchivePathParameterSetValidator $null $destinationPath + ExpandArchivePathParameterSetValidator $sourcePath $null + ExpandArchivePathParameterSetValidator $null $null + + ExpandArchivePathParameterSetValidator "" $destinationPath + ExpandArchivePathParameterSetValidator $sourcePath "" + ExpandArchivePathParameterSetValidator "" "" + + ExpandArchiveLiteralPathParameterSetValidator $null $destinationPath + ExpandArchiveLiteralPathParameterSetValidator $sourcePath $null + ExpandArchiveLiteralPathParameterSetValidator $null $null + + ExpandArchiveLiteralPathParameterSetValidator "" $destinationPath + ExpandArchiveLiteralPathParameterSetValidator $sourcePath "" + ExpandArchiveLiteralPathParameterSetValidator "" "" + } + + It "Throws when invalid path non-existing path is supplied for Path or LiteralPath parameters" { + $path = "$TestDrive$($DS)non-existant.zip" + $destinationPath = "$TestDrive($DS)DestinationFolder" + try + { + Expand-Archive -Path $path -DestinationPath $destinationPath + throw "Failed to validate that an invalid Path $invalidPath was supplied as input to Expand-Archive cmdlet." + } + catch + { + $_.FullyQualifiedErrorId | Should -Be "PathNotFound,$CmdletClassName" + } + + try + { + Expand-Archive -LiteralPath $path -DestinationPath $destinationPath + throw "Failed to validate that an invalid LiteralPath $invalidPath was supplied as input to Expand-Archive cmdlet." + } + catch + { + $_.FullyQualifiedErrorId | Should -Be "PathNotFound,$CmdletClassName" + } + } + + It "Throws when invalid path non-filesystem path is supplied for Path or LiteralPath parameters" { + $path = "Variable:DS" + $destinationPath = "$TestDrive($DS)DestinationFolder" + try + { + Expand-Archive -Path $path -DestinationPath $destinationPath + throw "Failed to validate that an invalid Path $invalidPath was supplied as input to Expand-Archive cmdlet." + } + catch + { + $_.FullyQualifiedErrorId | Should -Be "InvalidPath,$CmdletClassName" + } + + try + { + Expand-Archive -LiteralPath $path -DestinationPath $destinationPath + throw "Failed to validate that an invalid LiteralPath $invalidPath was supplied as input to Expand-Archive cmdlet." + } + catch + { + $_.FullyQualifiedErrorId | Should -Be "InvalidPath,$CmdletClassName" + } + } + + It "Throws an error when multiple paths are supplied as input to Path parameter" { + $sourcePath = @( + "$TestDrive$($DS)SourceDir$($DS)archive1.zip", + "$TestDrive$($DS)SourceDir$($DS)archive2.zip") + $destinationPath = "$TestDrive$($DS)DestinationFolder" + + try + { + Expand-Archive -Path $sourcePath -DestinationPath $destinationPath + throw "Failed to detect that duplicate Path $sourcePath is supplied as input to Path parameter." + } + catch + { + $_.FullyQualifiedErrorId | Should -Be "CannotConvertArgument,$CmdletClassName" + } + } + + It "Throws an error when multiple paths are supplied as input to LiteralPath parameter" { + $sourcePath = @( + "$TestDrive$($DS)SourceDir$($DS)archive1.zip", + "$TestDrive$($DS)SourceDir$($DS)archive2.zip") + $destinationPath = "$TestDrive$($DS)DestinationFolder" + + try + { + Expand-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath + throw "Failed to detect that duplicate Path $sourcePath is supplied as input to LiteralPath parameter." + } + catch + { + $_.FullyQualifiedErrorId | Should -Be "CannotConvertArgument,$CmdletClassName" + } + } + + ## From 504 + 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 + "Some Data" | Out-File -FilePath $sourcePath$($DS)SampleSourceFileForArchive.txt + try + { + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + Test-Path $destinationPath | Should -Be $true + } + finally + { + Remove-Item "$sourcePath" -Force -Recurse -ErrorAction SilentlyContinue + } + } + + It "Throws an error when Path and DestinationPath are the same and -WriteMode Overwrite is specified" { + $sourcePath = "$TestDrive$($DS)archive1.zip" + $destinationPath = $sourcePath + + try { + Expand-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,$CmdletClassName" + } + } + + It "Throws an error when LiteralPath and DestinationPath are the same and WriteMode -Overwrite is specified" { + $sourcePath = "$TestDrive$($DS)archive1.zip" + $destinationPath = $sourcePath + + try { + Expand-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 "SameLiteralPathAndDestinationPath,$CmdletClassName" + } + } + } + + Context "DestinationPath and Overwrite Tests" { + # error when destination path is a file and overwrite is not specified + # error when output has same name as existant file and overwrite is not specified + + # no error when destination path is existing folder + # no error when output is folder + + # output is directory w/ at least 1 item + # output has same name as current working directory + + # overwrite file works + # overwrite output file works done + # overwrite output file w/directory + # overwrite non-existant path works + + # last write times + + BeforeAll { + "Hello, World!" | Out-File -FilePath "$TestDrive$($DS)file1.txt" + Compress-Archive -Path "$TestDrive$($DS)file1.txt" -DestinationPath "$TestDrive$($DS)archive1.zip" + + New-Item -Path "$TestDrive$($DS)directory1" -ItemType Directory + + # Create archive2.zip containing directory1 + Compress-Archive -Path "$TestDrive$($DS)directory1" -DestinationPath "$TestDrive$($DS)archive2.zip" + + New-Item -Path "$TestDrive$($DS)ParentDir" -ItemType Directory + New-Item -Path "$TestDrive$($DS)ParentDir/file1.txt" -ItemType Directory + + # Create a dir that is a container for items to be overwritten + New-Item -Path "$TestDrive$($DS)ItemsToOverwriteContainer" -ItemType Directory + New-Item -Path "$TestDrive$($DS)ItemsToOverwriteContainer/file2" -ItemType File + New-Item -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir1" -ItemType Directory + New-Item -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir1/file1.txt" -ItemType File + } + + It "Throws an error when DestinationPath is an existing file" { + $sourcePath = "$TestDrive$($DS)archive1.zip" + $destinationPath = "$TestDrive$($DS)file1.txt" + + try { + Expand-Archive -Path $sourcePath -DestinationPath $destinationPath + } catch { + $_.FullyQualifiedErrorId | Should -Be "DestinationExists,$CmdletClassName" + } + } + + It "Does not throw an error when DestinationPath is an existing directory" { + $sourcePath = "$TestDrive$($DS)archive1.zip" + $destinationPath = "$TestDrive$($DS)directory1" + + try { + Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -ErrorAction Stop + } catch { + throw "An error was thrown but an error was not expected" + } + } + + It "Does not throw an error when a directory in the archive has the same destination path as an existing directory" { + $sourcePath = "$TestDrive$($DS)archive2.zip" + $destinationPath = "$TestDrive" + + try { + Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -ErrorAction Stop + } catch { + throw "An error was thrown but an error was not expected" + } + } + + It "Writes a non-terminating error when a file in the archive has a destination path that already exists" { + $sourcePath = "$TestDrive$($DS)archive1.zip" + $destinationPath = "$TestDrive" + + Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -ErrorVariable error + $error.Count | Should -Be 1 + $error[0].FullyQualifiedErrorId | Should -Be "DestinationExists,$CmdletClassName" + } + + It "Writes a non-terminating error when a file in the archive has a destination path that is an existing directory containing at least 1 item and -WriteMode Overwrite is specified" { + $sourcePath = "$TestDrive$($DS)archive1.zip" + $destinationPath = "$TestDrive" + + Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite -ErrorVariable error + $error.Count | Should -Be 1 + $error[0].FullyQualifiedErrorId | Should -Be "DestinationExists,$CmdletClassName" + } + + It "Writes a non-terminating error when a file in the archive has a destination path that is the working directory and -WriteMode Overwrite is specified" { + $sourcePath = "$TestDrive$($DS)archive1.zip" + $destinationPath = "$TestDrive$($DS)ParentDir/file1.txt" + + Push-Location $destinationPath + + Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite -ErrorVariable error + $error.Count | Should -Be 1 + $error[0].FullyQualifiedErrorId | Should -Be "CannotOverwriteWorkingDirectory,$CmdletClassName" + + Pop-Location + } + + It "Overwrites a file when it is DestinationPath and -WriteMode Overwrite is specified" { + $sourcePath = "$TestDrive$($DS)archive1.zip" + $destinationPath = "$TestDrive$($DS)ItemsToOverwriteContainer/file2" + + Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite -ErrorVariable error + $error.Count | Should -Be 0 + + # Ensure the file in archive1.zip was expanded + Test-Path "$TestDrive$($DS)ItemsToOverwriteContainer/file2/file1.txt" + } + + It "Overwrites a file whose path is the same as the destination path of a file in the archive when -WriteMode Overwrite is specified" { + $sourcePath = "$TestDrive$($DS)archive1.zip" + $destinationPath = "$TestDrive$($DS)ItemsToOverwriteContainer/subdir1" + + Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite -ErrorVariable error + $error.Count | Should -Be 0 + + # Ensure the file in archive1.zip was expanded + Test-Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir1/file1.txt" + + # Ensure the contents of file1.txt is "Hello, World!" + Get-Content -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir1/file1.txt" | Should -Be "Hello, World!" + } + } +} \ No newline at end of file diff --git a/src/ExpandArchiveCommand.cs b/src/ExpandArchiveCommand.cs index 21a6f0b..f354746 100644 --- a/src/ExpandArchiveCommand.cs +++ b/src/ExpandArchiveCommand.cs @@ -23,6 +23,7 @@ public class ExpandArchiveCommand: ArchiveCommandBase public string LiteralPath { get; set; } = String.Empty; [Parameter(Position = 2, Mandatory = true)] + [ValidateNotNullOrEmpty] public string DestinationPath { get; set; } = String.Empty; [Parameter] @@ -222,7 +223,15 @@ private void ValidateSourcePath(System.IO.FileSystemInfo sourcePath) // When the cmdlet is not in overwrite mode, other errors will be thrown when validating DestinationPath before it even gets to this line if (PathHelper.ArePathsSame(sourcePath, _destinationPathInfo) && WriteMode == ExpandArchiveWriteMode.Overwrite) { - var errorRecord = ErrorMessages.GetErrorRecord(errorCode: ErrorCode.SamePathAndDestinationPath, errorItem: sourcePath.FullName); + ErrorCode errorCode; + if (ParameterSetName == "Path") + { + errorCode = ErrorCode.SamePathAndDestinationPath; + } else + { + errorCode = ErrorCode.SameLiteralPathAndDestinationPath; + } + var errorRecord = ErrorMessages.GetErrorRecord(errorCode: errorCode, errorItem: sourcePath.FullName); ThrowTerminatingError(errorRecord); } } diff --git a/src/Microsoft.PowerShell.Archive.psd1 b/src/Microsoft.PowerShell.Archive.psd1 index a047d5a..46df1a8 100644 --- a/src/Microsoft.PowerShell.Archive.psd1 +++ b/src/Microsoft.PowerShell.Archive.psd1 @@ -18,5 +18,4 @@ PrivateData = @{ '@ Prerelease = 'preview1' } -} -} +} \ No newline at end of file From 9e6404c6962017648d3a772ec0dd17f5f2ad12bf Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Thu, 28 Jul 2022 16:33:17 -0700 Subject: [PATCH 05/34] added and updated tests for Expand-Archive --- Tests/Expand-Archive.Tests.ps1 | 57 ++++++++++++++--- src/ExpandArchiveCommand.cs | 112 +++++++++++++++++++-------------- src/IEntry.cs | 2 + src/ZipArchive.cs | 8 ++- 4 files changed, 120 insertions(+), 59 deletions(-) diff --git a/Tests/Expand-Archive.Tests.ps1 b/Tests/Expand-Archive.Tests.ps1 index 2a5be16..e684ccc 100644 --- a/Tests/Expand-Archive.Tests.ps1 +++ b/Tests/Expand-Archive.Tests.ps1 @@ -232,7 +232,9 @@ Describe("Expand-Archive Tests") { # overwrite file works # overwrite output file works done + # overwrite file w/file done # overwrite output file w/directory + # overwrite directory w/file # overwrite non-existant path works # last write times @@ -254,6 +256,15 @@ Describe("Expand-Archive Tests") { New-Item -Path "$TestDrive$($DS)ItemsToOverwriteContainer/file2" -ItemType File New-Item -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir1" -ItemType Directory New-Item -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir1/file1.txt" -ItemType File + New-Item -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir2" -ItemType Directory + New-Item -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir2/file1.txt" -ItemType Directory + New-Item -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir4" -ItemType Directory + New-Item -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir4/file1.txt" -ItemType Directory + New-Item -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir4/file1.txt/somefile" -ItemType File + + # Create directory to override + New-Item -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir3" -ItemType Directory + New-Item -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir3/directory1" -ItemType File } It "Throws an error when DestinationPath is an existing file" { @@ -300,11 +311,11 @@ Describe("Expand-Archive Tests") { It "Writes a non-terminating error when a file in the archive has a destination path that is an existing directory containing at least 1 item and -WriteMode Overwrite is specified" { $sourcePath = "$TestDrive$($DS)archive1.zip" - $destinationPath = "$TestDrive" + $destinationPath = "$TestDrive$($DS)ItemsToOverwriteContainer/subdir4" Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite -ErrorVariable error $error.Count | Should -Be 1 - $error[0].FullyQualifiedErrorId | Should -Be "DestinationExists,$CmdletClassName" + $error[0].FullyQualifiedErrorId | Should -Be "DestinationIsNonEmptyDirectory,$CmdletClassName" } It "Writes a non-terminating error when a file in the archive has a destination path that is the working directory and -WriteMode Overwrite is specified" { @@ -313,11 +324,13 @@ Describe("Expand-Archive Tests") { Push-Location $destinationPath - Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite -ErrorVariable error - $error.Count | Should -Be 1 - $error[0].FullyQualifiedErrorId | Should -Be "CannotOverwriteWorkingDirectory,$CmdletClassName" - - Pop-Location + try { + Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite -ErrorVariable error + $error.Count | Should -Be 1 + $error[0].FullyQualifiedErrorId | Should -Be "CannotOverwriteWorkingDirectory,$CmdletClassName" + } finally { + Pop-Location + } } It "Overwrites a file when it is DestinationPath and -WriteMode Overwrite is specified" { @@ -328,10 +341,10 @@ Describe("Expand-Archive Tests") { $error.Count | Should -Be 0 # Ensure the file in archive1.zip was expanded - Test-Path "$TestDrive$($DS)ItemsToOverwriteContainer/file2/file1.txt" + Test-Path "$TestDrive$($DS)ItemsToOverwriteContainer/file2/file1.txt" -PathType Leaf } - It "Overwrites a file whose path is the same as the destination path of a file in the archive when -WriteMode Overwrite is specified" { + It "Overwrites a file whose path is the same as the destination path of a file in the archive when -WriteMode Overwrite is specified" -Tag td { $sourcePath = "$TestDrive$($DS)archive1.zip" $destinationPath = "$TestDrive$($DS)ItemsToOverwriteContainer/subdir1" @@ -339,10 +352,34 @@ Describe("Expand-Archive Tests") { $error.Count | Should -Be 0 # Ensure the file in archive1.zip was expanded - Test-Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir1/file1.txt" + Test-Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir1/file1.txt" -PathType Leaf # Ensure the contents of file1.txt is "Hello, World!" Get-Content -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir1/file1.txt" | Should -Be "Hello, World!" } + + It "Overwrites a directory whose path is the same as the destination path of a file in the archive when -WriteMode Overwrite is specified" { + $sourcePath = "$TestDrive$($DS)archive1.zip" + $destinationPath = "$TestDrive$($DS)ItemsToOverwriteContainer/subdir2" + + Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite -ErrorVariable error + $error.Count | Should -Be 0 + + # Ensure the file in archive1.zip was expanded + Test-Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir2/file1.txt" -PathType Leaf + + # Ensure the contents of file1.txt is "Hello, World!" + Get-Content -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir1/file1.txt" | Should -Be "Hello, World!" + } + + It "Overwrites a file whose path is the same as the destination path of a directory in the archive when -WriteMode Overwrite is specified" { + $sourcePath = "$TestDrive$($DS)archive2.zip" + $destinationPath = "$TestDrive$($DS)ItemsToOverwriteContainer/subdir3" + Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite -ErrorVariable error + $error.Count | Should -Be 0 + + # Ensure the file in archive1.zip was expanded + Test-Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir3/directory1" -PathType Container + } } } \ No newline at end of file diff --git a/src/ExpandArchiveCommand.cs b/src/ExpandArchiveCommand.cs index f354746..0f1f64a 100644 --- a/src/ExpandArchiveCommand.cs +++ b/src/ExpandArchiveCommand.cs @@ -83,6 +83,7 @@ protected override void EndProcessing() try { // If the destination path is a file that needs to be overwriten, delete it + if (_destinationPathInfo.Exists && !_destinationPathInfo.Attributes.HasFlag(FileAttributes.Directory) && WriteMode == ExpandArchiveWriteMode.Overwrite) { if (ShouldProcess(target: _destinationPathInfo.FullName, action: "Overwrite")) @@ -100,56 +101,11 @@ protected override void EndProcessing() _destinationPathInfo = new System.IO.DirectoryInfo(_destinationPathInfo.FullName); } - // Get the next entry in the archive + // Get the next entry in the archive and process it var nextEntry = archive.GetNextEntry(); while (nextEntry != null) { - // TODO: Refactor this part - - // The location of the entry post-expanding of the archive - string postExpandPath = GetPostExpansionPath(entryName: nextEntry.Name, destinationPath: _destinationPathInfo.FullName); - - // If the entry name is invalid, write a non-terminating error - if (IsPathInvalid(postExpandPath)) - { - var errorRecord = ErrorMessages.GetErrorRecord(ErrorCode.InvalidPath, postExpandPath); - WriteError(errorRecord); - continue; - } - - System.IO.FileSystemInfo postExpandPathInfo = new System.IO.FileInfo(postExpandPath); - - if (!postExpandPathInfo.Exists && System.IO.Directory.Exists(postExpandPath)) - { - var directoryInfo = new System.IO.DirectoryInfo(postExpandPath); - // If postExpandPath is an existing directory containing files and/or directories, then write an error - if (directoryInfo.GetFileSystemInfos().Length > 0) - { - var errorRecord = ErrorMessages.GetErrorRecord(ErrorCode.DestinationIsNonEmptyDirectory, postExpandPath); - WriteError(errorRecord); - continue; - } - postExpandPathInfo = directoryInfo; - } - - // Throw an error if the cmdlet is not in Overwrite mode but the postExpandPath exists - if (postExpandPathInfo.Exists && WriteMode != ExpandArchiveWriteMode.Overwrite) - { - var errorRecord = ErrorMessages.GetErrorRecord(ErrorCode.DestinationExists, postExpandPath); - WriteError(errorRecord); - continue; - } - - if (postExpandPathInfo.Exists && ShouldProcess(target: _destinationPathInfo.FullName, action: "Expand and Overwrite")) - { - postExpandPathInfo.Delete(); - nextEntry.ExpandTo(_destinationPathInfo.FullName); - } else if (ShouldProcess(target: _destinationPathInfo.FullName, action: "Expand")) - { - nextEntry.ExpandTo(_destinationPathInfo.FullName); - } - - + ProcessArchiveEntry(nextEntry); nextEntry = archive.GetNextEntry(); } @@ -167,6 +123,68 @@ protected override void StopProcessing() #region PrivateMethods + private void ProcessArchiveEntry(IEntry entry) + { + // The location of the entry post-expanding of the archive + string postExpandPath = GetPostExpansionPath(entryName: entry.Name, destinationPath: _destinationPathInfo.FullName); + + // If the entry name is invalid, write a non-terminating error and stop processing the entry + if (IsPathInvalid(postExpandPath)) + { + var errorRecord = ErrorMessages.GetErrorRecord(ErrorCode.InvalidPath, postExpandPath); + WriteError(errorRecord); + return; + } + + + System.IO.FileSystemInfo postExpandPathInfo = new System.IO.FileInfo(postExpandPath); + + // Use this variable to keep track if there is a collision + // If the postExpandPath is a file, then no matter if the entry is a file or directory, it is a collision + bool hasCollision = postExpandPathInfo.Exists; + + if (System.IO.Directory.Exists(postExpandPath)) + { + var directoryInfo = new System.IO.DirectoryInfo(postExpandPath); + + // If the entry is a directory and postExpandPath is a directory, no collision occurs (because there is no need to overwrite directories) + hasCollision = !entry.IsDirectory; + + // If postExpandPath is an existing directory containing files and/or directories, then write an error + if (hasCollision && directoryInfo.GetFileSystemInfos().Length > 0) + { + var errorRecord = ErrorMessages.GetErrorRecord(ErrorCode.DestinationIsNonEmptyDirectory, postExpandPath); + WriteError(errorRecord); + return; + } + postExpandPathInfo = directoryInfo; + } + + // Throw an error if the cmdlet is not in Overwrite mode but the postExpandPath exists + if (hasCollision && WriteMode != ExpandArchiveWriteMode.Overwrite) + { + var errorRecord = ErrorMessages.GetErrorRecord(ErrorCode.DestinationExists, postExpandPath); + WriteError(errorRecord); + return; + } + + string expandAction = hasCollision ? "Overwrite and Expand" : "Expand"; + if (ShouldProcess(target: postExpandPath, action: expandAction)) + { + if (hasCollision) + { + postExpandPathInfo.Delete(); + System.Threading.Thread.Sleep(1000); + } + // Only expand the entry if there is a need to expand + // There is a need to expand unless the entry is a directory and the postExpandPath is also a directory + if (!(entry.IsDirectory && postExpandPathInfo.Attributes.HasFlag(FileAttributes.Directory))) + { + entry.ExpandTo(_destinationPathInfo.FullName); + } + } + } + private void ValidateDestinationPath() { ErrorCode? errorCode = null; diff --git a/src/IEntry.cs b/src/IEntry.cs index c3dfde8..503e52b 100644 --- a/src/IEntry.cs +++ b/src/IEntry.cs @@ -10,6 +10,8 @@ internal interface IEntry { public string Name { get; } + public bool IsDirectory { get; } + public void ExpandTo(string destinationPath); } } diff --git a/src/ZipArchive.cs b/src/ZipArchive.cs index 8c58119..1089be0 100644 --- a/src/ZipArchive.cs +++ b/src/ZipArchive.cs @@ -94,13 +94,13 @@ string[] IArchive.GetEntries() IEntry? IArchive.GetNextEntry() { - if (_entryIndex < 0 || _entryIndex >= _zipArchive.Entries.Count) + if (_entryIndex < 0) { _entryIndex = 0; } // If there are no entries, return null - if (_zipArchive.Entries.Count == 0) + if (_zipArchive.Entries.Count == 0 || _entryIndex >= _zipArchive.Entries.Count) { return null; } @@ -157,8 +157,12 @@ internal class ZipArchiveEntry : IEntry string IEntry.Name => _entry.FullName; + bool IEntry.IsDirectory => _entry.FullName.EndsWith(System.IO.Path.AltDirectorySeparatorChar); + void IEntry.ExpandTo(string destinationPath) { + string postExpandPath = System.IO.Path.Combine(destinationPath, _entry.FullName); + System.Diagnostics.Debug.Assert(!System.IO.File.Exists(postExpandPath)); _entry.ExtractToFile(destinationPath); } From 6c3b2965ec7ca2a3ed9ab2aa579c942988c48dff Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Fri, 29 Jul 2022 17:31:26 -0700 Subject: [PATCH 06/34] fixed bug where wrong destination path was determined for an item in the archive, added tests for Expand-Archive --- Tests/Compress-Archive.Tests.ps1 | 2 +- Tests/Expand-Archive.Tests.ps1 | 80 ++++++++++++++++++++++++++++++-- src/ArchiveCommandBase.cs | 4 +- src/ExpandArchiveCommand.cs | 32 ++++++++----- src/ZipArchive.cs | 12 +++-- 5 files changed, 109 insertions(+), 21 deletions(-) diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index 4acaf64..8271248 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -507,7 +507,7 @@ BeforeDiscovery { } catch { - $_.FullyQualifiedErrorId | Should -Be "ArchiveIsNonEmptyDirectory,Microsoft.PowerShell.Archive.CompressArchiveCommand" + $_.FullyQualifiedErrorId | Should -Be "DestinationIsNonEmptyDirectory,Microsoft.PowerShell.Archive.CompressArchiveCommand" } } diff --git a/Tests/Expand-Archive.Tests.ps1 b/Tests/Expand-Archive.Tests.ps1 index e684ccc..2e4c41c 100644 --- a/Tests/Expand-Archive.Tests.ps1 +++ b/Tests/Expand-Archive.Tests.ps1 @@ -240,6 +240,7 @@ Describe("Expand-Archive Tests") { # last write times BeforeAll { + New-Item -Path "$TestDrive$($DS)file1.txt" -ItemType File "Hello, World!" | Out-File -FilePath "$TestDrive$($DS)file1.txt" Compress-Archive -Path "$TestDrive$($DS)file1.txt" -DestinationPath "$TestDrive$($DS)archive1.zip" @@ -265,6 +266,14 @@ Describe("Expand-Archive Tests") { # Create directory to override New-Item -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir3" -ItemType Directory New-Item -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir3/directory1" -ItemType File + + # Set the error action preference so non-terminating errors aren't displayed + $ErrorActionPreference = 'SilentlyContinue' + } + + AfterAll { + # Reset to default value + $ErrorActionPreference = 'Continue' } It "Throws an error when DestinationPath is an existing file" { @@ -320,9 +329,9 @@ Describe("Expand-Archive Tests") { It "Writes a non-terminating error when a file in the archive has a destination path that is the working directory and -WriteMode Overwrite is specified" { $sourcePath = "$TestDrive$($DS)archive1.zip" - $destinationPath = "$TestDrive$($DS)ParentDir/file1.txt" + $destinationPath = "$TestDrive$($DS)ParentDir" - Push-Location $destinationPath + Push-Location "$destinationPath/file1.txt" try { Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite -ErrorVariable error @@ -369,10 +378,10 @@ Describe("Expand-Archive Tests") { Test-Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir2/file1.txt" -PathType Leaf # Ensure the contents of file1.txt is "Hello, World!" - Get-Content -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir1/file1.txt" | Should -Be "Hello, World!" + Get-Content -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir2/file1.txt" | Should -Be "Hello, World!" } - It "Overwrites a file whose path is the same as the destination path of a directory in the archive when -WriteMode Overwrite is specified" { + It "Overwrites a file whose path is the same as the destination path of a directory in the archive when -WriteMode Overwrite is specified" -Tag this1 { $sourcePath = "$TestDrive$($DS)archive2.zip" $destinationPath = "$TestDrive$($DS)ItemsToOverwriteContainer/subdir3" Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite -ErrorVariable error @@ -382,4 +391,67 @@ Describe("Expand-Archive Tests") { Test-Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir3/directory1" -PathType Container } } + + Context "Basic functionality tests" { + # extract to a directory works + # extract to working directory works when DestinationPath is specified + # expand archive works when -DestinationPath is not specified (and a single top level item which is a directory) + # expand archive works when -DestinationPath is not specified (and there are mutiple top level items) + + BeforeAll { + New-Item -Path "$TestDrive/file1.txt" -ItemType File + "Hello, World!" | Out-File -FilePath "$TestDrive/file1.txt" + Compress-Archive -Path "$TestDrive/file1.txt" -DestinationPath "$TestDrive/archive1.zip" + + New-Item -Path "$TestDrive/directory2" -ItemType Directory + + New-Item -Path "$TestDrive/DirectoryToArchive" -ItemType Directory + Compress-Archive -Path "$TestDrive/DirectoryToArchive" -DestinationPath "$TestDrive/archive2.zip" + } + + It "Expands an archive when a non-existant directory is specified as -DestinationPath" { + $sourcePath = "$TestDrive/archive1.zip" + $destinationPath = "$TestDrive/directory1" + + Expand-Archive -Path $sourcePath -DestinationPath $destinationPath + + $itemsInDestinationPath = Get-ChildItem $destinationPath -Name + $itemsInDestinationPath.Count | Should -Be 1 + $itemsInDestinationPath[0] | Should -Be "file1.txt" + } + + It "Expands an archive to the working directory when it is specified as -DestinationPath" { + $sourcePath = "$TestDrive/archive1.zip" + $destinationPath = "$TestDrive/directory2" + + Push-Location $destinationPath + + Expand-Archive -Path $sourcePath -DestinationPath $PWD + + $itemsInDestinationPath = Get-ChildItem $PWD -Name + $itemsInDestinationPath.Count | Should -Be 1 + $itemsInDestinationPath[0] | Should -Be "file1.txt" + + Pop-Location + } + + It "Expands an archive containing a single top-level directory and no other top-level items to a directory with that directory's name when -DestinationPath is not specified" { + $sourcePath = "$TestDrive/archive2.zip" + $destinationPath = "$TestDrive/directory2" + + Push-Location $destinationPath + + Expand-Archive -Path $sourcePath + + $itemsInDestinationPath = Get-ChildItem "$TestDrive/directory2" -Name -Recurse + $itemsInDestinationPath.Count | Should -Be 1 + $itemsInDestinationPath[0] | Should -Be "DirectoryToArchive" + + Test-Path -Path "$TestDrive/directory2/DirectoryToArchive" -PathType Container + + Pop-Location + } + } + + } \ No newline at end of file diff --git a/src/ArchiveCommandBase.cs b/src/ArchiveCommandBase.cs index 4dab683..1259d39 100644 --- a/src/ArchiveCommandBase.cs +++ b/src/ArchiveCommandBase.cs @@ -16,7 +16,7 @@ public class ArchiveCommandBase : PSCmdlet protected ArchiveFormat DetermineArchiveFormat(string destinationPath, ArchiveFormat? archiveFormat) { // 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 archiveFormatBasedOnExt); + bool ableToDetermineArchiveFormat = ArchiveFactory.TryGetArchiveFormatFromExtension(path: destinationPath, archiveFormat: out var archiveFormatBasedOnExt); // If the user did not specify which archive format to use, try to determine it automatically if (archiveFormat is null) { @@ -29,7 +29,7 @@ protected ArchiveFormat DetermineArchiveFormat(string destinationPath, ArchiveFo // 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); - archiveFormat = ArchiveFormat.zip; + archiveFormat = ArchiveFormat.Zip; } // Write a verbose message saying that Format is not specified and a format was determined automatically string verboseMessage = String.Format(Messages.ArchiveFormatDeterminedVerboseMessage, archiveFormat); diff --git a/src/ExpandArchiveCommand.cs b/src/ExpandArchiveCommand.cs index 0f1f64a..47c3dee 100644 --- a/src/ExpandArchiveCommand.cs +++ b/src/ExpandArchiveCommand.cs @@ -69,8 +69,8 @@ protected override void ProcessRecord() protected override void EndProcessing() { // Resolve Path or LiteralPath - bool checkForWildcards = ParameterSetName.StartsWith("Path"); - string path = ParameterSetName.StartsWith("Path") ? Path : LiteralPath; + bool checkForWildcards = ParameterSetName == "Path"; + string path = checkForWildcards ? Path : LiteralPath; System.IO.FileSystemInfo sourcePath = _pathHelper.ResolveToSingleFullyQualifiedPath(path: path, hasWildcards: checkForWildcards); ValidateSourcePath(sourcePath); @@ -79,7 +79,7 @@ protected override void EndProcessing() Format = DetermineArchiveFormat(destinationPath: sourcePath.FullName, archiveFormat: Format); // Get an archive from source path -- this is where we will switch between different types of archives - using IArchive? archive = ArchiveFactory.GetArchive(format: Format ?? ArchiveFormat.zip, archivePath: sourcePath.FullName, archiveMode: ArchiveMode.Extract, compressionLevel: System.IO.Compression.CompressionLevel.NoCompression); + using IArchive? archive = ArchiveFactory.GetArchive(format: Format ?? ArchiveFormat.Zip, archivePath: sourcePath.FullName, archiveMode: ArchiveMode.Extract, compressionLevel: System.IO.Compression.CompressionLevel.NoCompression); try { // If the destination path is a file that needs to be overwriten, delete it @@ -110,9 +110,10 @@ protected override void EndProcessing() } - } catch + } catch (System.UnauthorizedAccessException unauthorizedAccessException) { - + // TODO: Change this later to write an error + throw unauthorizedAccessException; } } @@ -128,6 +129,12 @@ private void ProcessArchiveEntry(IEntry entry) // The location of the entry post-expanding of the archive string postExpandPath = GetPostExpansionPath(entryName: entry.Name, destinationPath: _destinationPathInfo.FullName); + // If postExpandPath has a terminating `/`, remove it (there is case where overwriting a file may fail because of this) + if (postExpandPath.EndsWith(System.IO.Path.DirectorySeparatorChar)) + { + postExpandPath = postExpandPath.Remove(postExpandPath.Length - 1); + } + // If the entry name is invalid, write a non-terminating error and stop processing the entry if (IsPathInvalid(postExpandPath)) { @@ -157,6 +164,13 @@ private void ProcessArchiveEntry(IEntry entry) WriteError(errorRecord); return; } + // If postExpandPath is the same as the working directory, then write an error + if (hasCollision && postExpandPath == SessionState.Path.CurrentFileSystemLocation.ProviderPath) + { + var errorRecord = ErrorMessages.GetErrorRecord(ErrorCode.CannotOverwriteWorkingDirectory, postExpandPath); + WriteError(errorRecord); + return; + } postExpandPathInfo = directoryInfo; } @@ -180,7 +194,7 @@ private void ProcessArchiveEntry(IEntry entry) // There is a need to expand unless the entry is a directory and the postExpandPath is also a directory if (!(entry.IsDirectory && postExpandPathInfo.Attributes.HasFlag(FileAttributes.Directory))) { - entry.ExpandTo(_destinationPathInfo.FullName); + entry.ExpandTo(postExpandPath); } } } @@ -197,11 +211,7 @@ private void ValidateDestinationPath() // Check if DestinationPath is an existing directory else if (_destinationPathInfo.Attributes.HasFlag(FileAttributes.Directory)) { - // Throw an error if the DestinationPath is the current working directory and the cmdlet is in Overwrite mode - if (WriteMode == ExpandArchiveWriteMode.Overwrite && _destinationPathInfo.FullName == SessionState.Path.CurrentFileSystemLocation.ProviderPath) - { - errorCode = ErrorCode.CannotOverwriteWorkingDirectory; - } + // Do nothing } // If DestinationPath is an existing file else diff --git a/src/ZipArchive.cs b/src/ZipArchive.cs index 1089be0..73d3f51 100644 --- a/src/ZipArchive.cs +++ b/src/ZipArchive.cs @@ -161,9 +161,15 @@ internal class ZipArchiveEntry : IEntry void IEntry.ExpandTo(string destinationPath) { - string postExpandPath = System.IO.Path.Combine(destinationPath, _entry.FullName); - System.Diagnostics.Debug.Assert(!System.IO.File.Exists(postExpandPath)); - _entry.ExtractToFile(destinationPath); + // .NET APIs differentiate a file and directory by a terminating `/` + // If the entry name ends with `/`, it is a directory + if (_entry.FullName.EndsWith(System.IO.Path.AltDirectorySeparatorChar)) + { + System.IO.Directory.CreateDirectory(destinationPath); + } else + { + _entry.ExtractToFile(destinationPath); + } } internal ZipArchiveEntry(System.IO.Compression.ZipArchiveEntry entry) From 128c518ad55a95cc91accbd3f40e02418d07c594 Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Tue, 2 Aug 2022 14:13:13 -0700 Subject: [PATCH 07/34] added tests for basic functionality for Expand-Archive --- Tests/Compress-Archive.Tests.ps1 | 34 +++++++++- Tests/Expand-Archive.Tests.ps1 | 113 ++++++++++++++++++++++++++++--- src/ExpandArchiveCommand.cs | 23 ++++++- src/IArchive.cs | 6 ++ src/ZipArchive.cs | 29 +++++++- 5 files changed, 194 insertions(+), 11 deletions(-) diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index 8271248..0ec9a46 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -379,7 +379,7 @@ BeforeDiscovery { $destinationPath = "$TestDrive$($DS)archive11.zip" # Assert the last write time of the file is before 1980 - $dateProperty = Get-ItemProperty -Path $sourcePath -Name "LastWriteTime" + $dateProperty = Get-ItemPropertyValue -Path $sourcePath -Name "LastWriteTime" $dateProperty.Year | Should -BeLessThan 1980 Compress-Archive -Path $sourcePath -DestinationPath $destinationPath @@ -403,6 +403,38 @@ BeforeDiscovery { $entry.LastWriteTime.Millisecond | Should -BeExactly 0 + $archive.Dispose() + $archiveStream.Dispose() + } + + It "Compresses a directory whose last write time is before 1980" { + New-Item -Path "$TestDrive/olddirectory" -ItemType Directory + Set-ItemProperty -Path "$TestDrive/olddirectory" -Name "LastWriteTime" -Value '1974-01-16 14:44' + + $sourcePath = "$TestDrive$($DS)olddirectory" + $destinationPath = "$TestDrive$($DS)archive12.zip" + + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + $destinationPath | Should -Exist + Test-ZipArchive $destinationPath @('olddirectory/') + + # Get the archive + $fileMode = [System.IO.FileMode]::Open + $archiveStream = New-Object -TypeName System.IO.FileStream -ArgumentList $destinationPath,$fileMode + $zipArchiveMode = [System.IO.Compression.ZipArchiveMode]::Read + $archive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $archiveStream,$zipArchiveMode + $entry = $archive.GetEntry("olddirectory/") + $entry | Should -Not -BeNullOrEmpty + + $entry.LastWriteTime.Year | Should -BeExactly 1980 + $entry.LastWriteTime.Month| Should -BeExactly 1 + $entry.LastWriteTime.Day | Should -BeExactly 1 + $entry.LastWriteTime.Hour | Should -BeExactly 0 + $entry.LastWriteTime.Minute | Should -BeExactly 0 + $entry.LastWriteTime.Second | Should -BeExactly 0 + $entry.LastWriteTime.Millisecond | Should -BeExactly 0 + + $archive.Dispose() $archiveStream.Dispose() } diff --git a/Tests/Expand-Archive.Tests.ps1 b/Tests/Expand-Archive.Tests.ps1 index 2e4c41c..fecdbb0 100644 --- a/Tests/Expand-Archive.Tests.ps1 +++ b/Tests/Expand-Archive.Tests.ps1 @@ -404,20 +404,26 @@ Describe("Expand-Archive Tests") { Compress-Archive -Path "$TestDrive/file1.txt" -DestinationPath "$TestDrive/archive1.zip" New-Item -Path "$TestDrive/directory2" -ItemType Directory + New-Item -Path "$TestDrive/directory3" -ItemType Directory + New-Item -Path "$TestDrive/directory4" -ItemType Directory + New-Item -Path "$TestDrive/directory5" -ItemType Directory New-Item -Path "$TestDrive/DirectoryToArchive" -ItemType Directory Compress-Archive -Path "$TestDrive/DirectoryToArchive" -DestinationPath "$TestDrive/archive2.zip" + + # Create an archive containing a file and an empty folder + Compress-Archive -Path "$TestDrive/file1.txt","$TestDrive/DirectoryToArchive" -DestinationPath "$TestDrive/archive3.zip" } - It "Expands an archive when a non-existant directory is specified as -DestinationPath" { + It "Expands an archive when a non-existent directory is specified as -DestinationPath" { $sourcePath = "$TestDrive/archive1.zip" $destinationPath = "$TestDrive/directory1" Expand-Archive -Path $sourcePath -DestinationPath $destinationPath - $itemsInDestinationPath = Get-ChildItem $destinationPath -Name + $itemsInDestinationPath = Get-ChildItem $destinationPath -Recurse $itemsInDestinationPath.Count | Should -Be 1 - $itemsInDestinationPath[0] | Should -Be "file1.txt" + $itemsInDestinationPath[0].Name | Should -Be "file1.txt" } It "Expands an archive to the working directory when it is specified as -DestinationPath" { @@ -428,29 +434,120 @@ Describe("Expand-Archive Tests") { Expand-Archive -Path $sourcePath -DestinationPath $PWD - $itemsInDestinationPath = Get-ChildItem $PWD -Name + $itemsInDestinationPath = Get-ChildItem $PWD -Recurse $itemsInDestinationPath.Count | Should -Be 1 - $itemsInDestinationPath[0] | Should -Be "file1.txt" + $itemsInDestinationPath[0].Name | Should -Be "file1.txt" Pop-Location } It "Expands an archive containing a single top-level directory and no other top-level items to a directory with that directory's name when -DestinationPath is not specified" { $sourcePath = "$TestDrive/archive2.zip" - $destinationPath = "$TestDrive/directory2" + $destinationPath = "$TestDrive/directory3" Push-Location $destinationPath Expand-Archive -Path $sourcePath - $itemsInDestinationPath = Get-ChildItem "$TestDrive/directory2" -Name -Recurse + $itemsInDestinationPath = Get-ChildItem "$TestDrive/directory3" -Name -Recurse $itemsInDestinationPath.Count | Should -Be 1 $itemsInDestinationPath[0] | Should -Be "DirectoryToArchive" - Test-Path -Path "$TestDrive/directory2/DirectoryToArchive" -PathType Container + Test-Path -Path "$TestDrive/directory3/DirectoryToArchive" -PathType Container Pop-Location } + + It "Expands an archive containing multiple top-level items to a directory with that archive's name when -DestinationPath is not specified" { + $sourcePath = "$TestDrive/archive3.zip" + $destinationPath = "$TestDrive/directory4" + + Push-Location $destinationPath + + Expand-Archive -Path $sourcePath + + $itemsInDestinationPath = Get-ChildItem $destinationPath -Name -Recurse + $itemsInDestinationPath.Count | Should -Be 2 + $itemsInDestinationPath.Contains("DirectoryToArchive") | Should -Be $true + $itemsInDestinationPath.Contains("file1.txt") | Should -Be $true + + Pop-Location + } + + It "Expands an archive containing multiple files, non-empty directories, and empty directories" { + + # Create an archive containing multiple files, non-empty directories, and empty directories + New-Item -Path "$TestDrive/file2.txt" -ItemType File + "Hello, World!" | Out-File -FilePath "$TestDrive/file2.txt" + New-Item -Path "$TestDrive/file3.txt" -ItemType File + "Hello, World!" | Out-File -FilePath "$TestDrive/file3.txt" + + New-Item -Path "$TestDrive/emptydirectory1" -ItemType Directory + New-Item -Path "$TestDrive/emptydirectory2" -ItemType Directory + + New-Item -Path "$TestDrive/nonemptydirectory1" -ItemType Directory + New-Item -Path "$TestDrive/nonemptydirectory2" -ItemType Directory + + New-Item -Path "$TestDrive/nonemptydirectory1/subfile1.txt" -ItemType File + New-Item -Path "$TestDrive/nonemptydirectory2/subemptydirectory1" -ItemType Directory + + $archive4Paths = @("$TestDrive/file2.txt", "$TestDrive/file3.txt", "$TestDrive/emptydirectory1", "$TestDrive/emptydirectory2", "$TestDrive/nonemptydirectory1", "$TestDrive/nonemptydirectory2") + + Compress-Archive -Path $archive4Paths -DestinationPath "$TestDrive/archive4.zip" + + $sourcePath = "$TestDrive/archive4.zip" + $destinationPath = "$TestDrive/directory5" + + Expand-Archive -Path $sourcePath -DestinationPath $destinationPath + + $expandedItems = Get-ChildItem $destinationPath -Recurse -Name + + $itemsInArchive = @("file2.txt", "file3.txt", "emptydirectory1", "emptydirectory2", "nonemptydirectory1", "nonemptydirectory1/subfile1.txt", "nonemptydirectory2/subemptydirectory1") + + foreach ($item in $itemsInArchive) { + $item | Should -BeIn $expandedItems + } + } + + It "Expands an archive containing a file whose LastWriteTime is in the past" { + New-Item -Path "$TestDrive/oldfile.txt" -ItemType File + Set-ItemProperty -Path "$TestDrive/oldfile.txt" -Name "LastWriteTime" -Value '2003-01-16 14:44' + Compress-Archive -Path "$TestDrive/oldfile.txt" -DestinationPath "$TestDrive/archive_oldfile.zip" + + $sourcePath = "$TestDrive/archive_oldfile.zip" + $destinationPath = "$TestDrive/destination6" + Expand-Archive -Path $sourcePath -DestinationPath $destinationPath + + $lastWriteTime = Get-ItemPropertyValue -Path "$TestDrive/oldfile.txt" -Name "LastWriteTime" + + $lastWriteTime.Year | Should -Be 2003 + $lastWriteTime.Month | Should -Be 1 + $lastWriteTime.Day | Should -Be 16 + $lastWriteTime.Hour | Should -Be 14 + $lastWriteTime.Minute | Should -Be 44 + $lastWriteTime.Second | Should -Be 0 + $lastWriteTime.Millisecond | Should -Be 0 + } + + It "Expands an archive containing a directory whose LastWriteTime is in the past" { + New-Item -Path "$TestDrive/olddirectory" -ItemType Directory + Set-ItemProperty -Path "$TestDrive/olddirectory" -Name "LastWriteTime" -Value '2003-01-16 14:44' + Compress-Archive -Path "$TestDrive/olddirectory" -DestinationPath "$TestDrive/archive_olddirectory.zip" + + $sourcePath = "$TestDrive/archive_olddirectory.zip" + $destinationPath = "$TestDrive/destination_olddirectory" + Expand-Archive -Path $sourcePath -DestinationPath $destinationPath + + $lastWriteTime = Get-ItemPropertyValue -Path "$TestDrive/destination_olddirectory/olddirectory" -Name "LastWriteTime" + + $lastWriteTime.Year | Should -Be 2003 + $lastWriteTime.Month | Should -Be 1 + $lastWriteTime.Day | Should -Be 16 + $lastWriteTime.Hour | Should -Be 14 + $lastWriteTime.Minute | Should -Be 44 + $lastWriteTime.Second | Should -Be 0 + $lastWriteTime.Millisecond | Should -Be 0 + } } diff --git a/src/ExpandArchiveCommand.cs b/src/ExpandArchiveCommand.cs index 47c3dee..15371db 100644 --- a/src/ExpandArchiveCommand.cs +++ b/src/ExpandArchiveCommand.cs @@ -192,7 +192,7 @@ private void ProcessArchiveEntry(IEntry entry) } // Only expand the entry if there is a need to expand // There is a need to expand unless the entry is a directory and the postExpandPath is also a directory - if (!(entry.IsDirectory && postExpandPathInfo.Attributes.HasFlag(FileAttributes.Directory))) + if (!(entry.IsDirectory && postExpandPathInfo.Attributes.HasFlag(FileAttributes.Directory) && postExpandPathInfo.Exists)) { entry.ExpandTo(postExpandPath); } @@ -282,6 +282,27 @@ private bool IsPathInvalid(string path) } return false; } + + // Used to determine what the DestinationPath should be when it is not specified + private string DetermineDestinationPath(IArchive archive) + { + var workingDirectory = SessionState.Path.CurrentFileSystemLocation.ProviderPath; + if (archive.HasTopLevelDirectoryOnly()) + { + + } else + { + // destination path will be "working directory/archive file name" + var filename = System.IO.Path.GetFileName(archive.Path); + // If filename does not have an extension, throw a terminating error because the cmdlet + // cannot determine what destination path should be + if (System.IO.Path.GetExtension(filename) == String.Empty) + { + var errorRecord = + } + return System.IO.Path.Combine(workingDirectory, filename); + } + } #endregion } diff --git a/src/IArchive.cs b/src/IArchive.cs index 52d9aef..afba831 100644 --- a/src/IArchive.cs +++ b/src/IArchive.cs @@ -15,6 +15,9 @@ internal interface IArchive: IDisposable // Get the fully qualified path of the archive internal string Path { get; } + // Number of entries + internal int NumberOfEntries { 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. @@ -29,5 +32,8 @@ internal interface IArchive: IDisposable // Expands an archive to a destination folder. // Throws an exception if the archive is not in read mode. internal void Expand(string destinationPath); + + // Does the archive have only a top-level directory? + internal bool HasTopLevelDirectoryOnly(); } } diff --git a/src/ZipArchive.cs b/src/ZipArchive.cs index 73d3f51..9b96595 100644 --- a/src/ZipArchive.cs +++ b/src/ZipArchive.cs @@ -30,6 +30,8 @@ internal class ZipArchive : IArchive string IArchive.Path => _archivePath; + int IArchive.NumberOfEntries => _zipArchive.Entries.Count; + public ZipArchive(string archivePath, ArchiveMode mode, System.IO.FileStream archiveStream, CompressionLevel compressionLevel) { _disposedValue = false; @@ -71,7 +73,18 @@ void IArchive.AddFileSystemEntry(ArchiveAddition addition) entryName += ZipArchiveDirectoryPathTerminator; } - _zipArchive.CreateEntry(entryName); + entryInArchive = _zipArchive.CreateEntry(entryName); + + // Set the last write time + if (entryInArchive != null) + { + var lastWriteTime = addition.FileSystemInfo.LastWriteTime; + if (lastWriteTime.Year < 1980 || lastWriteTime.Year > 2107) + { + lastWriteTime = new DateTime(1980, 1, 1, 0, 0, 0); + } + entryInArchive.LastWriteTime = lastWriteTime; + } } } else @@ -149,6 +162,18 @@ public void Dispose() GC.SuppressFinalize(this); } + bool IArchive.HasTopLevelDirectoryOnly() + { + if (_zipArchive.Entries.Count == 0 || _zipArchive.Entries.Count > 1) + { + return false; + } + + // At this point, we know the archive has one entry only + // We can determine if the entry is a directory by checking if the entry name ends with '/' + return _zipArchive.Entries[0].FullName.EndsWith(ZipArchiveDirectoryPathTerminator); + } + internal class ZipArchiveEntry : IEntry { // Underlying object is System.IO.Compression.ZipArchiveEntry @@ -166,6 +191,8 @@ void IEntry.ExpandTo(string destinationPath) if (_entry.FullName.EndsWith(System.IO.Path.AltDirectorySeparatorChar)) { System.IO.Directory.CreateDirectory(destinationPath); + var lastWriteTime = _entry.LastWriteTime; + System.IO.Directory.SetLastWriteTime(destinationPath, lastWriteTime.DateTime); } else { _entry.ExtractToFile(destinationPath); From fda726353356f9d9fa8e273a156b7640b793d6a3 Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Tue, 2 Aug 2022 17:52:48 -0700 Subject: [PATCH 08/34] added support for automatically determining DestinationPath and worked on tests for Expand-Archive --- Tests/Expand-Archive.Tests.ps1 | 55 ++++++++++----- src/ErrorMessages.cs | 4 ++ src/ExpandArchiveCommand.cs | 94 +++++++++++++++---------- src/IArchive.cs | 2 +- src/Localized/Messages.resx | 9 +++ src/Microsoft.PowerShell.Archive.csproj | 8 --- src/TarArchive.cs | 7 ++ src/ZipArchive.cs | 36 +++++++--- 8 files changed, 140 insertions(+), 75 deletions(-) diff --git a/Tests/Expand-Archive.Tests.ps1 b/Tests/Expand-Archive.Tests.ps1 index fecdbb0..ccb5b32 100644 --- a/Tests/Expand-Archive.Tests.ps1 +++ b/Tests/Expand-Archive.Tests.ps1 @@ -287,17 +287,6 @@ Describe("Expand-Archive Tests") { } } - It "Does not throw an error when DestinationPath is an existing directory" { - $sourcePath = "$TestDrive$($DS)archive1.zip" - $destinationPath = "$TestDrive$($DS)directory1" - - try { - Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -ErrorAction Stop - } catch { - throw "An error was thrown but an error was not expected" - } - } - It "Does not throw an error when a directory in the archive has the same destination path as an existing directory" { $sourcePath = "$TestDrive$($DS)archive2.zip" $destinationPath = "$TestDrive" @@ -381,7 +370,7 @@ Describe("Expand-Archive Tests") { Get-Content -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir2/file1.txt" | Should -Be "Hello, World!" } - It "Overwrites a file whose path is the same as the destination path of a directory in the archive when -WriteMode Overwrite is specified" -Tag this1 { + It "Overwrites a file whose path is the same as the destination path of a directory in the archive when -WriteMode Overwrite is specified" { $sourcePath = "$TestDrive$($DS)archive2.zip" $destinationPath = "$TestDrive$($DS)ItemsToOverwriteContainer/subdir3" Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite -ErrorVariable error @@ -426,6 +415,17 @@ Describe("Expand-Archive Tests") { $itemsInDestinationPath[0].Name | Should -Be "file1.txt" } + It "Expands an archive when DestinationPath is an existing directory" { + $sourcePath = "$TestDrive$($DS)archive1.zip" + $destinationPath = "$TestDrive$($DS)directory1" + + try { + Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -ErrorAction Stop + } catch { + throw "An error was thrown but an error was not expected" + } + } + It "Expands an archive to the working directory when it is specified as -DestinationPath" { $sourcePath = "$TestDrive/archive1.zip" $destinationPath = "$TestDrive/directory2" @@ -441,7 +441,7 @@ Describe("Expand-Archive Tests") { Pop-Location } - It "Expands an archive containing a single top-level directory and no other top-level items to a directory with that directory's name when -DestinationPath is not specified" { + It "Expands an archive containing a single top-level directory and no other top-level items to a directory with that directory's name when -DestinationPath is not specified" -Tag this1{ $sourcePath = "$TestDrive/archive2.zip" $destinationPath = "$TestDrive/directory3" @@ -449,9 +449,9 @@ Describe("Expand-Archive Tests") { Expand-Archive -Path $sourcePath - $itemsInDestinationPath = Get-ChildItem "$TestDrive/directory3" -Name -Recurse + $itemsInDestinationPath = Get-ChildItem "$TestDrive/directory3" -Recurse $itemsInDestinationPath.Count | Should -Be 1 - $itemsInDestinationPath[0] | Should -Be "DirectoryToArchive" + $itemsInDestinationPath[0].Name | Should -Be "DirectoryToArchive" Test-Path -Path "$TestDrive/directory3/DirectoryToArchive" -PathType Container @@ -467,9 +467,11 @@ Describe("Expand-Archive Tests") { Expand-Archive -Path $sourcePath $itemsInDestinationPath = Get-ChildItem $destinationPath -Name -Recurse - $itemsInDestinationPath.Count | Should -Be 2 - $itemsInDestinationPath.Contains("DirectoryToArchive") | Should -Be $true - $itemsInDestinationPath.Contains("file1.txt") | Should -Be $true + $itemsInDestinationPath.Count | Should -Be 3 + "archive3" | Should -BeIn $itemsInDestinationPath + "archive3${DS}DirectoryToArchive" | Should -BeIn $itemsInDestinationPath + "archive3${DS}file1.txt" | Should -BeIn $itemsInDestinationPath + Pop-Location } @@ -502,8 +504,9 @@ Describe("Expand-Archive Tests") { $expandedItems = Get-ChildItem $destinationPath -Recurse -Name - $itemsInArchive = @("file2.txt", "file3.txt", "emptydirectory1", "emptydirectory2", "nonemptydirectory1", "nonemptydirectory1/subfile1.txt", "nonemptydirectory2/subemptydirectory1") + $itemsInArchive = @("file2.txt", "file3.txt", "emptydirectory1", "emptydirectory2", "nonemptydirectory1", "nonemptydirectory2", "nonemptydirectory1${DS}subfile1.txt", "nonemptydirectory2${DS}subemptydirectory1") + $expandedItems.Length | Should -Be $itemsInArchive.Count foreach ($item in $itemsInArchive) { $item | Should -BeIn $expandedItems } @@ -550,5 +553,19 @@ Describe("Expand-Archive Tests") { } } + Context "PassThru tests" { + BeforeAll { + New-Item -Path TestDrive:/file1.txt -ItemType File + "Hello, World!" | Out-File -Path Test:/file1.txt + $archivePath = "TestDrive:/archive.zip" + Compress-Archive -Path TestDrive:/file1.txt -DestinationPath + } + + It "Returns a System.IO.FileInfo object when PassThru is specified" { + $output = Expand-Archive -Path $archivePath -DestinationPath TestDrive:/archive_contents -PassThru + $output.GetType() + } + } + } \ No newline at end of file diff --git a/src/ErrorMessages.cs b/src/ErrorMessages.cs index 805b74d..810141e 100644 --- a/src/ErrorMessages.cs +++ b/src/ErrorMessages.cs @@ -40,6 +40,8 @@ internal static string GetErrorMessage(ErrorCode errorCode) ErrorCode.InsufficientPermissionsToAccessPath => Messages.InsufficientPermssionsToAccessPathMessage, ErrorCode.OverwriteDestinationPathFailed => Messages.OverwriteDestinationPathFailed, ErrorCode.CannotOverwriteWorkingDirectory => Messages.CannotOverwriteWorkingDirectoryMessage, + ErrorCode.PathResolvedToMultiplePaths => Messages.PathResolvedToMultiplePathsMessage, + ErrorCode.CannotDetermineDestinationPath => Messages.CannotDetermineDestinationPath, _ => throw new ArgumentOutOfRangeException(nameof(errorCode)) }; } @@ -76,5 +78,7 @@ internal enum ErrorCode CannotOverwriteWorkingDirectory, // Expand-Archive: used when a path resolved to multiple paths when only one was needed PathResolvedToMultiplePaths, + // Expand-Archive: used when the DestinationPath could not be determined + CannotDetermineDestinationPath } } diff --git a/src/ExpandArchiveCommand.cs b/src/ExpandArchiveCommand.cs index 15371db..4ddc1cd 100644 --- a/src/ExpandArchiveCommand.cs +++ b/src/ExpandArchiveCommand.cs @@ -1,8 +1,10 @@ using Microsoft.PowerShell.Archive.Localized; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.IO.Compression; +using System.IO.Enumeration; using System.Linq; using System.Management.Automation; using System.Text; @@ -22,9 +24,9 @@ public class ExpandArchiveCommand: ArchiveCommandBase [ValidateNotNullOrEmpty] public string LiteralPath { get; set; } = String.Empty; - [Parameter(Position = 2, Mandatory = true)] + [Parameter(Position = 2)] [ValidateNotNullOrEmpty] - public string DestinationPath { get; set; } = String.Empty; + public string? DestinationPath { get; set; } [Parameter] public ExpandArchiveWriteMode WriteMode { get; set; } = ExpandArchiveWriteMode.Expand; @@ -54,11 +56,7 @@ public ExpandArchiveCommand() protected override void BeginProcessing() { - // Resolve DestinationPath - _destinationPathInfo = _pathHelper.ResolveToSingleFullyQualifiedPath(path: DestinationPath, hasWildcards: false); - DestinationPath = _destinationPathInfo.FullName; - - ValidateDestinationPath(); + } protected override void ProcessRecord() @@ -78,12 +76,23 @@ protected override void EndProcessing() // Determine archive format based on sourcePath Format = DetermineArchiveFormat(destinationPath: sourcePath.FullName, archiveFormat: Format); - // Get an archive from source path -- this is where we will switch between different types of archives - using IArchive? archive = ArchiveFactory.GetArchive(format: Format ?? ArchiveFormat.Zip, archivePath: sourcePath.FullName, archiveMode: ArchiveMode.Extract, compressionLevel: System.IO.Compression.CompressionLevel.NoCompression); try { - // If the destination path is a file that needs to be overwriten, delete it + // Get an archive from source path -- this is where we will switch between different types of archives + using IArchive? archive = ArchiveFactory.GetArchive(format: Format ?? ArchiveFormat.Zip, archivePath: sourcePath.FullName, archiveMode: ArchiveMode.Extract, compressionLevel: System.IO.Compression.CompressionLevel.NoCompression); + + if (DestinationPath is null) + { + // If DestinationPath was not specified, try to determine it automatically based on the source path + // We should do this here because the destination path depends on whether the archive contains a single top-level directory or not + DestinationPath = DetermineDestinationPath(archive); + } + // Resolve DestinationPath and validate it + _destinationPathInfo = _pathHelper.ResolveToSingleFullyQualifiedPath(path: DestinationPath, hasWildcards: false); + DestinationPath = _destinationPathInfo.FullName; + ValidateDestinationPath(sourcePath); + // If the destination path is a file that needs to be overwriten, delete it if (_destinationPathInfo.Exists && !_destinationPathInfo.Attributes.HasFlag(FileAttributes.Directory) && WriteMode == ExpandArchiveWriteMode.Overwrite) { if (ShouldProcess(target: _destinationPathInfo.FullName, action: "Overwrite")) @@ -142,7 +151,6 @@ private void ProcessArchiveEntry(IEntry entry) WriteError(errorRecord); return; } - System.IO.FileSystemInfo postExpandPathInfo = new System.IO.FileInfo(postExpandPath); @@ -199,7 +207,8 @@ private void ProcessArchiveEntry(IEntry entry) } } - private void ValidateDestinationPath() + // TODO: Refactor this + private void ValidateDestinationPath(FileSystemInfo sourcePath) { ErrorCode? errorCode = null; @@ -229,6 +238,22 @@ private void ValidateDestinationPath() var errorRecord = ErrorMessages.GetErrorRecord(errorCode: errorCode.Value, errorItem: _destinationPathInfo.FullName); ThrowTerminatingError(errorRecord); } + + // Ensure sourcePath is not the same as the destination path when the cmdlet is in overwrite mode + // When the cmdlet is not in overwrite mode, other errors will be thrown when validating DestinationPath before it even gets to this line + if (PathHelper.ArePathsSame(sourcePath, _destinationPathInfo) && WriteMode == ExpandArchiveWriteMode.Overwrite) + { + if (ParameterSetName == "Path") + { + errorCode = ErrorCode.SamePathAndDestinationPath; + } + else + { + errorCode = ErrorCode.SameLiteralPathAndDestinationPath; + } + var errorRecord = ErrorMessages.GetErrorRecord(errorCode: errorCode.Value, errorItem: sourcePath.FullName); + ThrowTerminatingError(errorRecord); + } } private void ValidateSourcePath(System.IO.FileSystemInfo sourcePath) @@ -246,22 +271,6 @@ private void ValidateSourcePath(System.IO.FileSystemInfo sourcePath) var errorRecord = ErrorMessages.GetErrorRecord(errorCode: ErrorCode.DestinationExistsAsDirectory, errorItem: sourcePath.FullName); ThrowTerminatingError(errorRecord); } - - // Ensure sourcePath is not the same as the destination path when the cmdlet is in overwrite mode - // When the cmdlet is not in overwrite mode, other errors will be thrown when validating DestinationPath before it even gets to this line - if (PathHelper.ArePathsSame(sourcePath, _destinationPathInfo) && WriteMode == ExpandArchiveWriteMode.Overwrite) - { - ErrorCode errorCode; - if (ParameterSetName == "Path") - { - errorCode = ErrorCode.SamePathAndDestinationPath; - } else - { - errorCode = ErrorCode.SameLiteralPathAndDestinationPath; - } - var errorRecord = ErrorMessages.GetErrorRecord(errorCode: errorCode, errorItem: sourcePath.FullName); - ThrowTerminatingError(errorRecord); - } } private string GetPostExpansionPath(string entryName, string destinationPath) @@ -287,21 +296,32 @@ private bool IsPathInvalid(string path) private string DetermineDestinationPath(IArchive archive) { var workingDirectory = SessionState.Path.CurrentFileSystemLocation.ProviderPath; - if (archive.HasTopLevelDirectoryOnly()) + string? destinationDirectory = null; + + // If the archive has a single top-level directory only, the destination will be: "working directory" + // This makes it easier for the cmdlet to expand the directory without needing addition checks + if (archive.HasTopLevelDirectory()) { - - } else + destinationDirectory = workingDirectory; + } + // Otherwise, the destination path will be: "working directory/archive file name" + else { - // destination path will be "working directory/archive file name" var filename = System.IO.Path.GetFileName(archive.Path); - // If filename does not have an extension, throw a terminating error because the cmdlet - // cannot determine what destination path should be - if (System.IO.Path.GetExtension(filename) == String.Empty) + // If filename does have an exension, remove the extension and set the filename minus extension as destinationDirectory + if (System.IO.Path.GetExtension(filename) != string.Empty) { - var errorRecord = + destinationDirectory = System.IO.Path.ChangeExtension(path: filename, extension: string.Empty); } - return System.IO.Path.Combine(workingDirectory, filename); } + + if (destinationDirectory is null) + { + var errorRecord = ErrorMessages.GetErrorRecord(ErrorCode.CannotDetermineDestinationPath); + ThrowTerminatingError(errorRecord); + } + Debug.Assert(destinationDirectory is not null); + return System.IO.Path.Combine(workingDirectory, destinationDirectory); } #endregion diff --git a/src/IArchive.cs b/src/IArchive.cs index afba831..a5d33be 100644 --- a/src/IArchive.cs +++ b/src/IArchive.cs @@ -34,6 +34,6 @@ internal interface IArchive: IDisposable internal void Expand(string destinationPath); // Does the archive have only a top-level directory? - internal bool HasTopLevelDirectoryOnly(); + internal bool HasTopLevelDirectory(); } } diff --git a/src/Localized/Messages.resx b/src/Localized/Messages.resx index b2fe38f..b77c9e2 100644 --- a/src/Localized/Messages.resx +++ b/src/Localized/Messages.resx @@ -152,6 +152,9 @@ The destination {0} cannot be overwritten because it is a non-empty directory. + + Create + The path(s) {0} have been specified more than once. @@ -170,6 +173,9 @@ The path {0} could not be found. + + The path {0} refers to multiple paths, but only one was expected. Perhaps this occured because multiple paths matched the wildcard (if applicable). + {0}% complete @@ -182,4 +188,7 @@ A path supplied to -Path is the same as the path supplied to -DestinationPath. + + The destination path cannot be determined automatically. Please use the -DestinationPath parameter to enter a destination path. + \ No newline at end of file diff --git a/src/Microsoft.PowerShell.Archive.csproj b/src/Microsoft.PowerShell.Archive.csproj index b8bd133..f8bb9f9 100644 --- a/src/Microsoft.PowerShell.Archive.csproj +++ b/src/Microsoft.PowerShell.Archive.csproj @@ -25,14 +25,6 @@ - - - True - True - Messages.resx - - - ResXFileCodeGenerator diff --git a/src/TarArchive.cs b/src/TarArchive.cs index 5ae2e4d..070d6dd 100644 --- a/src/TarArchive.cs +++ b/src/TarArchive.cs @@ -25,6 +25,8 @@ internal class TarArchive : IArchive string IArchive.Path => _path; + int IArchive.NumberOfEntries => throw new NotImplementedException(); + public TarArchive(string path, ArchiveMode mode, FileStream fileStream) { _mode = mode; @@ -76,5 +78,10 @@ public void Dispose() Dispose(disposing: true); GC.SuppressFinalize(this); } + + bool IArchive.HasTopLevelDirectory() + { + throw new NotImplementedException(); + } } } diff --git a/src/ZipArchive.cs b/src/ZipArchive.cs index 9b96595..eae5539 100644 --- a/src/ZipArchive.cs +++ b/src/ZipArchive.cs @@ -3,8 +3,8 @@ using System; using System.Collections.Generic; +using System.IO; using System.IO.Compression; -using System.Text; namespace Microsoft.PowerShell.Archive { @@ -16,11 +16,11 @@ internal class ZipArchive : IArchive private readonly string _archivePath; - private readonly System.IO.FileStream _archiveStream; + private readonly FileStream _archiveStream; private readonly System.IO.Compression.ZipArchive _zipArchive; - private readonly System.IO.Compression.CompressionLevel _compressionLevel; + private readonly CompressionLevel _compressionLevel; private const char ZipArchiveDirectoryPathTerminator = '/'; @@ -32,7 +32,7 @@ internal class ZipArchive : IArchive int IArchive.NumberOfEntries => _zipArchive.Entries.Count; - public ZipArchive(string archivePath, ArchiveMode mode, System.IO.FileStream archiveStream, CompressionLevel compressionLevel) + public ZipArchive(string archivePath, ArchiveMode mode, FileStream archiveStream, CompressionLevel compressionLevel) { _disposedValue = false; _mode = mode; @@ -162,16 +162,26 @@ public void Dispose() GC.SuppressFinalize(this); } - bool IArchive.HasTopLevelDirectoryOnly() + bool IArchive.HasTopLevelDirectory() { - if (_zipArchive.Entries.Count == 0 || _zipArchive.Entries.Count > 1) + int topLevelDirectoriesCount = 0; + foreach (var entry in _zipArchive.Entries) { - return false; + if (entry.FullName.EndsWith(ZipArchiveDirectoryPathTerminator) && + entry.FullName.LastIndexOf(ZipArchiveDirectoryPathTerminator, entry.FullName.Length - 2) == -1) + { + topLevelDirectoriesCount++; + if (topLevelDirectoriesCount > 1) + { + break; + } + } else + { + return false; + } } - // At this point, we know the archive has one entry only - // We can determine if the entry is a directory by checking if the entry name ends with '/' - return _zipArchive.Entries[0].FullName.EndsWith(ZipArchiveDirectoryPathTerminator); + return topLevelDirectoriesCount == 1; } internal class ZipArchiveEntry : IEntry @@ -195,6 +205,12 @@ void IEntry.ExpandTo(string destinationPath) System.IO.Directory.SetLastWriteTime(destinationPath, lastWriteTime.DateTime); } else { + // If the parent directory does not exist, create it + string? parentDirectory = Path.GetDirectoryName(destinationPath); + if (parentDirectory is not null && !Directory.Exists(parentDirectory)) + { + Directory.CreateDirectory(parentDirectory); + } _entry.ExtractToFile(destinationPath); } } From e5237deabad0d4bc5c1fc434b426f42b6a1399eb Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Mon, 8 Aug 2022 18:01:01 -0700 Subject: [PATCH 09/34] added tests --- Tests/Compress-Archive.Tests.ps1 | 79 +++++++----- Tests/Expand-Archive.Tests.ps1 | 167 ++++++++++++++------------ src/CompressArchiveCommand.cs | 12 +- src/ExpandArchiveCommand.cs | 126 +++++++++---------- src/Localized/Messages.resx | 3 + src/Microsoft.PowerShell.Archive.psd1 | 2 +- src/PathHelper.cs | 82 +++++++++++-- 7 files changed, 283 insertions(+), 188 deletions(-) diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index 0ec9a46..9cbc7c1 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -281,8 +281,12 @@ BeforeDiscovery { New-Item $TestDrive$($DS)EmptyFile -Type File | Out-Null # Create a file whose last write time is before 1980 - $content | Out-File -FilePath $TestDrive$($DS)OldFile.txt - Set-ItemProperty -Path $TestDrive$($DS)OldFile.txt -Name LastWriteTime -Value '1974-01-16 14:44' + $content | Out-File -FilePath TestDrive:/OldFile.txt + Set-ItemProperty -Path TestDrive:/OldFile.txt -Name LastWriteTime -Value '1974-01-16 14:44' + + # Create a directory whose last write time is before 1980 + New-Item -Path "TestDrive:/olddirectory" -ItemType Directory + Set-ItemProperty -Path "TestDrive:/olddirectory" -Name "LastWriteTime" -Value '1974-01-16 14:44' } It "Compresses a single file" { @@ -408,11 +412,8 @@ BeforeDiscovery { } It "Compresses a directory whose last write time is before 1980" { - New-Item -Path "$TestDrive/olddirectory" -ItemType Directory - Set-ItemProperty -Path "$TestDrive/olddirectory" -Name "LastWriteTime" -Value '1974-01-16 14:44' - - $sourcePath = "$TestDrive$($DS)olddirectory" - $destinationPath = "$TestDrive$($DS)archive12.zip" + $sourcePath = "TestDrive:/olddirectory" + $destinationPath = "${TestDrive}/archive12.zip" Compress-Archive -Path $sourcePath -DestinationPath $destinationPath $destinationPath | Should -Exist @@ -438,6 +439,30 @@ BeforeDiscovery { $archive.Dispose() $archiveStream.Dispose() } + + It "Writes a warning when compressing a file whose last write time is before 1980" { + $sourcePath = "TestDrive:/OldFile.txt" + $destinationPath = "${TestDrive}/archive13.zip" + + # Assert the last write time of the file is before 1980 + $dateProperty = Get-ItemPropertyValue -Path $sourcePath -Name "LastWriteTime" + $dateProperty.Year | Should -BeLessThan 1980 + + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WarningVariable warnings + $warnings.Length | Should -Be 1 + } + + It "Writes a warning when compresing a directory whose last write time is before 1980" { + $sourcePath = "TestDrive:/olddirectory" + $destinationPath = "${TestDrive}/archive14.zip" + + # Assert the last write time of the file is before 1980 + $dateProperty = Get-ItemPropertyValue -Path $sourcePath -Name "LastWriteTime" + $dateProperty.Year | Should -BeLessThan 1980 + + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WarningVariable warnings + $warnings.Length | Should -Be 1 + } } Context "Update tests" -Skip { @@ -663,22 +688,12 @@ BeforeDiscovery { Context "Special and Wildcard Characters Tests" { BeforeAll { -<<<<<<< HEAD New-Item TestDrive:/SourceDir -Type Directory | Out-Null $content = "Some Data" $content | Out-File -FilePath TestDrive:/SourceDir/Sample-1.txt New-Item -LiteralPath "$TestDrive$($DS)Source[]Dir" -Type Directory | Out-Null $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)file1[].txt -======= - New-Item $TestDrive$($DS)SourceDir -Type Directory | Out-Null - - New-Item -Path "$TestDrive$($DS)Source`[`]Dir" -Type Directory | Out-Null - - $content = "Some Data" - $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)Sample-1.txt - $content | Out-File -LiteralPath $TestDrive$($DS)file1[].txt ->>>>>>> 8b3dcd5 (worked on Expand-Archive, added IEntry class, added support for ShouldProcess in Expand-Archive) } It "Accepts DestinationPath parameter with wildcard characters that resolves to one path" { @@ -692,16 +707,9 @@ BeforeDiscovery { It "Accepts DestinationPath parameter with [ but no matching ]" { $sourcePath = "TestDrive:/SourceDir" $destinationPath = "TestDrive:/archive[2.zip" - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -<<<<<<< HEAD $destinationPath | Should -BeZipArchiveOnlyContaining @("SourceDir/", "SourceDir/Sample-1.txt") -LiteralPath Remove-Item -LiteralPath $destinationPath -======= - Test-Path -LiteralPath $destinationPath | Should -Be $true - Test-ZipArchive $destinationPath @("SourceDir/", "SourceDir/Sample-1.txt") - Remove-Item -LiteralPath $destinationPath -Force ->>>>>>> 8b3dcd5 (worked on Expand-Archive, added IEntry class, added support for ShouldProcess in Expand-Archive) } It "Accepts LiteralPath parameter for a directory with special characters in the directory name" -skip:(($PSVersionTable.psversion.Major -lt 5) -and ($PSVersionTable.psversion.Minor -lt 0)) { @@ -734,15 +742,26 @@ BeforeDiscovery { } } - Context "test" -Tag lol { + Context "PassThru tests" { BeforeAll { - $content = "Some Data" - $content | Out-File -FilePath TestDrive:/Sample-1.txt - Compress-Archive -Path TestDrive:/Sample-1.txt -DestinationPath TestDrive:/archive1.zip + New-Item -Path TestDrive:/file.txt -ItemType File + } + + It "Returns an object of type System.IO.FileInfo when PassThru is specified" { + $output = Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive1.zip -PassThru + $output | Should -BeOfType System.IO.FileInfo + $destinationPath = Join-Path $TestDrive "archive1.zip" + $output.FullName | Should -Be $destinationPath + } + + It "Does not return an object when PassThru is not specified" { + $output = Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive2.zip + $output | Should -BeNullOrEmpty } - It "test custom assetion" { - "${TestDrive}/archive1.zip" | Should -BeZipArchiveOnlyContaining @("Sample-1.txt") + It "Does not return an object when PassThru is false" { + $output = Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive3.zip -PassThru:$false + $output | Should -BeNullOrEmpty } } } diff --git a/Tests/Expand-Archive.Tests.ps1 b/Tests/Expand-Archive.Tests.ps1 index ccb5b32..bd6ea25 100644 --- a/Tests/Expand-Archive.Tests.ps1 +++ b/Tests/Expand-Archive.Tests.ps1 @@ -2,20 +2,7 @@ Describe("Expand-Archive Tests") { BeforeAll { - 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 - } - } $CmdletClassName = "Microsoft.PowerShell.Archive.ExpandArchiveCommand" - $DS = [System.IO.Path]::DirectorySeparatorChar - Add-CompressionAssemblies # Progress perference $originalProgressPref = $ProgressPreference @@ -65,19 +52,19 @@ Describe("Expand-Archive Tests") { } # Set up files for tests - 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)Sample-1.txt + $content | Out-File -FilePath $TestDrive/Sample-1.txt # Create archives called archive1.zip and archive2.zip - Compress-Archive -Path $TestDrive$($DS)Sample-1.txt -DestinationPath $TestDrive$($DS)archive1.zip - Compress-Archive -Path $TestDrive$($DS)Sample-1.txt -DestinationPath $TestDrive$($DS)archive2.zip + Compress-Archive -Path $TestDrive/Sample-1.txt -DestinationPath $TestDrive/archive1.zip + Compress-Archive -Path $TestDrive/Sample-1.txt -DestinationPath $TestDrive/archive2.zip } It "Validate errors with NULL & EMPTY values for Path, LiteralPath, and DestinationPath" { - $sourcePath = "$TestDrive$($DS)SourceDir" - $destinationPath = "$TestDrive$($DS)SampleSingleFile.zip" + $sourcePath = "$TestDrive/SourceDir" + $destinationPath = "$TestDrive/SampleSingleFile.zip" ExpandArchivePathParameterSetValidator $null $destinationPath ExpandArchivePathParameterSetValidator $sourcePath $null @@ -96,8 +83,8 @@ Describe("Expand-Archive Tests") { ExpandArchiveLiteralPathParameterSetValidator "" "" } - It "Throws when invalid path non-existing path is supplied for Path or LiteralPath parameters" { - $path = "$TestDrive$($DS)non-existant.zip" + It "Throws when non-existing path is supplied for Path or LiteralPath parameters" { + $path = "$TestDrive/non-existant.zip" $destinationPath = "$TestDrive($DS)DestinationFolder" try { @@ -146,9 +133,9 @@ Describe("Expand-Archive Tests") { It "Throws an error when multiple paths are supplied as input to Path parameter" { $sourcePath = @( - "$TestDrive$($DS)SourceDir$($DS)archive1.zip", - "$TestDrive$($DS)SourceDir$($DS)archive2.zip") - $destinationPath = "$TestDrive$($DS)DestinationFolder" + "$TestDrive/SourceDir/archive1.zip", + "$TestDrive/SourceDir/archive2.zip") + $destinationPath = "$TestDrive/DestinationFolder" try { @@ -163,9 +150,9 @@ Describe("Expand-Archive Tests") { It "Throws an error when multiple paths are supplied as input to LiteralPath parameter" { $sourcePath = @( - "$TestDrive$($DS)SourceDir$($DS)archive1.zip", - "$TestDrive$($DS)SourceDir$($DS)archive2.zip") - $destinationPath = "$TestDrive$($DS)DestinationFolder" + "$TestDrive/SourceDir/archive1.zip", + "$TestDrive/SourceDir/archive2.zip") + $destinationPath = "$TestDrive/DestinationFolder" try { @@ -180,10 +167,10 @@ Describe("Expand-Archive Tests") { ## 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 @@ -196,7 +183,7 @@ Describe("Expand-Archive Tests") { } It "Throws an error when Path and DestinationPath are the same and -WriteMode Overwrite is specified" { - $sourcePath = "$TestDrive$($DS)archive1.zip" + $sourcePath = "$TestDrive/archive1.zip" $destinationPath = $sourcePath try { @@ -208,7 +195,7 @@ Describe("Expand-Archive Tests") { } It "Throws an error when LiteralPath and DestinationPath are the same and WriteMode -Overwrite is specified" { - $sourcePath = "$TestDrive$($DS)archive1.zip" + $sourcePath = "$TestDrive/archive1.zip" $destinationPath = $sourcePath try { @@ -218,6 +205,18 @@ Describe("Expand-Archive Tests") { $_.FullyQualifiedErrorId | Should -Be "SameLiteralPathAndDestinationPath,$CmdletClassName" } } + + It "Throws an error when an invalid path is supplied to DestinationPath" { + $sourcePath = "$TestDrive/archive1.zip" + $destinationPath = "Variable:/PWD" + + try { + Expand-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath + throw "Failed to detect an error when an invalid path is supplied to DestinationPath" + } catch { + $_.FullyQualifiedErrorId | Should -Be "InvalidPath,$CmdletClassName" + } + } } Context "DestinationPath and Overwrite Tests" { @@ -240,32 +239,32 @@ Describe("Expand-Archive Tests") { # last write times BeforeAll { - New-Item -Path "$TestDrive$($DS)file1.txt" -ItemType File - "Hello, World!" | Out-File -FilePath "$TestDrive$($DS)file1.txt" - Compress-Archive -Path "$TestDrive$($DS)file1.txt" -DestinationPath "$TestDrive$($DS)archive1.zip" + New-Item -Path "$TestDrive/file1.txt" -ItemType File + "Hello, World!" | Out-File -FilePath "$TestDrive/file1.txt" + Compress-Archive -Path "$TestDrive/file1.txt" -DestinationPath "$TestDrive/archive1.zip" - New-Item -Path "$TestDrive$($DS)directory1" -ItemType Directory + New-Item -Path "$TestDrive/directory1" -ItemType Directory # Create archive2.zip containing directory1 - Compress-Archive -Path "$TestDrive$($DS)directory1" -DestinationPath "$TestDrive$($DS)archive2.zip" + Compress-Archive -Path "$TestDrive/directory1" -DestinationPath "$TestDrive/archive2.zip" - New-Item -Path "$TestDrive$($DS)ParentDir" -ItemType Directory - New-Item -Path "$TestDrive$($DS)ParentDir/file1.txt" -ItemType Directory + New-Item -Path "$TestDrive/ParentDir" -ItemType Directory + New-Item -Path "$TestDrive/ParentDir/file1.txt" -ItemType Directory # Create a dir that is a container for items to be overwritten - New-Item -Path "$TestDrive$($DS)ItemsToOverwriteContainer" -ItemType Directory - New-Item -Path "$TestDrive$($DS)ItemsToOverwriteContainer/file2" -ItemType File - New-Item -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir1" -ItemType Directory - New-Item -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir1/file1.txt" -ItemType File - New-Item -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir2" -ItemType Directory - New-Item -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir2/file1.txt" -ItemType Directory - New-Item -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir4" -ItemType Directory - New-Item -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir4/file1.txt" -ItemType Directory - New-Item -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir4/file1.txt/somefile" -ItemType File + New-Item -Path "$TestDrive/ItemsToOverwriteContainer" -ItemType Directory + New-Item -Path "$TestDrive/ItemsToOverwriteContainer/file2" -ItemType File + New-Item -Path "$TestDrive/ItemsToOverwriteContainer/subdir1" -ItemType Directory + New-Item -Path "$TestDrive/ItemsToOverwriteContainer/subdir1/file1.txt" -ItemType File + New-Item -Path "$TestDrive/ItemsToOverwriteContainer/subdir2" -ItemType Directory + New-Item -Path "$TestDrive/ItemsToOverwriteContainer/subdir2/file1.txt" -ItemType Directory + New-Item -Path "$TestDrive/ItemsToOverwriteContainer/subdir4" -ItemType Directory + New-Item -Path "$TestDrive/ItemsToOverwriteContainer/subdir4/file1.txt" -ItemType Directory + New-Item -Path "$TestDrive/ItemsToOverwriteContainer/subdir4/file1.txt/somefile" -ItemType File # Create directory to override - New-Item -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir3" -ItemType Directory - New-Item -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir3/directory1" -ItemType File + New-Item -Path "$TestDrive/ItemsToOverwriteContainer/subdir3" -ItemType Directory + New-Item -Path "$TestDrive/ItemsToOverwriteContainer/subdir3/directory1" -ItemType File # Set the error action preference so non-terminating errors aren't displayed $ErrorActionPreference = 'SilentlyContinue' @@ -277,8 +276,8 @@ Describe("Expand-Archive Tests") { } It "Throws an error when DestinationPath is an existing file" { - $sourcePath = "$TestDrive$($DS)archive1.zip" - $destinationPath = "$TestDrive$($DS)file1.txt" + $sourcePath = "$TestDrive/archive1.zip" + $destinationPath = "$TestDrive/file1.txt" try { Expand-Archive -Path $sourcePath -DestinationPath $destinationPath @@ -288,7 +287,7 @@ Describe("Expand-Archive Tests") { } It "Does not throw an error when a directory in the archive has the same destination path as an existing directory" { - $sourcePath = "$TestDrive$($DS)archive2.zip" + $sourcePath = "$TestDrive/archive2.zip" $destinationPath = "$TestDrive" try { @@ -299,7 +298,7 @@ Describe("Expand-Archive Tests") { } It "Writes a non-terminating error when a file in the archive has a destination path that already exists" { - $sourcePath = "$TestDrive$($DS)archive1.zip" + $sourcePath = "$TestDrive/archive1.zip" $destinationPath = "$TestDrive" Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -ErrorVariable error @@ -308,8 +307,8 @@ Describe("Expand-Archive Tests") { } It "Writes a non-terminating error when a file in the archive has a destination path that is an existing directory containing at least 1 item and -WriteMode Overwrite is specified" { - $sourcePath = "$TestDrive$($DS)archive1.zip" - $destinationPath = "$TestDrive$($DS)ItemsToOverwriteContainer/subdir4" + $sourcePath = "$TestDrive/archive1.zip" + $destinationPath = "$TestDrive/ItemsToOverwriteContainer/subdir4" Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite -ErrorVariable error $error.Count | Should -Be 1 @@ -317,8 +316,8 @@ Describe("Expand-Archive Tests") { } It "Writes a non-terminating error when a file in the archive has a destination path that is the working directory and -WriteMode Overwrite is specified" { - $sourcePath = "$TestDrive$($DS)archive1.zip" - $destinationPath = "$TestDrive$($DS)ParentDir" + $sourcePath = "$TestDrive/archive1.zip" + $destinationPath = "$TestDrive/ParentDir" Push-Location "$destinationPath/file1.txt" @@ -332,52 +331,52 @@ Describe("Expand-Archive Tests") { } It "Overwrites a file when it is DestinationPath and -WriteMode Overwrite is specified" { - $sourcePath = "$TestDrive$($DS)archive1.zip" - $destinationPath = "$TestDrive$($DS)ItemsToOverwriteContainer/file2" + $sourcePath = "$TestDrive/archive1.zip" + $destinationPath = "$TestDrive/ItemsToOverwriteContainer/file2" Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite -ErrorVariable error $error.Count | Should -Be 0 # Ensure the file in archive1.zip was expanded - Test-Path "$TestDrive$($DS)ItemsToOverwriteContainer/file2/file1.txt" -PathType Leaf + Test-Path "$TestDrive/ItemsToOverwriteContainer/file2/file1.txt" -PathType Leaf } It "Overwrites a file whose path is the same as the destination path of a file in the archive when -WriteMode Overwrite is specified" -Tag td { - $sourcePath = "$TestDrive$($DS)archive1.zip" - $destinationPath = "$TestDrive$($DS)ItemsToOverwriteContainer/subdir1" + $sourcePath = "$TestDrive/archive1.zip" + $destinationPath = "$TestDrive/ItemsToOverwriteContainer/subdir1" Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite -ErrorVariable error $error.Count | Should -Be 0 # Ensure the file in archive1.zip was expanded - Test-Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir1/file1.txt" -PathType Leaf + Test-Path "$TestDrive/ItemsToOverwriteContainer/subdir1/file1.txt" -PathType Leaf # Ensure the contents of file1.txt is "Hello, World!" - Get-Content -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir1/file1.txt" | Should -Be "Hello, World!" + Get-Content -Path "$TestDrive/ItemsToOverwriteContainer/subdir1/file1.txt" | Should -Be "Hello, World!" } It "Overwrites a directory whose path is the same as the destination path of a file in the archive when -WriteMode Overwrite is specified" { - $sourcePath = "$TestDrive$($DS)archive1.zip" - $destinationPath = "$TestDrive$($DS)ItemsToOverwriteContainer/subdir2" + $sourcePath = "$TestDrive/archive1.zip" + $destinationPath = "$TestDrive/ItemsToOverwriteContainer/subdir2" Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite -ErrorVariable error $error.Count | Should -Be 0 # Ensure the file in archive1.zip was expanded - Test-Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir2/file1.txt" -PathType Leaf + Test-Path "$TestDrive/ItemsToOverwriteContainer/subdir2/file1.txt" -PathType Leaf # Ensure the contents of file1.txt is "Hello, World!" - Get-Content -Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir2/file1.txt" | Should -Be "Hello, World!" + Get-Content -Path "$TestDrive/ItemsToOverwriteContainer/subdir2/file1.txt" | Should -Be "Hello, World!" } It "Overwrites a file whose path is the same as the destination path of a directory in the archive when -WriteMode Overwrite is specified" { - $sourcePath = "$TestDrive$($DS)archive2.zip" - $destinationPath = "$TestDrive$($DS)ItemsToOverwriteContainer/subdir3" + $sourcePath = "$TestDrive/archive2.zip" + $destinationPath = "$TestDrive/ItemsToOverwriteContainer/subdir3" Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite -ErrorVariable error $error.Count | Should -Be 0 # Ensure the file in archive1.zip was expanded - Test-Path "$TestDrive$($DS)ItemsToOverwriteContainer/subdir3/directory1" -PathType Container + Test-Path "$TestDrive/ItemsToOverwriteContainer/subdir3/directory1" -PathType Container } } @@ -416,8 +415,8 @@ Describe("Expand-Archive Tests") { } It "Expands an archive when DestinationPath is an existing directory" { - $sourcePath = "$TestDrive$($DS)archive1.zip" - $destinationPath = "$TestDrive$($DS)directory1" + $sourcePath = "$TestDrive/archive1.zip" + $destinationPath = "$TestDrive/directory1" try { Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -ErrorAction Stop @@ -558,12 +557,24 @@ Describe("Expand-Archive Tests") { New-Item -Path TestDrive:/file1.txt -ItemType File "Hello, World!" | Out-File -Path Test:/file1.txt $archivePath = "TestDrive:/archive.zip" - Compress-Archive -Path TestDrive:/file1.txt -DestinationPath + Compress-Archive -Path TestDrive:/file1.txt -DestinationPath $archivePath + } + + It "Returns a System.IO.DirectoryInfo object when PassThru is specified" { + $destinationPath = "{TestDrive}/archive_contents" + $output = Expand-Archive -Path $archivePath -DestinationPath $destinationPath -PassThru + $output | Should -BeOfType System.IO.DirectoryInfo + $output.FullName | SHould -Be $destinationPath + } + + It "Does not return an object when PassThru is not specified" { + $output = Expand-Archive -Path $archivePath -DestinationPath TestDrive:/archive_contents2 + $output | Should -BeNullOrEmpty } - It "Returns a System.IO.FileInfo object when PassThru is specified" { - $output = Expand-Archive -Path $archivePath -DestinationPath TestDrive:/archive_contents -PassThru - $output.GetType() + It "Does not return an object when PassThru is false" { + $output = Compress-Archive -Path $archivePath -DestinationPath TestDrive:/archive_contents -PassThru:$false + $output | Should -BeNullOrEmpty } } diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index cadecd0..e76aea5 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -94,7 +94,7 @@ public CompressArchiveCommand() protected override void BeginProcessing() { // This resolves the path to a fully qualified path and handles provider exceptions - DestinationPath = _pathHelper.GetUnresolvedPathFromPSProviderPath(DestinationPath); + DestinationPath = _pathHelper.GetUnresolvedPathFromPSProviderPath(path: DestinationPath, pathMustExist: false); ValidateDestinationPath(); } @@ -104,7 +104,7 @@ protected override void ProcessRecord() { Debug.Assert(Path is not null); foreach (var path in Path) { - var resolvedPaths = _pathHelper.GetResolvedPathFromPSProviderPath(path, _nonexistentPaths); + var resolvedPaths = _pathHelper.GetResolvedPathFromPSProviderPathWhileCapturingNonexistentPaths(path, _nonexistentPaths); if (resolvedPaths is not null) { foreach (var resolvedPath in resolvedPaths) { // Add resolvedPath to _path @@ -118,7 +118,7 @@ protected override void ProcessRecord() { Debug.Assert(LiteralPath is not null); foreach (var path in LiteralPath) { - var unresolvedPath = _pathHelper.GetUnresolvedPathFromPSProviderPath(path, _nonexistentPaths); + var unresolvedPath = _pathHelper.GetUnresolvedPathFromPSProviderPathWhileCapturingNonexistentPaths(path, _nonexistentPaths); if (unresolvedPath is not null) { // Add unresolvedPath to _path AddPathToPaths(pathToAdd: unresolvedPath); @@ -155,6 +155,7 @@ protected override void EndProcessing() // Get archive entries // If a path causes an exception (e.g., SecurityException), _pathHelper should handle it + Debug.Assert(_paths is not null); List archiveAdditions = _pathHelper.GetArchiveAdditions(_paths); // Remove references to _paths, Path, and LiteralPath to free up memory @@ -203,6 +204,11 @@ protected override void EndProcessing() if (ShouldProcess(target: entry.FileSystemInfo.FullName, action: Messages.Add)) { + // Warn the user if the LastWriteTime of the file/directory is before 1980 + if (entry.FileSystemInfo.LastWriteTime.Year < 1980 && Format == ArchiveFormat.Zip) { + WriteWarning(string.Format(Messages.LastWriteTimeBefore1980Warning, entry.FileSystemInfo.FullName)); + } + archive?.AddFileSystemEntry(entry); // Write a verbose message saying this item was added to the archive var addedItemMessage = string.Format(Messages.AddedItemToArchiveVerboseMessage, entry.FileSystemInfo.FullName); diff --git a/src/ExpandArchiveCommand.cs b/src/ExpandArchiveCommand.cs index 4ddc1cd..3b6a0e5 100644 --- a/src/ExpandArchiveCommand.cs +++ b/src/ExpandArchiveCommand.cs @@ -16,11 +16,16 @@ namespace Microsoft.PowerShell.Archive [OutputType(typeof(System.IO.FileSystemInfo))] public class ExpandArchiveCommand: ArchiveCommandBase { - [Parameter(Position=0, Mandatory = true, ParameterSetName = "Path", ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + private enum ParameterSet { + Path, + LiteralPath + } + + [Parameter(Position=0, Mandatory = true, ParameterSetName = nameof(ParameterSet.Path), ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] public string Path { get; set; } = String.Empty; - [Parameter(Mandatory = true, ParameterSetName = "LiteralPath", ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [Parameter(Mandatory = true, ParameterSetName = nameof(ParameterSet.LiteralPath))] [ValidateNotNullOrEmpty] public string LiteralPath { get; set; } = String.Empty; @@ -45,6 +50,8 @@ public class ExpandArchiveCommand: ArchiveCommandBase private bool _didCreateOutput; + private string? _sourcePath; + #endregion public ExpandArchiveCommand() @@ -67,47 +74,45 @@ protected override void ProcessRecord() protected override void EndProcessing() { // Resolve Path or LiteralPath - bool checkForWildcards = ParameterSetName == "Path"; + bool checkForWildcards = ParameterSetName == nameof(ParameterSet.Path); string path = checkForWildcards ? Path : LiteralPath; - System.IO.FileSystemInfo sourcePath = _pathHelper.ResolveToSingleFullyQualifiedPath(path: path, hasWildcards: checkForWildcards); - - ValidateSourcePath(sourcePath); + ValidateSourcePath(path); + Debug.Assert(_sourcePath is not null); // Determine archive format based on sourcePath - Format = DetermineArchiveFormat(destinationPath: sourcePath.FullName, archiveFormat: Format); + Format = DetermineArchiveFormat(destinationPath: _sourcePath, archiveFormat: Format); try { // Get an archive from source path -- this is where we will switch between different types of archives - using IArchive? archive = ArchiveFactory.GetArchive(format: Format ?? ArchiveFormat.Zip, archivePath: sourcePath.FullName, archiveMode: ArchiveMode.Extract, compressionLevel: System.IO.Compression.CompressionLevel.NoCompression); + using IArchive? archive = ArchiveFactory.GetArchive(format: Format ?? ArchiveFormat.Zip, archivePath: _sourcePath, archiveMode: ArchiveMode.Extract, compressionLevel: System.IO.Compression.CompressionLevel.NoCompression); if (DestinationPath is null) { // If DestinationPath was not specified, try to determine it automatically based on the source path // We should do this here because the destination path depends on whether the archive contains a single top-level directory or not DestinationPath = DetermineDestinationPath(archive); + } else { + // Resolve DestinationPath and validate it + DestinationPath = _pathHelper.GetUnresolvedPathFromPSProviderPath(path: DestinationPath, pathMustExist: true); } - // Resolve DestinationPath and validate it - _destinationPathInfo = _pathHelper.ResolveToSingleFullyQualifiedPath(path: DestinationPath, hasWildcards: false); - DestinationPath = _destinationPathInfo.FullName; - ValidateDestinationPath(sourcePath); + ValidateDestinationPath(); + Debug.Assert(DestinationPath is not null); // If the destination path is a file that needs to be overwriten, delete it - if (_destinationPathInfo.Exists && !_destinationPathInfo.Attributes.HasFlag(FileAttributes.Directory) && WriteMode == ExpandArchiveWriteMode.Overwrite) + if (File.Exists(DestinationPath) && WriteMode == ExpandArchiveWriteMode.Overwrite) { - if (ShouldProcess(target: _destinationPathInfo.FullName, action: "Overwrite")) + if (ShouldProcess(target: DestinationPath, action: "Overwrite")) { - _destinationPathInfo.Delete(); - System.IO.Directory.CreateDirectory(_destinationPathInfo.FullName); - _destinationPathInfo = new System.IO.DirectoryInfo(_destinationPathInfo.FullName); + File.Delete(DestinationPath); + System.IO.Directory.CreateDirectory(DestinationPath); } } // If the destination path does not exist, create it - if (!_destinationPathInfo.Exists && ShouldProcess(target: _destinationPathInfo.FullName, action: "Create")) + if (!Directory.Exists(DestinationPath) && ShouldProcess(target: DestinationPath, action: "Create")) { - System.IO.Directory.CreateDirectory(_destinationPathInfo.FullName); - _destinationPathInfo = new System.IO.DirectoryInfo(_destinationPathInfo.FullName); + System.IO.Directory.CreateDirectory(DestinationPath); } // Get the next entry in the archive and process it @@ -135,6 +140,8 @@ protected override void StopProcessing() private void ProcessArchiveEntry(IEntry entry) { + Debug.Assert(DestinationPath is not null); + // The location of the entry post-expanding of the archive string postExpandPath = GetPostExpansionPath(entryName: entry.Name, destinationPath: _destinationPathInfo.FullName); @@ -196,7 +203,6 @@ private void ProcessArchiveEntry(IEntry entry) if (hasCollision) { postExpandPathInfo.Delete(); - System.Threading.Thread.Sleep(1000); } // Only expand the entry if there is a need to expand // There is a need to expand unless the entry is a directory and the postExpandPath is also a directory @@ -207,68 +213,54 @@ private void ProcessArchiveEntry(IEntry entry) } } - // TODO: Refactor this - private void ValidateDestinationPath(FileSystemInfo sourcePath) + private void ValidateDestinationPath() { - ErrorCode? errorCode = null; - - // In this case, DestinationPath does not exist - if (!_destinationPathInfo.Exists) - { - // Do nothing - } - // Check if DestinationPath is an existing directory - else if (_destinationPathInfo.Attributes.HasFlag(FileAttributes.Directory)) - { - // Do nothing - } - // If DestinationPath is an existing file - else - { - // Throw an error if DestinationPath exists and the cmdlet is not in Overwrite mode - if (WriteMode == ExpandArchiveWriteMode.Expand) - { - errorCode = ErrorCode.DestinationExists; - } - } + Debug.Assert(DestinationPath is not null); - if (errorCode != null) - { - // Throw an error -- since we are validating DestinationPath, the problem is with DestinationPath - var errorRecord = ErrorMessages.GetErrorRecord(errorCode: errorCode.Value, errorItem: _destinationPathInfo.FullName); + // Throw an error if DestinationPath exists and the cmdlet is not in Overwrite mode + if (File.Exists(DestinationPath) && WriteMode == ExpandArchiveWriteMode.Expand) { + var errorRecord = ErrorMessages.GetErrorRecord(errorCode: ErrorCode.CannotDetermineDestinationPath, errorItem: DestinationPath); ThrowTerminatingError(errorRecord); } // Ensure sourcePath is not the same as the destination path when the cmdlet is in overwrite mode // When the cmdlet is not in overwrite mode, other errors will be thrown when validating DestinationPath before it even gets to this line - if (PathHelper.ArePathsSame(sourcePath, _destinationPathInfo) && WriteMode == ExpandArchiveWriteMode.Overwrite) + if (_sourcePath == DestinationPath && WriteMode == ExpandArchiveWriteMode.Overwrite) { - if (ParameterSetName == "Path") - { - errorCode = ErrorCode.SamePathAndDestinationPath; - } - else - { - errorCode = ErrorCode.SameLiteralPathAndDestinationPath; - } - var errorRecord = ErrorMessages.GetErrorRecord(errorCode: errorCode.Value, errorItem: sourcePath.FullName); + ErrorCode errorCode = (ParameterSetName == nameof(ParameterSet.Path)) ? ErrorCode.SamePathAndDestinationPath : ErrorCode.SameLiteralPathAndDestinationPath; + var errorRecord = ErrorMessages.GetErrorRecord(errorCode: errorCode, errorItem: DestinationPath); ThrowTerminatingError(errorRecord); } } - private void ValidateSourcePath(System.IO.FileSystemInfo sourcePath) + private void ValidateSourcePath(string path) { - // Throw a terminating error if sourcePath does not exist - if (!sourcePath.Exists) - { - var errorRecord = ErrorMessages.GetErrorRecord(errorCode: ErrorCode.PathNotFound, errorItem: sourcePath.FullName); - ThrowTerminatingError(errorRecord); + // Resolve path + if (ParameterSetName == nameof(ParameterSet.Path)) { + // Set nonexistentPaths to null because we don't want to capture any nonexistent paths + var resolvedPaths = _pathHelper.GetResolvedPathFromPSProviderPath(path: path, pathMustExist: true); + Debug.Assert(resolvedPaths is not null); + + // If the path resolves to multiple paths, throw a terminating error + if (resolvedPaths.Count > 1) { + var errorRecord = ErrorMessages.GetErrorRecord(ErrorCode.PathResolvedToMultiplePaths, path); + ThrowTerminatingError(errorRecord); + } + + // Set _sourcePath to the first & only path in resolvedPaths + _sourcePath = resolvedPaths[0]; + } else { + // Set nonexistentPaths to null because we don't want to capture any nonexistent paths + var resolvedPath = _pathHelper.GetUnresolvedPathFromPSProviderPath(path: path, pathMustExist: true); + Debug.Assert(resolvedPath is not null); + // Set _sourcePath to resolvedPath + _sourcePath = resolvedPath; } - // Throw a terminating error if sourcePath is a directory - if (sourcePath.Attributes.HasFlag(FileAttributes.Directory)) + // Throw a terminating error if _sourcePath is a directory + if (Directory.Exists(_sourcePath)) { - var errorRecord = ErrorMessages.GetErrorRecord(errorCode: ErrorCode.DestinationExistsAsDirectory, errorItem: sourcePath.FullName); + var errorRecord = ErrorMessages.GetErrorRecord(errorCode: ErrorCode.DestinationExistsAsDirectory, errorItem: _sourcePath); ThrowTerminatingError(errorRecord); } } diff --git a/src/Localized/Messages.resx b/src/Localized/Messages.resx index b77c9e2..7d28961 100644 --- a/src/Localized/Messages.resx +++ b/src/Localized/Messages.resx @@ -191,4 +191,7 @@ The destination path cannot be determined automatically. Please use the -DestinationPath parameter to enter a destination path. + + The last write time of the path {0} is before 1980. Since zip does not support dates before January 1, 1980, the last write time will be set to January 1, 1980. + \ No newline at end of file diff --git a/src/Microsoft.PowerShell.Archive.psd1 b/src/Microsoft.PowerShell.Archive.psd1 index 46df1a8..6dd9f7d 100644 --- a/src/Microsoft.PowerShell.Archive.psd1 +++ b/src/Microsoft.PowerShell.Archive.psd1 @@ -7,7 +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') -CmdletsToExport = @('Compress-Archive') +CmdletsToExport = @('Compress-Archive', 'Expand-Archive') PrivateData = @{ PSData = @{ Tags = @('Archive', 'Zip', 'Compress') diff --git a/src/PathHelper.cs b/src/PathHelper.cs index b5262ed..9de80cb 100644 --- a/src/PathHelper.cs +++ b/src/PathHelper.cs @@ -212,7 +212,7 @@ private bool TryGetPathRelativeToCurrentWorkingDirectory(string path, out string return relativePathToWorkingDirectory is not null; } - internal System.Collections.ObjectModel.Collection? GetResolvedPathFromPSProviderPath(string path, HashSet nonexistentPaths) { + internal System.Collections.ObjectModel.Collection? GetResolvedPathFromPSProviderPath(string path, bool pathMustExist) { // Keep the exception at the top, then when an error occurs, use the exception to create an ErrorRecord Exception? exception = null; System.Collections.ObjectModel.Collection? fullyQualifiedPaths = null; @@ -251,8 +251,67 @@ private bool TryGetPathRelativeToCurrentWorkingDirectory(string path, out string { exception = invalidOperationException; } - // If a path can't be found, write an error - catch (System.Management.Automation.ItemNotFoundException) + // If a path can't be found, add it the set of non-existant paths + catch (System.Management.Automation.ItemNotFoundException itemNotFoundException) + { + if (pathMustExist) { + var errorRecord = ErrorMessages.GetErrorRecord(ErrorCode.PathNotFound); + _cmdlet.ThrowTerminatingError(errorRecord); + } + } + + // 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.ThrowTerminatingError(errorRecord); + } + + return fullyQualifiedPaths; + } + + internal System.Collections.ObjectModel.Collection? GetResolvedPathFromPSProviderPathWhileCapturingNonexistentPaths(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; + System.Collections.ObjectModel.Collection? fullyQualifiedPaths = null; + try + { + // Resolve path + var resolvedPaths = _cmdlet.SessionState.Path.GetResolvedProviderPathFromPSPath(path, out var providerInfo); + + // 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; + } + } + 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 a path can't be found, add it the set of non-existant paths + catch (System.Management.Automation.ItemNotFoundException itemNotFoundException) { nonexistentPaths.Add(path); } @@ -268,9 +327,9 @@ private bool TryGetPathRelativeToCurrentWorkingDirectory(string path, out string return fullyQualifiedPaths; } - // 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) { + // 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, bool pathMustExist) { // Keep the exception at the top, then when an error occurs, use the exception to create an ErrorRecord Exception? exception = null; string? fullyQualifiedPath = null; @@ -286,6 +345,11 @@ private bool TryGetPathRelativeToCurrentWorkingDirectory(string path, out string var exceptionMsg = ErrorMessages.GetErrorMessage(ErrorCode.InvalidPath); exception = new ArgumentException(exceptionMsg); } + // If the path does not exist, create an exception + else if (pathMustExist && !Path.Exists(resolvedPath)) { + var exceptionMsg = ErrorMessages.GetErrorMessage(ErrorCode.PathNotFound); + exception = new ArgumentException(exceptionMsg); + } else { fullyQualifiedPath = resolvedPath; @@ -312,7 +376,7 @@ private bool TryGetPathRelativeToCurrentWorkingDirectory(string path, out string exception = invalidOperationException; } - // If an exception was caught, write a non-terminating error of throwError == false. Otherwise, throw a terminating errror + // 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, @@ -325,7 +389,7 @@ private bool TryGetPathRelativeToCurrentWorkingDirectory(string path, out string // 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) { + internal string? GetUnresolvedPathFromPSProviderPathWhileCapturingNonexistentPaths(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; @@ -341,7 +405,7 @@ private bool TryGetPathRelativeToCurrentWorkingDirectory(string path, out string var exceptionMsg = ErrorMessages.GetErrorMessage(ErrorCode.InvalidPath); exception = new ArgumentException(exceptionMsg); } - // If the path does not exist, create an exception + // If the path does not exist, capture it else if (!Path.Exists(resolvedPath)) { nonexistentPaths.Add(resolvedPath); } From e68ea094a9bb40c178200103f0742c8b56fd6a65 Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Tue, 9 Aug 2022 14:44:17 -0700 Subject: [PATCH 10/34] added tests for special and wildcard tests, invalid DestinationPath, file permissions, and fixed a bug where the wrong error ID was used --- Tests/Compress-Archive.Tests.ps1 | 12 + Tests/Expand-Archive.Tests.ps1 | 428 +++++++++++++++++-------------- src/ExpandArchiveCommand.cs | 9 +- src/PathHelper.cs | 10 +- 4 files changed, 262 insertions(+), 197 deletions(-) diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index 9cbc7c1..dc3290f 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -225,6 +225,18 @@ BeforeDiscovery { $_.FullyQualifiedErrorId | Should -Be "SameLiteralPathAndDestinationPath,Microsoft.PowerShell.Archive.CompressArchiveCommand" } } + + It "Throws an error when an invalid path is supplied to DestinationPath" { + $sourcePath = "TestDrive:/SourceDir" + $destinationPath = "Variable:/PWD" + + try { + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + throw "Failed to detect an error when an invalid path is supplied to DestinationPath" + } catch { + $_.FullyQualifiedErrorId | Should -Be "InvalidPath,$CmdletClassName" + } + } } Context "WriteMode tests" { diff --git a/Tests/Expand-Archive.Tests.ps1 b/Tests/Expand-Archive.Tests.ps1 index bd6ea25..fc8a636 100644 --- a/Tests/Expand-Archive.Tests.ps1 +++ b/Tests/Expand-Archive.Tests.ps1 @@ -15,77 +15,50 @@ Describe("Expand-Archive Tests") { Context "Parameter set validation tests" { BeforeAll { - function ExpandArchivePathParameterSetValidator { - param - ( - [string] $path, - [string] $destinationPath - ) - - try - { - Expand-Archive -Path $path -DestinationPath $destinationPath - throw "ValidateNotNullOrEmpty attribute is missing on one of parameters belonging to Path parameterset." - } - catch - { - $_.FullyQualifiedErrorId | Should -Be "ParameterArgumentValidationError,$CmdletClassName" - } - } - - function ExpandArchiveLiteralPathParameterSetValidator { - param - ( - [string] $literalPath, - [string] $destinationPath - ) - - try - { - Expand-Archive -LiteralPath $literalPath -DestinationPath $destinationPath - throw "ValidateNotNullOrEmpty attribute is missing on one of parameters belonging to LiteralPath parameterset." - } - catch - { - $_.FullyQualifiedErrorId | Should -Be "ParameterArgumentValidationError,$CmdletClassName" - } - } - # Set up files for tests - New-Item $TestDrive/SourceDir -Type Directory | Out-Null + New-Item TestDrive:/SourceDir -Type Directory | Out-Null $content = "Some Data" - $content | Out-File -FilePath $TestDrive/Sample-1.txt + $content | Out-File -FilePath TestDrive:/Sample-1.txt # Create archives called archive1.zip and archive2.zip - Compress-Archive -Path $TestDrive/Sample-1.txt -DestinationPath $TestDrive/archive1.zip - Compress-Archive -Path $TestDrive/Sample-1.txt -DestinationPath $TestDrive/archive2.zip + Compress-Archive -Path TestDrive:/Sample-1.txt -DestinationPath TestDrive:/archive1.zip + Compress-Archive -Path TestDrive:/Sample-1.txt -DestinationPath TestDrive:/archive2.zip } - It "Validate errors with NULL & EMPTY values for Path, LiteralPath, and DestinationPath" { - $sourcePath = "$TestDrive/SourceDir" - $destinationPath = "$TestDrive/SampleSingleFile.zip" - - ExpandArchivePathParameterSetValidator $null $destinationPath - ExpandArchivePathParameterSetValidator $sourcePath $null - ExpandArchivePathParameterSetValidator $null $null - - ExpandArchivePathParameterSetValidator "" $destinationPath - ExpandArchivePathParameterSetValidator $sourcePath "" - ExpandArchivePathParameterSetValidator "" "" + It "Validate errors with NULL & EMPTY values for Path, LiteralPath, and DestinationPath" -ForEach @( + @{ Path = $null; DestinationPath = "TestDrive:/destination" } + @{ Path = "TestDrive:/archive1.zip"; DestinationPath = $null } + @{ Path = $null; DestinationPath = $null } + @{ Path = ""; DestinationPath = "TestDrive:/destination" } + @{ Path = "TestDrive:/archive1.zip"; DestinationPath = "" } + @{ Path = ""; DestinationPath = "" } + ) { - ExpandArchiveLiteralPathParameterSetValidator $null $destinationPath - ExpandArchiveLiteralPathParameterSetValidator $sourcePath $null - ExpandArchiveLiteralPathParameterSetValidator $null $null + try + { + Expand-Archive -Path $Path -DestinationPath $DestinationPath + throw "ValidateNotNullOrEmpty attribute is missing on one of parameters belonging to Path parameterset." + } + catch + { + $_.FullyQualifiedErrorId | Should -Be "ParameterArgumentValidationError,$CmdletClassName" + } - ExpandArchiveLiteralPathParameterSetValidator "" $destinationPath - ExpandArchiveLiteralPathParameterSetValidator $sourcePath "" - ExpandArchiveLiteralPathParameterSetValidator "" "" + try + { + Expand-Archive -LiteralPath $Path -DestinationPath $DestinationPath + throw "ValidateNotNullOrEmpty attribute is missing on one of parameters belonging to LiteralPath parameterset." + } + catch + { + $_.FullyQualifiedErrorId | Should -Be "ParameterArgumentValidationError,$CmdletClassName" + } } It "Throws when non-existing path is supplied for Path or LiteralPath parameters" { - $path = "$TestDrive/non-existant.zip" - $destinationPath = "$TestDrive($DS)DestinationFolder" + $path = "TestDrive:/non-existant.zip" + $destinationPath = "TestDrive:($DS)DestinationFolder" try { Expand-Archive -Path $path -DestinationPath $destinationPath @@ -107,9 +80,9 @@ Describe("Expand-Archive Tests") { } } - It "Throws when invalid path non-filesystem path is supplied for Path or LiteralPath parameters" { - $path = "Variable:DS" - $destinationPath = "$TestDrive($DS)DestinationFolder" + It "Throws when path non-filesystem path is supplied for Path or LiteralPath parameters" { + $path = "Variable:/PWD" + $destinationPath = "TestDrive:($DS)DestinationFolder" try { Expand-Archive -Path $path -DestinationPath $destinationPath @@ -133,9 +106,9 @@ Describe("Expand-Archive Tests") { It "Throws an error when multiple paths are supplied as input to Path parameter" { $sourcePath = @( - "$TestDrive/SourceDir/archive1.zip", - "$TestDrive/SourceDir/archive2.zip") - $destinationPath = "$TestDrive/DestinationFolder" + "TestDrive:/SourceDir/archive1.zip", + "TestDrive:/SourceDir/archive2.zip") + $destinationPath = "TestDrive:/DestinationFolder" try { @@ -150,9 +123,9 @@ Describe("Expand-Archive Tests") { It "Throws an error when multiple paths are supplied as input to LiteralPath parameter" { $sourcePath = @( - "$TestDrive/SourceDir/archive1.zip", - "$TestDrive/SourceDir/archive2.zip") - $destinationPath = "$TestDrive/DestinationFolder" + "TestDrive:/SourceDir/archive1.zip", + "TestDrive:/SourceDir/archive2.zip") + $destinationPath = "TestDrive:/DestinationFolder" try { @@ -168,7 +141,7 @@ Describe("Expand-Archive Tests") { ## From 504 It "Validate that Source Path can be at SystemDrive location" -Skip { $sourcePath = "$env:SystemDrive/SourceDir" - $destinationPath = "$TestDrive/SampleFromSystemDrive.zip" + $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/SampleSourceFileForArchive.txt try @@ -183,7 +156,7 @@ Describe("Expand-Archive Tests") { } It "Throws an error when Path and DestinationPath are the same and -WriteMode Overwrite is specified" { - $sourcePath = "$TestDrive/archive1.zip" + $sourcePath = "TestDrive:/archive1.zip" $destinationPath = $sourcePath try { @@ -195,7 +168,7 @@ Describe("Expand-Archive Tests") { } It "Throws an error when LiteralPath and DestinationPath are the same and WriteMode -Overwrite is specified" { - $sourcePath = "$TestDrive/archive1.zip" + $sourcePath = "TestDrive:/archive1.zip" $destinationPath = $sourcePath try { @@ -207,7 +180,7 @@ Describe("Expand-Archive Tests") { } It "Throws an error when an invalid path is supplied to DestinationPath" { - $sourcePath = "$TestDrive/archive1.zip" + $sourcePath = "TestDrive:/archive1.zip" $destinationPath = "Variable:/PWD" try { @@ -220,51 +193,33 @@ Describe("Expand-Archive Tests") { } Context "DestinationPath and Overwrite Tests" { - # error when destination path is a file and overwrite is not specified - # error when output has same name as existant file and overwrite is not specified - - # no error when destination path is existing folder - # no error when output is folder - - # output is directory w/ at least 1 item - # output has same name as current working directory - - # overwrite file works - # overwrite output file works done - # overwrite file w/file done - # overwrite output file w/directory - # overwrite directory w/file - # overwrite non-existant path works - - # last write times - BeforeAll { - New-Item -Path "$TestDrive/file1.txt" -ItemType File - "Hello, World!" | Out-File -FilePath "$TestDrive/file1.txt" - Compress-Archive -Path "$TestDrive/file1.txt" -DestinationPath "$TestDrive/archive1.zip" + New-Item -Path "TestDrive:/file1.txt" -ItemType File + "Hello, World!" | Out-File -FilePath "TestDrive:/file1.txt" + Compress-Archive -Path "TestDrive:/file1.txt" -DestinationPath "TestDrive:/archive1.zip" - New-Item -Path "$TestDrive/directory1" -ItemType Directory + New-Item -Path "TestDrive:/directory1" -ItemType Directory # Create archive2.zip containing directory1 - Compress-Archive -Path "$TestDrive/directory1" -DestinationPath "$TestDrive/archive2.zip" + Compress-Archive -Path "TestDrive:/directory1" -DestinationPath "TestDrive:/archive2.zip" - New-Item -Path "$TestDrive/ParentDir" -ItemType Directory - New-Item -Path "$TestDrive/ParentDir/file1.txt" -ItemType Directory + New-Item -Path "TestDrive:/ParentDir" -ItemType Directory + New-Item -Path "TestDrive:/ParentDir/file1.txt" -ItemType Directory # Create a dir that is a container for items to be overwritten - New-Item -Path "$TestDrive/ItemsToOverwriteContainer" -ItemType Directory - New-Item -Path "$TestDrive/ItemsToOverwriteContainer/file2" -ItemType File - New-Item -Path "$TestDrive/ItemsToOverwriteContainer/subdir1" -ItemType Directory - New-Item -Path "$TestDrive/ItemsToOverwriteContainer/subdir1/file1.txt" -ItemType File - New-Item -Path "$TestDrive/ItemsToOverwriteContainer/subdir2" -ItemType Directory - New-Item -Path "$TestDrive/ItemsToOverwriteContainer/subdir2/file1.txt" -ItemType Directory - New-Item -Path "$TestDrive/ItemsToOverwriteContainer/subdir4" -ItemType Directory - New-Item -Path "$TestDrive/ItemsToOverwriteContainer/subdir4/file1.txt" -ItemType Directory - New-Item -Path "$TestDrive/ItemsToOverwriteContainer/subdir4/file1.txt/somefile" -ItemType File + New-Item -Path "TestDrive:/ItemsToOverwriteContainer" -ItemType Directory + New-Item -Path "TestDrive:/ItemsToOverwriteContainer/file2" -ItemType File + New-Item -Path "TestDrive:/ItemsToOverwriteContainer/subdir1" -ItemType Directory + New-Item -Path "TestDrive:/ItemsToOverwriteContainer/subdir1/file1.txt" -ItemType File + New-Item -Path "TestDrive:/ItemsToOverwriteContainer/subdir2" -ItemType Directory + New-Item -Path "TestDrive:/ItemsToOverwriteContainer/subdir2/file1.txt" -ItemType Directory + New-Item -Path "TestDrive:/ItemsToOverwriteContainer/subdir4" -ItemType Directory + New-Item -Path "TestDrive:/ItemsToOverwriteContainer/subdir4/file1.txt" -ItemType Directory + New-Item -Path "TestDrive:/ItemsToOverwriteContainer/subdir4/file1.txt/somefile" -ItemType File # Create directory to override - New-Item -Path "$TestDrive/ItemsToOverwriteContainer/subdir3" -ItemType Directory - New-Item -Path "$TestDrive/ItemsToOverwriteContainer/subdir3/directory1" -ItemType File + New-Item -Path "TestDrive:/ItemsToOverwriteContainer/subdir3" -ItemType Directory + New-Item -Path "TestDrive:/ItemsToOverwriteContainer/subdir3/directory1" -ItemType File # Set the error action preference so non-terminating errors aren't displayed $ErrorActionPreference = 'SilentlyContinue' @@ -275,9 +230,9 @@ Describe("Expand-Archive Tests") { $ErrorActionPreference = 'Continue' } - It "Throws an error when DestinationPath is an existing file" { - $sourcePath = "$TestDrive/archive1.zip" - $destinationPath = "$TestDrive/file1.txt" + It "Throws an error when DestinationPath is an existing file" -Tag debug2 { + $sourcePath = "TestDrive:/archive1.zip" + $destinationPath = "TestDrive:/file1.txt" try { Expand-Archive -Path $sourcePath -DestinationPath $destinationPath @@ -287,8 +242,8 @@ Describe("Expand-Archive Tests") { } It "Does not throw an error when a directory in the archive has the same destination path as an existing directory" { - $sourcePath = "$TestDrive/archive2.zip" - $destinationPath = "$TestDrive" + $sourcePath = "TestDrive:/archive2.zip" + $destinationPath = "TestDrive:" try { Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -ErrorAction Stop @@ -298,8 +253,8 @@ Describe("Expand-Archive Tests") { } It "Writes a non-terminating error when a file in the archive has a destination path that already exists" { - $sourcePath = "$TestDrive/archive1.zip" - $destinationPath = "$TestDrive" + $sourcePath = "TestDrive:/archive1.zip" + $destinationPath = "TestDrive:" Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -ErrorVariable error $error.Count | Should -Be 1 @@ -307,8 +262,8 @@ Describe("Expand-Archive Tests") { } It "Writes a non-terminating error when a file in the archive has a destination path that is an existing directory containing at least 1 item and -WriteMode Overwrite is specified" { - $sourcePath = "$TestDrive/archive1.zip" - $destinationPath = "$TestDrive/ItemsToOverwriteContainer/subdir4" + $sourcePath = "TestDrive:/archive1.zip" + $destinationPath = "TestDrive:/ItemsToOverwriteContainer/subdir4" Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite -ErrorVariable error $error.Count | Should -Be 1 @@ -316,8 +271,8 @@ Describe("Expand-Archive Tests") { } It "Writes a non-terminating error when a file in the archive has a destination path that is the working directory and -WriteMode Overwrite is specified" { - $sourcePath = "$TestDrive/archive1.zip" - $destinationPath = "$TestDrive/ParentDir" + $sourcePath = "TestDrive:/archive1.zip" + $destinationPath = "TestDrive:/ParentDir" Push-Location "$destinationPath/file1.txt" @@ -331,52 +286,52 @@ Describe("Expand-Archive Tests") { } It "Overwrites a file when it is DestinationPath and -WriteMode Overwrite is specified" { - $sourcePath = "$TestDrive/archive1.zip" - $destinationPath = "$TestDrive/ItemsToOverwriteContainer/file2" + $sourcePath = "TestDrive:/archive1.zip" + $destinationPath = "TestDrive:/ItemsToOverwriteContainer/file2" Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite -ErrorVariable error $error.Count | Should -Be 0 # Ensure the file in archive1.zip was expanded - Test-Path "$TestDrive/ItemsToOverwriteContainer/file2/file1.txt" -PathType Leaf + Test-Path "TestDrive:/ItemsToOverwriteContainer/file2/file1.txt" -PathType Leaf } It "Overwrites a file whose path is the same as the destination path of a file in the archive when -WriteMode Overwrite is specified" -Tag td { - $sourcePath = "$TestDrive/archive1.zip" - $destinationPath = "$TestDrive/ItemsToOverwriteContainer/subdir1" + $sourcePath = "TestDrive:/archive1.zip" + $destinationPath = "TestDrive:/ItemsToOverwriteContainer/subdir1" Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite -ErrorVariable error $error.Count | Should -Be 0 # Ensure the file in archive1.zip was expanded - Test-Path "$TestDrive/ItemsToOverwriteContainer/subdir1/file1.txt" -PathType Leaf + Test-Path "TestDrive:/ItemsToOverwriteContainer/subdir1/file1.txt" -PathType Leaf # Ensure the contents of file1.txt is "Hello, World!" - Get-Content -Path "$TestDrive/ItemsToOverwriteContainer/subdir1/file1.txt" | Should -Be "Hello, World!" + Get-Content -Path "TestDrive:/ItemsToOverwriteContainer/subdir1/file1.txt" | Should -Be "Hello, World!" } It "Overwrites a directory whose path is the same as the destination path of a file in the archive when -WriteMode Overwrite is specified" { - $sourcePath = "$TestDrive/archive1.zip" - $destinationPath = "$TestDrive/ItemsToOverwriteContainer/subdir2" + $sourcePath = "TestDrive:/archive1.zip" + $destinationPath = "TestDrive:/ItemsToOverwriteContainer/subdir2" Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite -ErrorVariable error $error.Count | Should -Be 0 # Ensure the file in archive1.zip was expanded - Test-Path "$TestDrive/ItemsToOverwriteContainer/subdir2/file1.txt" -PathType Leaf + Test-Path "TestDrive:/ItemsToOverwriteContainer/subdir2/file1.txt" -PathType Leaf # Ensure the contents of file1.txt is "Hello, World!" - Get-Content -Path "$TestDrive/ItemsToOverwriteContainer/subdir2/file1.txt" | Should -Be "Hello, World!" + Get-Content -Path "TestDrive:/ItemsToOverwriteContainer/subdir2/file1.txt" | Should -Be "Hello, World!" } It "Overwrites a file whose path is the same as the destination path of a directory in the archive when -WriteMode Overwrite is specified" { - $sourcePath = "$TestDrive/archive2.zip" - $destinationPath = "$TestDrive/ItemsToOverwriteContainer/subdir3" + $sourcePath = "TestDrive:/archive2.zip" + $destinationPath = "TestDrive:/ItemsToOverwriteContainer/subdir3" Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite -ErrorVariable error $error.Count | Should -Be 0 # Ensure the file in archive1.zip was expanded - Test-Path "$TestDrive/ItemsToOverwriteContainer/subdir3/directory1" -PathType Container + Test-Path "TestDrive:/ItemsToOverwriteContainer/subdir3/directory1" -PathType Container } } @@ -387,25 +342,26 @@ Describe("Expand-Archive Tests") { # expand archive works when -DestinationPath is not specified (and there are mutiple top level items) BeforeAll { - New-Item -Path "$TestDrive/file1.txt" -ItemType File - "Hello, World!" | Out-File -FilePath "$TestDrive/file1.txt" - Compress-Archive -Path "$TestDrive/file1.txt" -DestinationPath "$TestDrive/archive1.zip" + New-Item -Path "TestDrive:/file1.txt" -ItemType File + "Hello, World!" | Out-File -FilePath "TestDrive:/file1.txt" + Compress-Archive -Path "TestDrive:/file1.txt" -DestinationPath "TestDrive:/archive1.zip" - New-Item -Path "$TestDrive/directory2" -ItemType Directory - New-Item -Path "$TestDrive/directory3" -ItemType Directory - New-Item -Path "$TestDrive/directory4" -ItemType Directory - New-Item -Path "$TestDrive/directory5" -ItemType Directory + New-Item -Path "TestDrive:/directory2" -ItemType Directory + New-Item -Path "TestDrive:/directory3" -ItemType Directory + New-Item -Path "TestDrive:/directory4" -ItemType Directory + New-Item -Path "TestDrive:/directory5" -ItemType Directory + New-Item -Path "TestDrive:/directory6" -ItemType Directory - New-Item -Path "$TestDrive/DirectoryToArchive" -ItemType Directory - Compress-Archive -Path "$TestDrive/DirectoryToArchive" -DestinationPath "$TestDrive/archive2.zip" + New-Item -Path "TestDrive:/DirectoryToArchive" -ItemType Directory + Compress-Archive -Path "TestDrive:/DirectoryToArchive" -DestinationPath "TestDrive:/archive2.zip" # Create an archive containing a file and an empty folder - Compress-Archive -Path "$TestDrive/file1.txt","$TestDrive/DirectoryToArchive" -DestinationPath "$TestDrive/archive3.zip" + Compress-Archive -Path "TestDrive:/file1.txt","TestDrive:/DirectoryToArchive" -DestinationPath "TestDrive:/archive3.zip" } It "Expands an archive when a non-existent directory is specified as -DestinationPath" { - $sourcePath = "$TestDrive/archive1.zip" - $destinationPath = "$TestDrive/directory1" + $sourcePath = "TestDrive:/archive1.zip" + $destinationPath = "TestDrive:/directory1" Expand-Archive -Path $sourcePath -DestinationPath $destinationPath @@ -414,9 +370,9 @@ Describe("Expand-Archive Tests") { $itemsInDestinationPath[0].Name | Should -Be "file1.txt" } - It "Expands an archive when DestinationPath is an existing directory" { - $sourcePath = "$TestDrive/archive1.zip" - $destinationPath = "$TestDrive/directory1" + It "Expands an archive when DestinationPath is an existing directory" -Tag debug3 { + $sourcePath = "TestDrive:/archive1.zip" + $destinationPath = "TestDrive:/directory2" try { Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -ErrorAction Stop @@ -426,8 +382,8 @@ Describe("Expand-Archive Tests") { } It "Expands an archive to the working directory when it is specified as -DestinationPath" { - $sourcePath = "$TestDrive/archive1.zip" - $destinationPath = "$TestDrive/directory2" + $sourcePath = "TestDrive:/archive1.zip" + $destinationPath = "TestDrive:/directory3" Push-Location $destinationPath @@ -440,26 +396,24 @@ Describe("Expand-Archive Tests") { Pop-Location } - It "Expands an archive containing a single top-level directory and no other top-level items to a directory with that directory's name when -DestinationPath is not specified" -Tag this1{ - $sourcePath = "$TestDrive/archive2.zip" - $destinationPath = "$TestDrive/directory3" + It "Expands an archive containing a single top-level directory and no other top-level items to a directory with that directory's name when -DestinationPath is not specified" { + $sourcePath = "TestDrive:/archive2.zip" + $destinationPath = "TestDrive:/directory4" Push-Location $destinationPath Expand-Archive -Path $sourcePath - $itemsInDestinationPath = Get-ChildItem "$TestDrive/directory3" -Recurse + $itemsInDestinationPath = Get-ChildItem $destinationPath -Recurse $itemsInDestinationPath.Count | Should -Be 1 $itemsInDestinationPath[0].Name | Should -Be "DirectoryToArchive" - Test-Path -Path "$TestDrive/directory3/DirectoryToArchive" -PathType Container - Pop-Location } It "Expands an archive containing multiple top-level items to a directory with that archive's name when -DestinationPath is not specified" { - $sourcePath = "$TestDrive/archive3.zip" - $destinationPath = "$TestDrive/directory4" + $sourcePath = "TestDrive:/archive3.zip" + $destinationPath = "TestDrive:/directory5" Push-Location $destinationPath @@ -468,8 +422,8 @@ Describe("Expand-Archive Tests") { $itemsInDestinationPath = Get-ChildItem $destinationPath -Name -Recurse $itemsInDestinationPath.Count | Should -Be 3 "archive3" | Should -BeIn $itemsInDestinationPath - "archive3${DS}DirectoryToArchive" | Should -BeIn $itemsInDestinationPath - "archive3${DS}file1.txt" | Should -BeIn $itemsInDestinationPath + (Join-Path "archive3" "DirectoryToArchive") | Should -BeIn $itemsInDestinationPath + (Join-Path "archive3" "file1.txt") | Should -BeIn $itemsInDestinationPath Pop-Location @@ -478,32 +432,32 @@ Describe("Expand-Archive Tests") { It "Expands an archive containing multiple files, non-empty directories, and empty directories" { # Create an archive containing multiple files, non-empty directories, and empty directories - New-Item -Path "$TestDrive/file2.txt" -ItemType File - "Hello, World!" | Out-File -FilePath "$TestDrive/file2.txt" - New-Item -Path "$TestDrive/file3.txt" -ItemType File - "Hello, World!" | Out-File -FilePath "$TestDrive/file3.txt" + New-Item -Path "TestDrive:/file2.txt" -ItemType File + "Hello, World!" | Out-File -FilePath "TestDrive:/file2.txt" + New-Item -Path "TestDrive:/file3.txt" -ItemType File + "Hello, World!" | Out-File -FilePath "TestDrive:/file3.txt" - New-Item -Path "$TestDrive/emptydirectory1" -ItemType Directory - New-Item -Path "$TestDrive/emptydirectory2" -ItemType Directory + New-Item -Path "TestDrive:/emptydirectory1" -ItemType Directory + New-Item -Path "TestDrive:/emptydirectory2" -ItemType Directory - New-Item -Path "$TestDrive/nonemptydirectory1" -ItemType Directory - New-Item -Path "$TestDrive/nonemptydirectory2" -ItemType Directory + New-Item -Path "TestDrive:/nonemptydirectory1" -ItemType Directory + New-Item -Path "TestDrive:/nonemptydirectory2" -ItemType Directory - New-Item -Path "$TestDrive/nonemptydirectory1/subfile1.txt" -ItemType File - New-Item -Path "$TestDrive/nonemptydirectory2/subemptydirectory1" -ItemType Directory + New-Item -Path "TestDrive:/nonemptydirectory1/subfile1.txt" -ItemType File + New-Item -Path "TestDrive:/nonemptydirectory2/subemptydirectory1" -ItemType Directory - $archive4Paths = @("$TestDrive/file2.txt", "$TestDrive/file3.txt", "$TestDrive/emptydirectory1", "$TestDrive/emptydirectory2", "$TestDrive/nonemptydirectory1", "$TestDrive/nonemptydirectory2") + $archive4Paths = @("TestDrive:/file2.txt", "TestDrive:/file3.txt", "TestDrive:/emptydirectory1", "TestDrive:/emptydirectory2", "TestDrive:/nonemptydirectory1", "TestDrive:/nonemptydirectory2") - Compress-Archive -Path $archive4Paths -DestinationPath "$TestDrive/archive4.zip" + Compress-Archive -Path $archive4Paths -DestinationPath "TestDrive:/archive4.zip" - $sourcePath = "$TestDrive/archive4.zip" - $destinationPath = "$TestDrive/directory5" + $sourcePath = "TestDrive:/archive4.zip" + $destinationPath = "TestDrive:/directory6" Expand-Archive -Path $sourcePath -DestinationPath $destinationPath $expandedItems = Get-ChildItem $destinationPath -Recurse -Name - $itemsInArchive = @("file2.txt", "file3.txt", "emptydirectory1", "emptydirectory2", "nonemptydirectory1", "nonemptydirectory2", "nonemptydirectory1${DS}subfile1.txt", "nonemptydirectory2${DS}subemptydirectory1") + $itemsInArchive = @("file2.txt", "file3.txt", "emptydirectory1", "emptydirectory2", "nonemptydirectory1", "nonemptydirectory2", (Join-Path "nonemptydirectory1" "subfile1.txt"), (Join-Path "nonemptydirectory2" "subemptydirectory1")) $expandedItems.Length | Should -Be $itemsInArchive.Count foreach ($item in $itemsInArchive) { @@ -512,15 +466,15 @@ Describe("Expand-Archive Tests") { } It "Expands an archive containing a file whose LastWriteTime is in the past" { - New-Item -Path "$TestDrive/oldfile.txt" -ItemType File - Set-ItemProperty -Path "$TestDrive/oldfile.txt" -Name "LastWriteTime" -Value '2003-01-16 14:44' - Compress-Archive -Path "$TestDrive/oldfile.txt" -DestinationPath "$TestDrive/archive_oldfile.zip" + New-Item -Path "TestDrive:/oldfile.txt" -ItemType File + Set-ItemProperty -Path "TestDrive:/oldfile.txt" -Name "LastWriteTime" -Value '2003-01-16 14:44' + Compress-Archive -Path "TestDrive:/oldfile.txt" -DestinationPath "TestDrive:/archive_oldfile.zip" - $sourcePath = "$TestDrive/archive_oldfile.zip" - $destinationPath = "$TestDrive/destination6" + $sourcePath = "TestDrive:/archive_oldfile.zip" + $destinationPath = "TestDrive:/destination7" Expand-Archive -Path $sourcePath -DestinationPath $destinationPath - $lastWriteTime = Get-ItemPropertyValue -Path "$TestDrive/oldfile.txt" -Name "LastWriteTime" + $lastWriteTime = Get-ItemPropertyValue -Path (Join-Path $destinationPath "oldfile.txt") -Name "LastWriteTime" $lastWriteTime.Year | Should -Be 2003 $lastWriteTime.Month | Should -Be 1 @@ -532,15 +486,15 @@ Describe("Expand-Archive Tests") { } It "Expands an archive containing a directory whose LastWriteTime is in the past" { - New-Item -Path "$TestDrive/olddirectory" -ItemType Directory - Set-ItemProperty -Path "$TestDrive/olddirectory" -Name "LastWriteTime" -Value '2003-01-16 14:44' - Compress-Archive -Path "$TestDrive/olddirectory" -DestinationPath "$TestDrive/archive_olddirectory.zip" + New-Item -Path "TestDrive:/olddirectory" -ItemType Directory + Set-ItemProperty -Path "TestDrive:/olddirectory" -Name "LastWriteTime" -Value '2003-01-16 14:44' + Compress-Archive -Path "TestDrive:/olddirectory" -DestinationPath "TestDrive:/archive_olddirectory.zip" - $sourcePath = "$TestDrive/archive_olddirectory.zip" - $destinationPath = "$TestDrive/destination_olddirectory" + $sourcePath = "TestDrive:/archive_olddirectory.zip" + $destinationPath = "TestDrive:/destination_olddirectory" Expand-Archive -Path $sourcePath -DestinationPath $destinationPath - $lastWriteTime = Get-ItemPropertyValue -Path "$TestDrive/destination_olddirectory/olddirectory" -Name "LastWriteTime" + $lastWriteTime = Get-ItemPropertyValue -Path "TestDrive:/destination_olddirectory/olddirectory" -Name "LastWriteTime" $lastWriteTime.Year | Should -Be 2003 $lastWriteTime.Month | Should -Be 1 @@ -555,16 +509,16 @@ Describe("Expand-Archive Tests") { Context "PassThru tests" { BeforeAll { New-Item -Path TestDrive:/file1.txt -ItemType File - "Hello, World!" | Out-File -Path Test:/file1.txt + "Hello, World!" | Out-File -Path TestDrive:/file1.txt $archivePath = "TestDrive:/archive.zip" Compress-Archive -Path TestDrive:/file1.txt -DestinationPath $archivePath } It "Returns a System.IO.DirectoryInfo object when PassThru is specified" { - $destinationPath = "{TestDrive}/archive_contents" + $destinationPath = "TestDrive:/archive_contents" $output = Expand-Archive -Path $archivePath -DestinationPath $destinationPath -PassThru $output | Should -BeOfType System.IO.DirectoryInfo - $output.FullName | SHould -Be $destinationPath + $output.FullName | Should -Be (Convert-Path $destinationPath) } It "Does not return an object when PassThru is not specified" { @@ -573,10 +527,100 @@ Describe("Expand-Archive Tests") { } It "Does not return an object when PassThru is false" { - $output = Compress-Archive -Path $archivePath -DestinationPath TestDrive:/archive_contents -PassThru:$false + $output = Expand-Archive -Path $archivePath -DestinationPath TestDrive:/archive_contents3 -PassThru:$false $output | Should -BeNullOrEmpty } } - + Context "Special and Wildcard Character Tests" { + BeforeAll { + New-Item TestDrive:/file.txt -ItemType File + "Hello, World!" | Out-File -Path TestDrive:/file.txt + + Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive_containing_file.zip + Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive_with_number_1.zip + Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive_with_number_2.zip + Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive_with_[.zip + } + + AfterAll { + Remove-Item -LiteralPath "TestDrive:/archive_with_[.zip" + } + + It "Expands an archive when -Path contains wildcard character and resolves to 1 path" { + Expand-Archive -Path TestDrive:/archive_containing* -DestinationPath TestDrive:/destination1 + (Convert-Path TestDrive:/destination1/file.txt) | Should -Exist + } + + It "Throws a terminating error when archive when -Path contains wildcard character and resolves to multiple paths" { + try { + Expand-Archive -Path TestDrive:/archive_with* -DestinationPath TestDrive:/destination2 + } catch { + $_.FullyQualifiedErrorId | Should -Be "PathResolvedToMultiplePaths,$CmdletClassName" + } + } + + It "Expands an archive when -LiteralPath contains [ but no matching ]" { + Expand-Archive -LiteralPath TestDrive:/archive_with_[.zip -DestinationPath TestDrive:/destination3 + (Convert-Path TestDrive:/destination3/file.txt) | Should -Exist + } + + It "Expands an archive when -DestinationPath contains [ but no matching ]" { + Expand-Archive -Path TestDrive:/archive_containing_file.zip -DestinationPath TestDrive:/destination[ + Test-Path -LiteralPath TestDrive:/destination[/file.txt | Should -Be $true + Remove-Item -LiteralPath "${TestDrive}/destination[" -Recurse + } + } + + Context "File permssions, attributes, etc tests" { + BeforeAll { + New-Item TestDrive:/file.txt -ItemType File + "Hello, World!" | Out-File -Path TestDrive:/file.txt + + # Create a readonly archive + Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/readonly.zip + Set-ItemProperty -Path TestDrive:/readonly.zip -Name "IsReadOnly" -Value $true + + # Create an archive in-use + Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive_in_use.zip + $fileMode = [System.IO.FileMode]::Open + $fileAccess = [System.IO.FileAccess]::Read + $fileShare = [System.IO.FileShare]::Read + $archiveInUseStream = New-Object -TypeName "System.IO.FileStream" -ArgumentList "${TestDrive}/archive_in_use.zip",$fileMode,$fileAccess,$fileShare + + # Create an archive containing an entry with non-latin characters + New-Item TestDrive:/ملف -ItemType File + "Hello, World!" | Out-File -Path TestDrive:/ملف + $archiveWithNonLatinEntryPath = Join-Path $TestDrive "archive_with_nonlatin_entry.zip" + if ($IsWindows) { + 7z.exe a $archiveWithNonLatinEntryPath (Join-Path $TestDrive ملف) + } else { + 7z a $archiveWithNonLatinEntryPath (Join-Path $TestDrive ملف) + } + + } + + AfterAll { + $archiveInUseStream.Dispose() + } + + It "Expands a read-only archive" { + Expand-Archive -Path TestDrive:/readonly.zip -DestinationPath TestDrive:/readonly_output + "TestDrive:/readonly_output/file.txt" | Should -Exist + } + + It "Expands an archive in-use" { + Expand-Archive -Path TestDrive:/archive_in_use.zip -DestinationPath TestDrive:/archive_in_use_output + "TestDrive:/archive_in_use_output/file.txt" | Should -Exist + } + + It "Expands an archive containing an entry with non-latin characters" { + Expand-Archive -Path $archiveWithNonLatinEntryPath -DestinationPath TestDrive:/archive_with_nonlatin_entry_output + "TestDrive:/archive_with_nonlatin_entry_output/ملف" | Should -Exist + } + } + + Context "Large File Tests" { + + } } \ No newline at end of file diff --git a/src/ExpandArchiveCommand.cs b/src/ExpandArchiveCommand.cs index 3b6a0e5..510bba6 100644 --- a/src/ExpandArchiveCommand.cs +++ b/src/ExpandArchiveCommand.cs @@ -94,7 +94,7 @@ protected override void EndProcessing() DestinationPath = DetermineDestinationPath(archive); } else { // Resolve DestinationPath and validate it - DestinationPath = _pathHelper.GetUnresolvedPathFromPSProviderPath(path: DestinationPath, pathMustExist: true); + DestinationPath = _pathHelper.GetUnresolvedPathFromPSProviderPath(path: DestinationPath, pathMustExist: false); } ValidateDestinationPath(); Debug.Assert(DestinationPath is not null); @@ -129,6 +129,11 @@ protected override void EndProcessing() // TODO: Change this later to write an error throw unauthorizedAccessException; } + + // If PassThru is true, return a System.IO.DirectoryInfo object pointing to directory where archive expanded + if (PassThru) { + WriteObject(new DirectoryInfo(DestinationPath)); + } } protected override void StopProcessing() @@ -219,7 +224,7 @@ private void ValidateDestinationPath() // Throw an error if DestinationPath exists and the cmdlet is not in Overwrite mode if (File.Exists(DestinationPath) && WriteMode == ExpandArchiveWriteMode.Expand) { - var errorRecord = ErrorMessages.GetErrorRecord(errorCode: ErrorCode.CannotDetermineDestinationPath, errorItem: DestinationPath); + var errorRecord = ErrorMessages.GetErrorRecord(errorCode: ErrorCode.DestinationExists, errorItem: DestinationPath); ThrowTerminatingError(errorRecord); } diff --git a/src/PathHelper.cs b/src/PathHelper.cs index 9de80cb..9dbb9af 100644 --- a/src/PathHelper.cs +++ b/src/PathHelper.cs @@ -333,6 +333,7 @@ private bool TryGetPathRelativeToCurrentWorkingDirectory(string path, out string // Keep the exception at the top, then when an error occurs, use the exception to create an ErrorRecord Exception? exception = null; string? fullyQualifiedPath = null; + ErrorCode errorCode = ErrorCode.InvalidPath; try { // Resolve path @@ -347,8 +348,9 @@ private bool TryGetPathRelativeToCurrentWorkingDirectory(string path, out string } // If the path does not exist, create an exception else if (pathMustExist && !Path.Exists(resolvedPath)) { - var exceptionMsg = ErrorMessages.GetErrorMessage(ErrorCode.PathNotFound); - exception = new ArgumentException(exceptionMsg); + errorCode = ErrorCode.PathNotFound; + var exceptionMsg = ErrorMessages.GetErrorMessage(errorCode); + throw new ItemNotFoundException(exceptionMsg); } else { @@ -374,12 +376,14 @@ private bool TryGetPathRelativeToCurrentWorkingDirectory(string path, out string catch (System.Management.Automation.PSInvalidOperationException invalidOperationException) { exception = invalidOperationException; + } catch (System.Management.Automation.ItemNotFoundException itemNotFoundException) { + exception = itemNotFoundException; } // 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, + var errorRecord = new ErrorRecord(exception: exception, errorId: errorCode.ToString(), errorCategory: ErrorCategory.InvalidArgument, targetObject: path); _cmdlet.ThrowTerminatingError(errorRecord); } From a0452f009a7b9a53061a64b01b4e5a74ebc9f961 Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Tue, 9 Aug 2022 19:21:53 -0700 Subject: [PATCH 11/34] updated CI to run Expand-Archive tests, worked on tar support --- .azdevops/RunTests.ps1 | 2 +- Tests/Compress-Archive.Tests.ps1 | 2 +- src/ArchiveAddition.cs | 2 +- src/IArchive.cs | 11 ---- src/IEntry.cs | 2 +- src/TarArchive.cs | 99 +++++++++++++++++++++++++++----- src/ZipArchive.cs | 25 +++----- 7 files changed, 97 insertions(+), 46 deletions(-) diff --git a/.azdevops/RunTests.ps1 b/.azdevops/RunTests.ps1 index 0361d4f..f85198e 100644 --- a/.azdevops/RunTests.ps1 +++ b/.azdevops/RunTests.ps1 @@ -21,7 +21,7 @@ Import-Module -Name "Pester" -MinimumVersion $pesterMinVersion -MaximumVersion $ # 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 +$results = Invoke-Pester -Script ./Tests -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) { diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index dc3290f..5f1387b 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -234,7 +234,7 @@ BeforeDiscovery { Compress-Archive -Path $sourcePath -DestinationPath $destinationPath throw "Failed to detect an error when an invalid path is supplied to DestinationPath" } catch { - $_.FullyQualifiedErrorId | Should -Be "InvalidPath,$CmdletClassName" + $_.FullyQualifiedErrorId | Should -Be "InvalidPath,Microsoft.PowerShell.Archive.CompressArchiveCommand" } } } diff --git a/src/ArchiveAddition.cs b/src/ArchiveAddition.cs index d0e8337..2256488 100644 --- a/src/ArchiveAddition.cs +++ b/src/ArchiveAddition.cs @@ -11,7 +11,7 @@ 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 + public class ArchiveAddition { /// /// The name of the file or directory in the archive. diff --git a/src/IArchive.cs b/src/IArchive.cs index a5d33be..c0e3b77 100644 --- a/src/IArchive.cs +++ b/src/IArchive.cs @@ -15,24 +15,13 @@ internal interface IArchive: IDisposable // Get the fully qualified path of the archive internal string Path { get; } - // Number of entries - internal int NumberOfEntries { 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 AddFileSystemEntry(ArchiveAddition entry); - // Get the entries in the archive. - // Throws an exception if the archive is in create mode. - internal string[] GetEntries(); - internal IEntry? GetNextEntry(); - // Expands an archive to a destination folder. - // Throws an exception if the archive is not in read mode. - internal void Expand(string destinationPath); - // Does the archive have only a top-level directory? internal bool HasTopLevelDirectory(); } diff --git a/src/IEntry.cs b/src/IEntry.cs index 503e52b..13307b7 100644 --- a/src/IEntry.cs +++ b/src/IEntry.cs @@ -6,7 +6,7 @@ namespace Microsoft.PowerShell.Archive { - internal interface IEntry + public interface IEntry { public string Name { get; } diff --git a/src/TarArchive.cs b/src/TarArchive.cs index 070d6dd..9e7ef6a 100644 --- a/src/TarArchive.cs +++ b/src/TarArchive.cs @@ -17,16 +17,20 @@ internal class TarArchive : IArchive private readonly string _path; - private readonly TarWriter _tarWriter; + private TarWriter _tarWriter; + + private TarReader? _tarReader; private readonly FileStream _fileStream; + private FileStream _copyStream; + + private string _copyPath; + ArchiveMode IArchive.Mode => _mode; string IArchive.Path => _path; - int IArchive.NumberOfEntries => throw new NotImplementedException(); - public TarArchive(string path, ArchiveMode mode, FileStream fileStream) { _mode = mode; @@ -37,22 +41,49 @@ public TarArchive(string path, ArchiveMode mode, FileStream fileStream) void IArchive.AddFileSystemEntry(ArchiveAddition entry) { - _tarWriter.WriteEntry(fileName: entry.FileSystemInfo.FullName, entryName: entry.EntryName); - } - - string[] IArchive.GetEntries() - { - throw new NotImplementedException(); + if (_mode == ArchiveMode.Extract) { + throw new ArgumentException("Adding entries to the archive is not supported on Extract mode."); + } + + // If the archive is in Update mode, we want to update the archive by copying it to a new archive + // and then adding the entries to that archive + if (_mode == ArchiveMode.Update) { + + } else { + // If the archive mode is Create, no copy + _tarWriter.WriteEntry(fileName: entry.FileSystemInfo.FullName, entryName: entry.EntryName); + } } IEntry? IArchive.GetNextEntry() { - return null; + // If _tarReader is null, create it + if (_tarReader is null) { + _tarReader = new TarReader(archiveStream: _fileStream, leaveOpen: true); + } + var entry = _tarReader.GetNextEntry(); + if (entry is null) { + return null; + } + // Create and return a TarArchiveEntry, which is a wrapper around entry + return new TarArchiveEntry(entry); } - void IArchive.Expand(string destinationPath) - { - throw new NotImplementedException(); + private void CreateCopyStream() { + // Determine an appropritae and random filenname + string copyName = Path.GetRandomFileName(); + + // Directory of the copy will be the same as the directory of the archive + string directory = Path.GetDirectoryName(_path); + + _copyPath = Path.Combine(directory, copyName); + _copyStream = new FileStream(_copyPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None); + + // Create a tar reader that will read the contents of the archive + _tarReader = new TarReader(_fileStream, leaveOpen: false); + + // Create a tar writer that will write the contents of the archive to the copy + _tarWriter = new TarWriter(_) } protected virtual void Dispose(bool disposing) @@ -83,5 +114,47 @@ bool IArchive.HasTopLevelDirectory() { throw new NotImplementedException(); } + + internal class TarArchiveEntry : IEntry { + + // Underlying object is System.Formats.Tar.TarEntry + private TarEntry _entry; + + private IEntry _objectAsIEntry; + + string IEntry.Name => _entry.Name; + + bool IEntry.IsDirectory => _entry.EntryType == TarEntryType.Directory; + + public TarArchiveEntry(TarEntry entry) + { + _entry = entry; + _objectAsIEntry = this; + } + + void IEntry.ExpandTo(string destinationPath) + { + // If the parent directory does not exist, create it + string? parentDirectory = Path.GetDirectoryName(destinationPath); + if (parentDirectory is not null && !Directory.Exists(parentDirectory)) + { + Directory.CreateDirectory(parentDirectory); + } + + if (_objectAsIEntry.IsDirectory) + { + System.IO.Directory.CreateDirectory(destinationPath); + var lastWriteTime = _entry.ModificationTime; + System.IO.Directory.SetLastWriteTime(destinationPath, lastWriteTime.DateTime); + } else + { + _entry.ExtractToFile(destinationPath, overwrite: false); + } + } + + private void SetFileAttributes(string destinationPath) { + + } + } } } diff --git a/src/ZipArchive.cs b/src/ZipArchive.cs index eae5539..e2a20e7 100644 --- a/src/ZipArchive.cs +++ b/src/ZipArchive.cs @@ -30,8 +30,6 @@ internal class ZipArchive : IArchive string IArchive.Path => _archivePath; - int IArchive.NumberOfEntries => _zipArchive.Entries.Count; - public ZipArchive(string archivePath, ArchiveMode mode, FileStream archiveStream, CompressionLevel compressionLevel) { _disposedValue = false; @@ -100,11 +98,6 @@ void IArchive.AddFileSystemEntry(ArchiveAddition addition) } } - string[] IArchive.GetEntries() - { - throw new NotImplementedException(); - } - IEntry? IArchive.GetNextEntry() { if (_entryIndex < 0) @@ -125,11 +118,6 @@ string[] IArchive.GetEntries() return new ZipArchiveEntry(nextEntry); } - void IArchive.Expand(string destinationPath) - { - throw new NotImplementedException(); - } - private static System.IO.Compression.ZipArchiveMode ConvertToZipArchiveMode(ArchiveMode archiveMode) { switch (archiveMode) @@ -196,6 +184,13 @@ internal class ZipArchiveEntry : IEntry void IEntry.ExpandTo(string destinationPath) { + // If the parent directory does not exist, create it + string? parentDirectory = Path.GetDirectoryName(destinationPath); + if (parentDirectory is not null && !Directory.Exists(parentDirectory)) + { + Directory.CreateDirectory(parentDirectory); + } + // .NET APIs differentiate a file and directory by a terminating `/` // If the entry name ends with `/`, it is a directory if (_entry.FullName.EndsWith(System.IO.Path.AltDirectorySeparatorChar)) @@ -205,12 +200,6 @@ void IEntry.ExpandTo(string destinationPath) System.IO.Directory.SetLastWriteTime(destinationPath, lastWriteTime.DateTime); } else { - // If the parent directory does not exist, create it - string? parentDirectory = Path.GetDirectoryName(destinationPath); - if (parentDirectory is not null && !Directory.Exists(parentDirectory)) - { - Directory.CreateDirectory(parentDirectory); - } _entry.ExtractToFile(destinationPath); } } From cca70eda21a50454994fdc6c99d257b66cdf843a Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Tue, 9 Aug 2022 19:38:40 -0700 Subject: [PATCH 12/34] fixed bug where percent complete was not updating --- src/CompressArchiveCommand.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index e76aea5..74f6f58 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -199,7 +199,8 @@ protected override void EndProcessing() { // Update progress var percentComplete = numberOfAddedItems / (float)numberOfAdditions * 100f; - progressRecord.StatusDescription = string.Format(Messages.ProgressDisplay, "{percentComplete:0.0}"); + progressRecord.StatusDescription = string.Format(Messages.ProgressDisplay, $"{percentComplete:0.0}"); + progressRecord.PercentComplete = (int)percentComplete; WriteProgress(progressRecord); if (ShouldProcess(target: entry.FileSystemInfo.FullName, action: Messages.Add)) @@ -221,6 +222,7 @@ protected override void EndProcessing() // 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"); + progressRecord.PercentComplete = 100; WriteProgress(progressRecord); } finally From 80917c789fe5415729140927c44372e6e2ab3990 Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Wed, 10 Aug 2022 12:11:58 -0700 Subject: [PATCH 13/34] added progress info in Expand-Archive, addded changelog --- CHANGELOG.md | 19 +++++++++++++++++++ src/CompressArchiveCommand.cs | 1 + src/ExpandArchiveCommand.cs | 7 +++++++ src/Localized/Messages.resx | 6 ++++++ src/Microsoft.PowerShell.Archive.psd1 | 16 +++++++++++++++- src/TarArchive.cs | 2 +- 6 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2decfb6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,19 @@ +# Changelog + +## 2.0.1-preview2 + +- Rewrite `Expand-Archive` cmdlet in C# +- Added `-Format` parameter to `Expand-Archive` +- Added `-WriteMode` parameter to `Expand-Archive` +- Added support for zip64 +- Fixed a bug where the entry names of files in a directory would not be correct when compressing an archive + +## 2.0.1-preview1 + +- Rewrite `Compress-Archive` cmdlet in C# +- Added `-Format` parameter to `Compress-Archive` +- Added `-WriteMode` parameter to `Compress-Archive` +- Added support for relative path structure preservating when paths relative to the working directory are specified to `-Path` or `-LiteralPath` in `Compress-Archive` +- Added support for zip64 +- Fixed a bug where empty directories would not be compressed +- Fixed a bug where an abrupt stop when compressing empty directories would not delete the newly created archive diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index 74f6f58..9d040ed 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -199,6 +199,7 @@ protected override void EndProcessing() { // Update progress var percentComplete = numberOfAddedItems / (float)numberOfAdditions * 100f; + progressRecord.StatusDescription = string.Format(Messages.ProgressDisplay, $"{percentComplete:0.0}"); progressRecord.PercentComplete = (int)percentComplete; WriteProgress(progressRecord); diff --git a/src/ExpandArchiveCommand.cs b/src/ExpandArchiveCommand.cs index 510bba6..9664030 100644 --- a/src/ExpandArchiveCommand.cs +++ b/src/ExpandArchiveCommand.cs @@ -115,10 +115,13 @@ protected override void EndProcessing() System.IO.Directory.CreateDirectory(DestinationPath); } + WriteObject(string.Format(Messages.ExpandingArchiveMessage, DestinationPath)); + // Get the next entry in the archive and process it var nextEntry = archive.GetNextEntry(); while (nextEntry != null) { + // The process function will write the progress ProcessArchiveEntry(nextEntry); nextEntry = archive.GetNextEntry(); } @@ -156,6 +159,10 @@ private void ProcessArchiveEntry(IEntry entry) postExpandPath = postExpandPath.Remove(postExpandPath.Length - 1); } + // Notify the user that we are expanding the entry + var expandingEntryMsg = string.Format(Messages.ExpandingEntryMessage, entry.Name, postExpandPath); + WriteObject(expandingEntryMsg); + // If the entry name is invalid, write a non-terminating error and stop processing the entry if (IsPathInvalid(postExpandPath)) { diff --git a/src/Localized/Messages.resx b/src/Localized/Messages.resx index 7d28961..24471bc 100644 --- a/src/Localized/Messages.resx +++ b/src/Localized/Messages.resx @@ -194,4 +194,10 @@ The last write time of the path {0} is before 1980. Since zip does not support dates before January 1, 1980, the last write time will be set to January 1, 1980. + + Expanding archive {0} + + + Expanding archive entry {0} to destination {1} + \ No newline at end of file diff --git a/src/Microsoft.PowerShell.Archive.psd1 b/src/Microsoft.PowerShell.Archive.psd1 index 6dd9f7d..c11f5b0 100644 --- a/src/Microsoft.PowerShell.Archive.psd1 +++ b/src/Microsoft.PowerShell.Archive.psd1 @@ -12,9 +12,23 @@ PrivateData = @{ PSData = @{ Tags = @('Archive', 'Zip', 'Compress') ProjectUri = 'https://github.com/PowerShell/Microsoft.PowerShell.Archive' + LicenseUri = 'https://go.microsoft.com/fwlink/?linkid=2203619' ReleaseNotes = @' + ## 2.0.1-preview2 + - Rewrite `Expand-Archive` cmdlet in C# + - Added `-Format` parameter to `Expand-Archive` + - Added `-WriteMode` parameter to `Expand-Archive` + - Added support for zip64 + - Fixed a bug where the entry names of files in a directory would not be correct when compressing an archive + ## 2.0.1-preview1 - - Rewrote Compress-Archive cmdlet in C# + - Rewrite `Compress-Archive` cmdlet in C# + - Added `-Format` parameter to `Compress-Archive` + - Added `-WriteMode` parameter to `Compress-Archive` + - Added support for relative path structure preservating when paths relative to the working directory are specified to `-Path` or `-LiteralPath` in `Compress-Archive` + - Added support for zip64 + - Fixed a bug where empty directories would not be compressed + - Fixed a bug where an abrupt stop when compressing empty directories would not delete the newly created archive '@ Prerelease = 'preview1' } diff --git a/src/TarArchive.cs b/src/TarArchive.cs index 9e7ef6a..9601591 100644 --- a/src/TarArchive.cs +++ b/src/TarArchive.cs @@ -83,7 +83,7 @@ private void CreateCopyStream() { _tarReader = new TarReader(_fileStream, leaveOpen: false); // Create a tar writer that will write the contents of the archive to the copy - _tarWriter = new TarWriter(_) + //_tarWriter = new TarWriter(_) } protected virtual void Dispose(bool disposing) From 0210c8d44adf392b35a48e4b7d9179eb97dbd7e0 Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Wed, 10 Aug 2022 12:20:33 -0700 Subject: [PATCH 14/34] fixed failing tests --- src/ExpandArchiveCommand.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ExpandArchiveCommand.cs b/src/ExpandArchiveCommand.cs index 9664030..5419ee4 100644 --- a/src/ExpandArchiveCommand.cs +++ b/src/ExpandArchiveCommand.cs @@ -115,7 +115,7 @@ protected override void EndProcessing() System.IO.Directory.CreateDirectory(DestinationPath); } - WriteObject(string.Format(Messages.ExpandingArchiveMessage, DestinationPath)); + //WriteObject(string.Format(Messages.ExpandingArchiveMessage, DestinationPath)); // Get the next entry in the archive and process it var nextEntry = archive.GetNextEntry(); @@ -160,8 +160,8 @@ private void ProcessArchiveEntry(IEntry entry) } // Notify the user that we are expanding the entry - var expandingEntryMsg = string.Format(Messages.ExpandingEntryMessage, entry.Name, postExpandPath); - WriteObject(expandingEntryMsg); + //var expandingEntryMsg = string.Format(Messages.ExpandingEntryMessage, entry.Name, postExpandPath); + //WriteObject(expandingEntryMsg); // If the entry name is invalid, write a non-terminating error and stop processing the entry if (IsPathInvalid(postExpandPath)) From ab31d3e67f8752ff0bb1fc87f086cbed2f286bb8 Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Wed, 10 Aug 2022 12:44:09 -0700 Subject: [PATCH 15/34] fixed a bug where removing the extension on Unix platforms would append a . to the path, added progress bar for zip archive --- src/ExpandArchiveCommand.cs | 34 ++++++++++++++++++++++++++++++++-- src/ZipArchive.cs | 2 ++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/ExpandArchiveCommand.cs b/src/ExpandArchiveCommand.cs index 5419ee4..ec4f4c7 100644 --- a/src/ExpandArchiveCommand.cs +++ b/src/ExpandArchiveCommand.cs @@ -85,7 +85,7 @@ protected override void EndProcessing() try { // Get an archive from source path -- this is where we will switch between different types of archives - using IArchive? archive = ArchiveFactory.GetArchive(format: Format ?? ArchiveFormat.Zip, archivePath: _sourcePath, archiveMode: ArchiveMode.Extract, compressionLevel: System.IO.Compression.CompressionLevel.NoCompression); + using IArchive archive = ArchiveFactory.GetArchive(format: Format ?? ArchiveFormat.Zip, archivePath: _sourcePath, archiveMode: ArchiveMode.Extract, compressionLevel: System.IO.Compression.CompressionLevel.NoCompression); if (DestinationPath is null) { @@ -115,6 +115,16 @@ protected override void EndProcessing() System.IO.Directory.CreateDirectory(DestinationPath); } + long numberOfExpandedItems = 0; + + // Show a progress bar + if (Format == ArchiveFormat.Zip && archive is ZipArchive) { + var statusDescription = string.Format(Messages.ProgressDisplay, "0.0"); + var progressRecord = new ProgressRecord(1, "Expand-Archive", statusDescription); + progressRecord.PercentComplete = 0; + WriteProgress(progressRecord); + } + //WriteObject(string.Format(Messages.ExpandingArchiveMessage, DestinationPath)); // Get the next entry in the archive and process it @@ -124,6 +134,25 @@ protected override void EndProcessing() // The process function will write the progress ProcessArchiveEntry(nextEntry); nextEntry = archive.GetNextEntry(); + + // Update progress info + numberOfExpandedItems++; + if (Format == ArchiveFormat.Zip && archive is not null && archive is ZipArchive zipArchive) { + var percentComplete = numberOfExpandedItems / (float)zipArchive.NumberOfEntries * 100f; + var statusDescription = string.Format(Messages.ProgressDisplay, $"{percentComplete:0.0}"); + var progressRecord = new ProgressRecord(1, "Expand-Archive", statusDescription); + progressRecord.PercentComplete = (int)percentComplete; + WriteProgress(progressRecord); + } + } + + // Show progress as 100% complete + // Show a progress bar + if (Format == ArchiveFormat.Zip && archive is ZipArchive) { + var statusDescription = string.Format(Messages.ProgressDisplay, "100.0"); + var progressRecord = new ProgressRecord(1, "Expand-Archive", statusDescription); + progressRecord.PercentComplete = 100; + WriteProgress(progressRecord); } @@ -315,7 +344,8 @@ private string DetermineDestinationPath(IArchive archive) // If filename does have an exension, remove the extension and set the filename minus extension as destinationDirectory if (System.IO.Path.GetExtension(filename) != string.Empty) { - destinationDirectory = System.IO.Path.ChangeExtension(path: filename, extension: string.Empty); + int indexOfLastPeriod = filename.LastIndexOf('.'); + destinationDirectory = filename.Substring(0, indexOfLastPeriod); } } diff --git a/src/ZipArchive.cs b/src/ZipArchive.cs index e2a20e7..3954831 100644 --- a/src/ZipArchive.cs +++ b/src/ZipArchive.cs @@ -30,6 +30,8 @@ internal class ZipArchive : IArchive string IArchive.Path => _archivePath; + internal int NumberOfEntries => _zipArchive.Entries.Count; + public ZipArchive(string archivePath, ArchiveMode mode, FileStream archiveStream, CompressionLevel compressionLevel) { _disposedValue = false; From bb24f0e83e8bcd747138575802d8e352d4d29fdc Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Wed, 10 Aug 2022 13:01:24 -0700 Subject: [PATCH 16/34] updated release build pipeline --- .azdevops/ReleaseBuildPipeline.yml | 2 +- src/ExpandArchiveCommand.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.azdevops/ReleaseBuildPipeline.yml b/.azdevops/ReleaseBuildPipeline.yml index cf05994..c8bf600 100644 --- a/.azdevops/ReleaseBuildPipeline.yml +++ b/.azdevops/ReleaseBuildPipeline.yml @@ -38,7 +38,7 @@ stages: includePreviewVersions: true - pwsh: | - & $(Build.SourcesDirectory)/Microsoft.PowerShell.Archive/SimpleBuild.ps1 + & $(Build.SourcesDirectory)/Microsoft.PowerShell.Archive/Build.ps1 displayName: Build Microsoft.PowerShell.Archive module - pwsh: | diff --git a/src/ExpandArchiveCommand.cs b/src/ExpandArchiveCommand.cs index ec4f4c7..cc41587 100644 --- a/src/ExpandArchiveCommand.cs +++ b/src/ExpandArchiveCommand.cs @@ -125,7 +125,8 @@ protected override void EndProcessing() WriteProgress(progressRecord); } - //WriteObject(string.Format(Messages.ExpandingArchiveMessage, DestinationPath)); + // Write a verbose message saying "Expanding archive ..." + WriteVerbose(string.Format(Messages.ExpandingArchiveMessage, DestinationPath)); // Get the next entry in the archive and process it var nextEntry = archive.GetNextEntry(); From 28c4754f25cd7c87940e8b07556192d015b5afc3 Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Wed, 10 Aug 2022 13:26:30 -0700 Subject: [PATCH 17/34] added test for seeing if a file can be added to an archive while it is in use, updated prelease version, added exception handling when adding archive entries --- CHANGELOG.md | 1 + Tests/Compress-Archive.Tests.ps1 | 21 ++++++++++++++++ src/CompressArchiveCommand.cs | 36 +++++++++++++++++++++++---- src/ErrorMessages.cs | 4 ++- src/Microsoft.PowerShell.Archive.psd1 | 3 ++- 5 files changed, 58 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2decfb6..6590597 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Added `-WriteMode` parameter to `Expand-Archive` - Added support for zip64 - Fixed a bug where the entry names of files in a directory would not be correct when compressing an archive +- `Compress-Archive` skips writing an entry to an archive if an error occurs while doing so ## 2.0.1-preview1 diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index 5f1387b..844041e 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -776,4 +776,25 @@ BeforeDiscovery { $output | Should -BeNullOrEmpty } } + + Context "File permissions, attributes, etc. tests" { + BeforeAll { + New-Item TestDrive:/file.txt -ItemType File + "Hello, World!" | Out-File -Path TestDrive:/file.txt + } + + + It "Skips archiving a file in use" { + $fileMode = [System.IO.FileMode]::Open + $fileAccess = [System.IO.FileAccess]::Write + $fileShare = [System.IO.FileShare]::None + $archiveInUseStream = New-Object -TypeName "System.IO.FileStream" -ArgumentList "${TestDrive}/file.txt",$fileMode,$fileAccess,$fileShare + + Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive_in_use.zip + # Ensure it creates an empty zip archive + "TestDrive:/archive_in_use.zip" | Should -BeZipArchiveOnlyContaining @() + + $archiveInUseStream.Dispose() + } + } } diff --git a/src/CompressArchiveCommand.cs b/src/CompressArchiveCommand.cs index 9d040ed..dea909f 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/CompressArchiveCommand.cs @@ -207,14 +207,40 @@ protected override void EndProcessing() if (ShouldProcess(target: entry.FileSystemInfo.FullName, action: Messages.Add)) { // Warn the user if the LastWriteTime of the file/directory is before 1980 - if (entry.FileSystemInfo.LastWriteTime.Year < 1980 && Format == ArchiveFormat.Zip) { + if (entry.FileSystemInfo.LastWriteTime.Year < 1980 && Format == ArchiveFormat.Zip) + { WriteWarning(string.Format(Messages.LastWriteTimeBefore1980Warning, entry.FileSystemInfo.FullName)); } - archive?.AddFileSystemEntry(entry); - // Write a verbose message saying this item was added to the archive - var addedItemMessage = string.Format(Messages.AddedItemToArchiveVerboseMessage, entry.FileSystemInfo.FullName); - WriteVerbose(addedItemMessage); + // Use this to track of an exception that occurs when adding an entry to the archive + // so a non-terminating error can be reported + Exception? exception = null; + try + { + archive?.AddFileSystemEntry(entry); + // Write a verbose message saying this item was added to the archive + var addedItemMessage = string.Format(Messages.AddedItemToArchiveVerboseMessage, entry.FileSystemInfo.FullName); + WriteVerbose(addedItemMessage); + } + // This catches PathTooLongException as well + catch (IOException ioException) + { + exception = ioException; + } + catch (UnauthorizedAccessException unauthorizedAccessException) + { + exception = unauthorizedAccessException; + } + catch (System.NotSupportedException notSupportedException) + { + exception = notSupportedException; + } + + if (exception is not null) + { + var errorRecord = new ErrorRecord(exception, nameof(ErrorCode.ExceptionOccuredWhileAddingEntry), ErrorCategory.InvalidOperation, entry.EntryName); + WriteError(errorRecord); + } } // Keep track of number of items added to the archive numberOfAddedItems++; diff --git a/src/ErrorMessages.cs b/src/ErrorMessages.cs index 810141e..d79ab66 100644 --- a/src/ErrorMessages.cs +++ b/src/ErrorMessages.cs @@ -79,6 +79,8 @@ internal enum ErrorCode // Expand-Archive: used when a path resolved to multiple paths when only one was needed PathResolvedToMultiplePaths, // Expand-Archive: used when the DestinationPath could not be determined - CannotDetermineDestinationPath + CannotDetermineDestinationPath, + // Compress-Archive: Used when an exception occurs when adding an entry to an archive + ExceptionOccuredWhileAddingEntry } } diff --git a/src/Microsoft.PowerShell.Archive.psd1 b/src/Microsoft.PowerShell.Archive.psd1 index c11f5b0..0861e0e 100644 --- a/src/Microsoft.PowerShell.Archive.psd1 +++ b/src/Microsoft.PowerShell.Archive.psd1 @@ -20,6 +20,7 @@ PrivateData = @{ - Added `-WriteMode` parameter to `Expand-Archive` - Added support for zip64 - Fixed a bug where the entry names of files in a directory would not be correct when compressing an archive + - `Compress-Archive` skips writing an entry to an archive if an error occurs while doing so ## 2.0.1-preview1 - Rewrite `Compress-Archive` cmdlet in C# @@ -30,6 +31,6 @@ PrivateData = @{ - Fixed a bug where empty directories would not be compressed - Fixed a bug where an abrupt stop when compressing empty directories would not delete the newly created archive '@ - Prerelease = 'preview1' + Prerelease = 'preview2' } } \ No newline at end of file From 13e65509dfea6393fea116418937ede1abcc626e Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Wed, 10 Aug 2022 14:45:59 -0700 Subject: [PATCH 18/34] updated sign and package script to use prelease version from manifest --- .azdevops/SignAndPackageModule.ps1 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.azdevops/SignAndPackageModule.ps1 b/.azdevops/SignAndPackageModule.ps1 index 06427ac..a57be44 100644 --- a/.azdevops/SignAndPackageModule.ps1 +++ b/.azdevops/SignAndPackageModule.ps1 @@ -12,6 +12,7 @@ $BuildOutputDir = Join-Path $root "\src\bin\Release" $ManifestPath = "${BuildOutputDir}\${Name}.psd1" $ManifestData = Import-PowerShellDataFile -Path $ManifestPath $Version = $ManifestData.ModuleVersion +$Prelease = $ManifestPath.PrivateData.PSData.Prerelease # 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 @@ -31,7 +32,9 @@ function Export-Module 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} + $nupkgName = "{0}.{1}-{2}.nupkg" -f ${Name},${Version},${Prerelease} + + $nupkgPath = Join-Path $packageRoot $nupkgName if ($env:TF_BUILD) { # In Azure DevOps From 538bd726fb0bd106eafe907f8a1dee590b67c3fb Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Wed, 10 Aug 2022 15:02:08 -0700 Subject: [PATCH 19/34] fixed typo in sign package script --- .azdevops/SignAndPackageModule.ps1 | 2 +- Tests/Compress-Archive.Tests.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.azdevops/SignAndPackageModule.ps1 b/.azdevops/SignAndPackageModule.ps1 index a57be44..ed9561c 100644 --- a/.azdevops/SignAndPackageModule.ps1 +++ b/.azdevops/SignAndPackageModule.ps1 @@ -12,7 +12,7 @@ $BuildOutputDir = Join-Path $root "\src\bin\Release" $ManifestPath = "${BuildOutputDir}\${Name}.psd1" $ManifestData = Import-PowerShellDataFile -Path $ManifestPath $Version = $ManifestData.ModuleVersion -$Prelease = $ManifestPath.PrivateData.PSData.Prerelease +$Prerelease = $ManifestPath.PrivateData.PSData.Prerelease # 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 diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index 844041e..1e4a62e 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -790,7 +790,7 @@ BeforeDiscovery { $fileShare = [System.IO.FileShare]::None $archiveInUseStream = New-Object -TypeName "System.IO.FileStream" -ArgumentList "${TestDrive}/file.txt",$fileMode,$fileAccess,$fileShare - Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive_in_use.zip + Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive_in_use.zip -ErrorAction SilentlyContinue # Ensure it creates an empty zip archive "TestDrive:/archive_in_use.zip" | Should -BeZipArchiveOnlyContaining @() From fea4d2e95dda443fb04ed634bd0ae4c035d5b109 Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Wed, 10 Aug 2022 15:20:10 -0700 Subject: [PATCH 20/34] added test, fixed an error in sign and package script --- .azdevops/SignAndPackageModule.ps1 | 4 +-- Tests/Compress-Archive.Tests.ps1 | 50 ++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/.azdevops/SignAndPackageModule.ps1 b/.azdevops/SignAndPackageModule.ps1 index ed9561c..5d0db53 100644 --- a/.azdevops/SignAndPackageModule.ps1 +++ b/.azdevops/SignAndPackageModule.ps1 @@ -12,7 +12,7 @@ $BuildOutputDir = Join-Path $root "\src\bin\Release" $ManifestPath = "${BuildOutputDir}\${Name}.psd1" $ManifestData = Import-PowerShellDataFile -Path $ManifestPath $Version = $ManifestData.ModuleVersion -$Prerelease = $ManifestPath.PrivateData.PSData.Prerelease +#$Prerelease = $ManifestPath.PrivateData.PSData.Prerelease # 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 @@ -32,7 +32,7 @@ function Export-Module Publish-Module -Path $packageRoot -Repository $repoName Unregister-PSRepository -Name $repoName Get-ChildItem -Recurse -Name $packageRoot | Write-Verbose - $nupkgName = "{0}.{1}-{2}.nupkg" -f ${Name},${Version},${Prerelease} + $nupkgName = "{0}.{1}-preview2.nupkg" -f ${Name},${Version} $nupkgPath = Join-Path $packageRoot $nupkgName diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index 1e4a62e..8efdfb8 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -797,4 +797,54 @@ BeforeDiscovery { $archiveInUseStream.Dispose() } } + + Context "Long path tests" { + BeforeAll { + if ($IsWindows) { + $maxPathLength = 260 + } + if ($IsLinux) { + $maxPathLength = 255 + } + if ($IsMacOS) { + $maxPathLength = 1024 + } + + function Get-MaxLengthPath { + param ( + [string] $character + ) + + $path = "${TestDrive}/" + while ($path.Length -le $maxPathLength + 2) { + $path += $character + } + return $path + } + + New-Item -Path "TestDrive:/file.txt" -ItemType File + "Hello, World!" | Out-File -FilePath "TestDrive:/file.txt" + } + + + It "Throws an error when -Path is too long" { + + } + + It "Throws an error when -LiteralPath is too long" { + + } + + It "Throws an error when -DestinationPath is too long" { + $path = "TestDrive:/file.txt" + # This will generate a path like TestDrive:/aaaaaa...aaaaaa + $destinationPath = Get-MaxLengthPath -character a + Write-Warning $destinationPath.Length + try { + Compress-Archive -Path $path -DestinationPath $destinationPath + } catch { + throw "${$_.Exception}" + } + } + } } From 2e977d1e35744d6a827b9b977c477736fbc80853 Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Wed, 10 Aug 2022 17:28:09 -0700 Subject: [PATCH 21/34] worked on tar support and added test case and assertion for tar --- .../Should-BeArchiveOnlyContaining.psm1 | 34 ++++ .../Should-BeTarArchiveOnlyContaining.psm1 | 148 +++++++++++++++ Tests/Compress-Archive.Tests.ps1 | 171 +++++++++--------- src/ArchiveFactory.cs | 7 +- src/ArchiveFormat.cs | 3 +- src/TarArchive.cs | 66 +++++-- 6 files changed, 324 insertions(+), 105 deletions(-) create mode 100644 Tests/Assertions/Should-BeArchiveOnlyContaining.psm1 create mode 100644 Tests/Assertions/Should-BeTarArchiveOnlyContaining.psm1 diff --git a/Tests/Assertions/Should-BeArchiveOnlyContaining.psm1 b/Tests/Assertions/Should-BeArchiveOnlyContaining.psm1 new file mode 100644 index 0000000..6df4399 --- /dev/null +++ b/Tests/Assertions/Should-BeArchiveOnlyContaining.psm1 @@ -0,0 +1,34 @@ +function Should-BeArchiveOnlyContaining { + <# + .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, + [string] $Format + ) + + if ($Format -eq "Zip") { + return Should-BeZipArchiveOnlyContaining -ActualValue $ActualValue -ExpectedValue $ExpectedValue -Negate:$Negate -Because $Because -LiteralPath:$LiteralPath -CallerSessionState $CallerSessionState + } + if ($Format -eq "Tar") { + return Should-BeTarArchiveOnlyContaining -ActualValue $ActualValue -ExpectedValue $ExpectedValue -Negate:$Negate -Because $Because -LiteralPath:$LiteralPath -CallerSessionState $CallerSessionState + } + return return [pscustomobject]@{ + Succeeded = $false + FailureMessage = "Format ${Format} is not supported." + } + +} +Add-ShouldOperator -Name BeArchiveOnlyContaining -InternalName 'Should-BeArchiveOnlyContaining' -Test ${function:Should-BeArchiveOnlyContaining} \ No newline at end of file diff --git a/Tests/Assertions/Should-BeTarArchiveOnlyContaining.psm1 b/Tests/Assertions/Should-BeTarArchiveOnlyContaining.psm1 new file mode 100644 index 0000000..5c7fc91 --- /dev/null +++ b/Tests/Assertions/Should-BeTarArchiveOnlyContaining.psm1 @@ -0,0 +1,148 @@ +function Should-BeTarArchiveOnlyContaining { + <# + .SYNOPSIS + Checks if a tar 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 + if ($IsWindows) { + $output = 7z.exe l $ActualValue -ba -ttar + } else { + $output = 7z l $ActualValue -ba -ttar + } + + # 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 BeTarArchiveOnlyContaining -InternalName 'Should-BeTarArchiveOnlyContaining' -Test ${function:Should-BeTarArchiveOnlyContaining} \ No newline at end of file diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index 8efdfb8..257b3ac 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -4,6 +4,8 @@ BeforeDiscovery { # Loads and registers custom assertion. Ignores usage of unapproved verb with -DisableNameChecking Import-Module "$PSScriptRoot/Assertions/Should-BeZipArchiveOnlyContaining.psm1" -DisableNameChecking + Import-Module "$PSScriptRoot/Assertions/Should-BeTarArchiveOnlyContaining.psm1" -DisableNameChecking + Import-Module "$PSScriptRoot/Assertions/Should-BeArchiveOnlyContaining.psm1" -DisableNameChecking } Describe("Microsoft.PowerShell.Archive tests") { @@ -12,6 +14,21 @@ BeforeDiscovery { $originalProgressPref = $ProgressPreference $ProgressPreference = "SilentlyContinue" $originalPSModulePath = $env:PSModulePath + + function Add-FileExtensionBasedOnFormat { + Param ( + [string] $Path, + [string] $Format + ) + + if ($Format -eq "Zip") { + return $Path += ".zip" + } + if ($Format -eq "Tar") { + return $Path += ".tar" + } + throw "Format type is not supported" + } } AfterAll { @@ -272,7 +289,10 @@ BeforeDiscovery { } } - Context "Basic functional tests" { + Context "Basic functional tests" -ForEach @( + @{Format = "Zip"}, + @{Format = "Tar"} + ) { BeforeAll { New-Item TestDrive:/SourceDir -Type Directory | Out-Null New-Item TestDrive:/SourceDir/ChildDir-1 -Type Directory | Out-Null @@ -301,93 +321,82 @@ BeforeDiscovery { Set-ItemProperty -Path "TestDrive:/olddirectory" -Name "LastWriteTime" -Value '1974-01-16 14:44' } - It "Compresses a single file" { - $sourcePath = "$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-2.txt" - $destinationPath = "$TestDrive$($DS)archive1.zip" - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - $destinationPath | Should -BeZipArchiveOnlyContaining @('Sample-2.txt') + It "Compresses a single file with format " { + $sourcePath = "TestDrive:/SourceDir/ChildDir-1/Sample-2.txt" + $destinationPath = Add-FileExtensionBasedOnFormat -Path "TestDrive:/archive1" -Format $Format + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format + $destinationPath | Should -BeArchiveOnlyContaining @('Sample-2.txt') -Format $Format } - It "Compresses a non-empty directory" { - $sourcePath = "$TestDrive$($DS)SourceDir$($DS)ChildDir-1" - $destinationPath = "$TestDrive$($DS)archive2.zip" - - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - $destinationPath | Should -Exist - Test-ZipArchive $destinationPath @('ChildDir-1/', 'ChildDir-1/Sample-2.txt') + It "Compresses a non-empty directory with format " -Tag td1 { + $sourcePath = "TestDrive:/SourceDir/ChildDir-1" + $destinationPath = Add-FileExtensionBasedOnFormat -Path "TestDrive:/archive2" -Format $Format + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format + $destinationPath | Should -BeArchiveOnlyContaining @('ChildDir-1/', 'ChildDir-1/Sample-2.txt') -Format $Format } - It "Compresses an empty directory" { - $sourcePath = "$TestDrive$($DS)EmptyDir" - $destinationPath = "$TestDrive$($DS)archive3.zip" - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - $destinationPath | Should -BeZipArchiveOnlyContaining @('EmptyDir/') + It "Compresses an empty directory with format " { + $sourcePath = "TestDrive:/EmptyDir" + $destinationPath = Add-FileExtensionBasedOnFormat -Path "TestDrive:/archive3" -Format $Format + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format + $destinationPath | Should -BeArchiveOnlyContaining @('EmptyDir/') -Format $Format } - It "Compresses multiple files" { - $sourcePath = @("$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-2.txt", "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt") - $destinationPath = "$TestDrive$($DS)archive4.zip" - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - $destinationPath | Should -Exist - Test-ZipArchive $destinationPath @('Sample-1.txt', 'Sample-2.txt') + It "Compresses multiple files with format " { + $sourcePath = @("TestDrive:/SourceDir/ChildDir-1/Sample-2.txt", "TestDrive:/SourceDir/Sample-1.txt") + $destinationPath = Add-FileExtensionBasedOnFormat "TestDrive:/archive4" -Format $Format + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format + $destinationPath | Should -BeArchiveOnlyContaining @('Sample-1.txt', 'Sample-2.txt') -Format $Format } - It "Compresses multiple files and a single empty directory" { - $sourcePath = @("$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-2.txt", "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt", - "$TestDrive$($DS)SourceDir$($DS)ChildEmptyDir") - - $destinationPath = "$TestDrive$($DS)archive5.zip" - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - $destinationPath | Should -Exist - Test-ZipArchive $destinationPath @('Sample-1.txt', 'Sample-2.txt', 'ChildEmptyDir/') + It "Compresses multiple files and a single empty directory with format " { + $sourcePath = @("TestDrive:/SourceDir/ChildDir-1/Sample-2.txt", "TestDrive:/SourceDir/Sample-1.txt", + "TestDrive:/SourceDir/ChildEmptyDir") + $destinationPath = Add-FileExtensionBasedOnFormat "TestDrive:/archive5" -Format $Format + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format + $destinationPath | Should -BeArchiveOnlyContaining @('Sample-1.txt', 'Sample-2.txt', 'ChildEmptyDir/') -Format $Format } - It "Compresses multiple files and a single non-empty directory" { - $sourcePath = @("$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-2.txt", "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt", - "$TestDrive$($DS)SourceDir$($DS)ChildDir-2") - - $destinationPath = "$TestDrive$($DS)archive6.zip" - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - $destinationPath | Should -Exist - Test-ZipArchive $destinationPath @('Sample-1.txt', 'Sample-2.txt', 'ChildDir-2/', 'ChildDir-2/Sample-3.txt') + It "Compresses multiple files and a single non-empty directory with format " { + $sourcePath = @("TestDrive:/SourceDir/ChildDir-1/Sample-2.txt", "TestDrive:/SourceDir/Sample-1.txt", + "TestDrive:/SourceDir/ChildDir-2") + $destinationPath = Add-FileExtensionBasedOnFormat "TestDrive:/archive6.zip" -Format $Format + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format + $destinationPath | Should -BeArchiveOnlyContaining @('Sample-1.txt', 'Sample-2.txt', 'ChildDir-2/', 'ChildDir-2/Sample-3.txt') -Format $Format } - It "Compresses multiple files and non-empty directories" { - $sourcePath = @("$TestDrive$($DS)HelloWorld.txt", "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt", - "$TestDrive$($DS)SourceDir$($DS)ChildDir-1", "$TestDrive$($DS)SourceDir$($DS)ChildDir-2") - - $destinationPath = "$TestDrive$($DS)archive7.zip" - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - $destinationPath | Should -Exist - Test-ZipArchive $destinationPath @('Sample-1.txt', 'HelloWorld.txt', 'ChildDir-1/', 'ChildDir-2/', - 'ChildDir-1/Sample-2.txt', 'ChildDir-2/Sample-3.txt') + It "Compresses multiple files and non-empty directories with format " { + $sourcePath = @("TestDrive:/HelloWorld.txt", "TestDrive:/SourceDir/Sample-1.txt", + "TestDrive:/SourceDir/ChildDir-1", "TestDrive:/SourceDir/ChildDir-2") + $destinationPath = Add-FileExtensionBasedOnFormat "TestDrive:/archive7.zip" -Format $Format + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format + $destinationPath | Should -BeArchiveOnlyContaining @('Sample-1.txt', 'HelloWorld.txt', 'ChildDir-1/', 'ChildDir-2/', + 'ChildDir-1/Sample-2.txt', 'ChildDir-2/Sample-3.txt') -Format $Format } - It "Compresses multiple files, non-empty directories, and an empty directory" { - $sourcePath = @("$TestDrive$($DS)HelloWorld.txt", "$TestDrive$($DS)SourceDir$($DS)Sample-1.txt", - "$TestDrive$($DS)SourceDir$($DS)ChildDir-1", "$TestDrive$($DS)SourceDir$($DS)ChildDir-2", "$TestDrive$($DS)SourceDir$($DS)ChildEmptyDir") - - $destinationPath = "$TestDrive$($DS)archive8.zip" - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - $destinationPath | Should -Exist - Test-ZipArchive $destinationPath @('Sample-1.txt', 'HelloWorld.txt', 'ChildDir-1/', 'ChildDir-2/', - 'ChildDir-1/Sample-2.txt', 'ChildDir-2/Sample-3.txt', "ChildEmptyDir/") + It "Compresses multiple files, non-empty directories, and an empty directory with format " { + $sourcePath = @("TestDrive:/HelloWorld.txt", "TestDrive:/SourceDir/Sample-1.txt", + "TestDrive:/SourceDir/ChildDir-1", "TestDrive:/SourceDir/ChildDir-2", "TestDrive:/SourceDir/ChildEmptyDir") + $destinationPath = Add-FileExtensionBasedOnFormat "TestDrive:/archive8.zip" -Format $Format + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format + $destinationPath | Should -BeArchiveOnlyContaining @('Sample-1.txt', 'HelloWorld.txt', 'ChildDir-1/', 'ChildDir-2/', + 'ChildDir-1/Sample-2.txt', 'ChildDir-2/Sample-3.txt', "ChildEmptyDir/") -Format $Format } - It "Compresses a directory containing files, non-empty directories, and an empty directory can be compressed" -Tag td4 { - $sourcePath = "$TestDrive$($DS)SourceDir" - $destinationPath = "$TestDrive$($DS)archive9.zip" - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - $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') + It "Compresses a directory containing files, non-empty directories, and an empty directory can be compressed with format " { + $sourcePath = "TestDrive:/SourceDir" + $destinationPath = Add-FileExtensionBasedOnFormat "TestDrive:/archive9.zip" -Format $Format + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format + $contents = @('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') + $destinationPath | Should -BeArchiveOnlyContaining $contents -Format $Format } - It "Compresses a zero-byte file" { - $sourcePath = "$TestDrive$($DS)EmptyFile" - $destinationPath = "$TestDrive$($DS)archive10.zip" - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - $destinationPath | Should -Exist - $contents = @('EmptyFile') - Test-ZipArchive $destinationPath $contents + It "Compresses a zero-byte file with format " { + $sourcePath = "TestDrive:/EmptyFile" + $destinationPath = Add-FileExtensionBasedOnFormat "TestDrive:/archive10.zip" -Format $Format + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format + $destinationPath | Should -BeArchiveOnlyContaining @('EmptyFile') -Format $Format } It "Compresses a file whose last write time is before 1980" { @@ -423,7 +432,7 @@ BeforeDiscovery { $archiveStream.Dispose() } - It "Compresses a directory whose last write time is before 1980" { + It "Compresses a directory whose last write time is before 1980 with format " { $sourcePath = "TestDrive:/olddirectory" $destinationPath = "${TestDrive}/archive12.zip" @@ -452,7 +461,7 @@ BeforeDiscovery { $archiveStream.Dispose() } - It "Writes a warning when compressing a file whose last write time is before 1980" { + It "Writes a warning when compressing a file whose last write time is before 1980 with format " { $sourcePath = "TestDrive:/OldFile.txt" $destinationPath = "${TestDrive}/archive13.zip" @@ -464,7 +473,7 @@ BeforeDiscovery { $warnings.Length | Should -Be 1 } - It "Writes a warning when compresing a directory whose last write time is before 1980" { + It "Writes a warning when compresing a directory whose last write time is before 1980 with format " { $sourcePath = "TestDrive:/olddirectory" $destinationPath = "${TestDrive}/archive14.zip" @@ -477,10 +486,6 @@ BeforeDiscovery { } } - Context "Update tests" -Skip { - - } - Context "DestinationPath and -WriteMode Overwrite tests" { BeforeAll { New-Item TestDrive:/SourceDir -Type Directory | Out-Null @@ -682,7 +687,7 @@ BeforeDiscovery { } # From 596 - It "Validate that relative path can be specified as DestinationPath parameter of Compress-Archive cmdlet" -Tag this3 { + It "Validate that relative path can be specified as DestinationPath parameter of Compress-Archive cmdlet" { $sourcePath = "TestDrive:/SourceDir" $destinationPath = "./RelativePathForDestinationPathParameter.zip" try @@ -798,10 +803,11 @@ BeforeDiscovery { } } - Context "Long path tests" { + # This can be difficult to test + Context "Long path tests" -Skip { BeforeAll { if ($IsWindows) { - $maxPathLength = 260 + $maxPathLength = 300 } if ($IsLinux) { $maxPathLength = 255 @@ -816,7 +822,7 @@ BeforeDiscovery { ) $path = "${TestDrive}/" - while ($path.Length -le $maxPathLength + 2) { + while ($path.Length -le $maxPathLength + 10) { $path += $character } return $path @@ -841,10 +847,11 @@ BeforeDiscovery { $destinationPath = Get-MaxLengthPath -character a Write-Warning $destinationPath.Length try { - Compress-Archive -Path $path -DestinationPath $destinationPath + Compress-Archive -Path $path -DestinationPath $destinationPath -ErrorVariable err } catch { throw "${$_.Exception}" } + $destinationPath | Should -Not -Exist } } } diff --git a/src/ArchiveFactory.cs b/src/ArchiveFactory.cs index fd0e636..dacf713 100644 --- a/src/ArchiveFactory.cs +++ b/src/ArchiveFactory.cs @@ -21,7 +21,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 Tar.gz here _ => throw new ArgumentOutOfRangeException(nameof(archiveMode)) }; @@ -32,9 +32,8 @@ internal static bool TryGetArchiveFormatFromExtension(string path, out ArchiveFo archiveFormat = Path.GetExtension(path).ToLowerInvariant() switch { ".zip" => ArchiveFormat.Zip, - /* Disable support for tar and tar.gz for preview1 release - ".gz" => path.EndsWith(".tar.gz) ? ArchiveFormat.Tgz : null, - */ + ".tar" => ArchiveFormat.Tar, + ".gz" => path.EndsWith(".tar.gz") ? ArchiveFormat.Tgz : null, _ => null }; return archiveFormat is not null; diff --git a/src/ArchiveFormat.cs b/src/ArchiveFormat.cs index 5ee6fef..f8032a9 100644 --- a/src/ArchiveFormat.cs +++ b/src/ArchiveFormat.cs @@ -10,8 +10,7 @@ namespace Microsoft.PowerShell.Archive public enum ArchiveFormat { Zip, - /* Removing these formats for preview relase Tar, - Tgz*/ + Tgz } } diff --git a/src/TarArchive.cs b/src/TarArchive.cs index 9601591..56828dc 100644 --- a/src/TarArchive.cs +++ b/src/TarArchive.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Formats.Tar; using System.IO; -using System.Text; +using System.Diagnostics; namespace Microsoft.PowerShell.Archive { @@ -17,15 +17,15 @@ internal class TarArchive : IArchive private readonly string _path; - private TarWriter _tarWriter; + private TarWriter? _tarWriter; private TarReader? _tarReader; private readonly FileStream _fileStream; - private FileStream _copyStream; + private FileStream? _copyStream; - private string _copyPath; + private string? _copyPath; ArchiveMode IArchive.Mode => _mode; @@ -35,34 +35,43 @@ public TarArchive(string path, ArchiveMode mode, FileStream fileStream) { _mode = mode; _path = path; - _tarWriter = new TarWriter(archiveStream: fileStream, format: TarEntryFormat.Pax, leaveOpen: false); _fileStream = fileStream; } void IArchive.AddFileSystemEntry(ArchiveAddition entry) { - if (_mode == ArchiveMode.Extract) { + if (_mode == ArchiveMode.Extract) + { throw new ArgumentException("Adding entries to the archive is not supported on Extract mode."); } // If the archive is in Update mode, we want to update the archive by copying it to a new archive // and then adding the entries to that archive - if (_mode == ArchiveMode.Update) { - - } else { - // If the archive mode is Create, no copy - _tarWriter.WriteEntry(fileName: entry.FileSystemInfo.FullName, entryName: entry.EntryName); - } + if (_mode == ArchiveMode.Update) + { + if (_copyStream is null) + { + CreateCopyStream(); + } + } + else + { + _tarWriter = new TarWriter(_fileStream, TarEntryFormat.Pax, true); + } + Debug.Assert(_tarWriter is not null); + _tarWriter.WriteEntry(fileName: entry.FileSystemInfo.FullName, entryName: entry.EntryName); } IEntry? IArchive.GetNextEntry() { // If _tarReader is null, create it - if (_tarReader is null) { + if (_tarReader is null) + { _tarReader = new TarReader(archiveStream: _fileStream, leaveOpen: true); } var entry = _tarReader.GetNextEntry(); - if (entry is null) { + if (entry is null) + { return null; } // Create and return a TarArchiveEntry, which is a wrapper around entry @@ -74,7 +83,8 @@ private void CreateCopyStream() { string copyName = Path.GetRandomFileName(); // Directory of the copy will be the same as the directory of the archive - string directory = Path.GetDirectoryName(_path); + string? directory = Path.GetDirectoryName(_path); + Debug.Assert(directory is not null); _copyPath = Path.Combine(directory, copyName); _copyStream = new FileStream(_copyPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None); @@ -83,7 +93,22 @@ private void CreateCopyStream() { _tarReader = new TarReader(_fileStream, leaveOpen: false); // Create a tar writer that will write the contents of the archive to the copy - //_tarWriter = new TarWriter(_) + _tarWriter = new TarWriter(_copyStream, TarEntryFormat.Pax, true); + + var entry = _tarReader.GetNextEntry(); + while (entry is not null) + { + _tarWriter.WriteEntry(entry); + entry = _tarReader.GetNextEntry(); + } + } + + private void ReplaceArchiveWithCopy() { + Debug.Assert(_copyPath is not null); + // Delete the archive + File.Delete(_path); + // Move copy to archive path + File.Move(_copyPath, _path); } protected virtual void Dispose(bool disposing) @@ -93,8 +118,15 @@ protected virtual void Dispose(bool disposing) if (disposing) { // TODO: dispose managed state (managed objects) - _tarWriter.Dispose(); + _tarWriter?.Dispose(); + _copyStream?.Dispose(); + _tarReader?.Dispose(); _fileStream.Dispose(); + + if (_mode == ArchiveMode.Update) + { + ReplaceArchiveWithCopy(); + } } // TODO: free unmanaged resources (unmanaged objects) and override finalizer From a7f0ee23de7be7a6132a2c794cd806dd985674e8 Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Thu, 11 Aug 2022 12:10:22 -0700 Subject: [PATCH 22/34] worked on tar support, added support for determining whether an archive has a top-level directory, added tests for tar --- Tests/Compress-Archive.Tests.ps1 | 24 +++++++++++- Tests/Expand-Archive.Tests.ps1 | 67 +++++++++++++++++++++----------- src/TarArchive.cs | 42 +++++++++++++++++--- 3 files changed, 104 insertions(+), 29 deletions(-) diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index 257b3ac..807c6a4 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -310,7 +310,7 @@ BeforeDiscovery { "Hello, World!" | Out-File -FilePath $TestDrive$($DS)HelloWorld.txt # Create a zero-byte file - New-Item $TestDrive$($DS)EmptyFile -Type File | Out-Null + New-Item TestDrive:/EmptyFile -Type File | Out-Null # Create a file whose last write time is before 1980 $content | Out-File -FilePath TestDrive:/OldFile.txt @@ -398,6 +398,18 @@ BeforeDiscovery { Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format $destinationPath | Should -BeArchiveOnlyContaining @('EmptyFile') -Format $Format } + } + + Context "Zip-specific tests" { + BeforeAll { + # Create a file whose last write time is before 1980 + $content | Out-File -FilePath TestDrive:/OldFile.txt + Set-ItemProperty -Path TestDrive:/OldFile.txt -Name LastWriteTime -Value '1974-01-16 14:44' + + # Create a directory whose last write time is before 1980 + New-Item -Path "TestDrive:/olddirectory" -ItemType Directory + Set-ItemProperty -Path "TestDrive:/olddirectory" -Name "LastWriteTime" -Value '1974-01-16 14:44' + } It "Compresses a file whose last write time is before 1980" { $sourcePath = "$TestDrive$($DS)OldFile.txt" @@ -786,6 +798,10 @@ BeforeDiscovery { BeforeAll { New-Item TestDrive:/file.txt -ItemType File "Hello, World!" | Out-File -Path TestDrive:/file.txt + + # Create a read-only file + New-Item TestDrive:/readonly.txt -ItemType File + "Hello, World!" | Out-File -Path TestDrive:/readonly.txt } @@ -801,6 +817,12 @@ BeforeDiscovery { $archiveInUseStream.Dispose() } + + It "Compresses a read-only file" { + $destinationPath = "TestDrive:/archive_with_readonly_file.zip" + Compress-Archive -Path TestDrive:/readonly.txt -DestinationPath $destinationPath + $destinationPath | Should -BeArchiveOnlyContaining @("readonly.txt") -Format Zip + } } # This can be difficult to test diff --git a/Tests/Expand-Archive.Tests.ps1 b/Tests/Expand-Archive.Tests.ps1 index fc8a636..5d4d65a 100644 --- a/Tests/Expand-Archive.Tests.ps1 +++ b/Tests/Expand-Archive.Tests.ps1 @@ -7,6 +7,21 @@ Describe("Expand-Archive Tests") { # Progress perference $originalProgressPref = $ProgressPreference $ProgressPreference = "SilentlyContinue" + + function Add-FileExtensionBasedOnFormat { + Param ( + [string] $Path, + [string] $Format + ) + + if ($Format -eq "Zip") { + return $Path += ".zip" + } + if ($Format -eq "Tar") { + return $Path += ".tar" + } + throw "Format type is not supported" + } } AfterAll { @@ -335,7 +350,10 @@ Describe("Expand-Archive Tests") { } } - Context "Basic functionality tests" { + Context "Basic functionality tests" -ForEach @( + @{Format = "Zip"}, + @{Format = "Tar"} + ) { # extract to a directory works # extract to working directory works when DestinationPath is specified # expand archive works when -DestinationPath is not specified (and a single top level item which is a directory) @@ -344,7 +362,7 @@ Describe("Expand-Archive Tests") { BeforeAll { New-Item -Path "TestDrive:/file1.txt" -ItemType File "Hello, World!" | Out-File -FilePath "TestDrive:/file1.txt" - Compress-Archive -Path "TestDrive:/file1.txt" -DestinationPath "TestDrive:/archive1.zip" + Compress-Archive -Path "TestDrive:/file1.txt" -DestinationPath (Add-FileExtensionBasedOnFormat "TestDrive:/archive1" -Format $Format) New-Item -Path "TestDrive:/directory2" -ItemType Directory New-Item -Path "TestDrive:/directory3" -ItemType Directory @@ -353,25 +371,25 @@ Describe("Expand-Archive Tests") { New-Item -Path "TestDrive:/directory6" -ItemType Directory New-Item -Path "TestDrive:/DirectoryToArchive" -ItemType Directory - Compress-Archive -Path "TestDrive:/DirectoryToArchive" -DestinationPath "TestDrive:/archive2.zip" + Compress-Archive -Path "TestDrive:/DirectoryToArchive" -DestinationPath (Add-FileExtensionBasedOnFormat "TestDrive:/archive2" -Format $Format) # Create an archive containing a file and an empty folder - Compress-Archive -Path "TestDrive:/file1.txt","TestDrive:/DirectoryToArchive" -DestinationPath "TestDrive:/archive3.zip" + Compress-Archive -Path "TestDrive:/file1.txt","TestDrive:/DirectoryToArchive" -DestinationPath (Add-FileExtensionBasedOnFormat "TestDrive:/archive3" -Format $Format) } - It "Expands an archive when a non-existent directory is specified as -DestinationPath" { - $sourcePath = "TestDrive:/archive1.zip" + It "Expands an archive when a non-existent directory is specified as -DestinationPath with format " { + $sourcePath = Add-FileExtensionBasedOnFormat "TestDrive:/archive1" -Format $Format $destinationPath = "TestDrive:/directory1" - Expand-Archive -Path $sourcePath -DestinationPath $destinationPath + Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format $itemsInDestinationPath = Get-ChildItem $destinationPath -Recurse $itemsInDestinationPath.Count | Should -Be 1 $itemsInDestinationPath[0].Name | Should -Be "file1.txt" } - It "Expands an archive when DestinationPath is an existing directory" -Tag debug3 { - $sourcePath = "TestDrive:/archive1.zip" + It "Expands an archive when DestinationPath is an existing directory" { + $sourcePath = Add-FileExtensionBasedOnFormat "TestDrive:/archive1" -Format $Format $destinationPath = "TestDrive:/directory2" try { @@ -382,7 +400,7 @@ Describe("Expand-Archive Tests") { } It "Expands an archive to the working directory when it is specified as -DestinationPath" { - $sourcePath = "TestDrive:/archive1.zip" + $sourcePath = Add-FileExtensionBasedOnFormat "TestDrive:/archive1" -Format $Format $destinationPath = "TestDrive:/directory3" Push-Location $destinationPath @@ -397,7 +415,7 @@ Describe("Expand-Archive Tests") { } It "Expands an archive containing a single top-level directory and no other top-level items to a directory with that directory's name when -DestinationPath is not specified" { - $sourcePath = "TestDrive:/archive2.zip" + $sourcePath = Add-FileExtensionBasedOnFormat "TestDrive:/archive2" -Format $Format $destinationPath = "TestDrive:/directory4" Push-Location $destinationPath @@ -412,7 +430,7 @@ Describe("Expand-Archive Tests") { } It "Expands an archive containing multiple top-level items to a directory with that archive's name when -DestinationPath is not specified" { - $sourcePath = "TestDrive:/archive3.zip" + $sourcePath = Add-FileExtensionBasedOnFormat "TestDrive:/archive3" -Format $Format $destinationPath = "TestDrive:/directory5" Push-Location $destinationPath @@ -448,12 +466,13 @@ Describe("Expand-Archive Tests") { $archive4Paths = @("TestDrive:/file2.txt", "TestDrive:/file3.txt", "TestDrive:/emptydirectory1", "TestDrive:/emptydirectory2", "TestDrive:/nonemptydirectory1", "TestDrive:/nonemptydirectory2") - Compress-Archive -Path $archive4Paths -DestinationPath "TestDrive:/archive4.zip" + $sourcePath = Add-FileExtensionBasedOnFormat "TestDrive:/archive4" -Format $Format + Compress-Archive -Path $archive4Paths -DestinationPath $sourcePath -Format $Format - $sourcePath = "TestDrive:/archive4.zip" + $destinationPath = "TestDrive:/directory6" - Expand-Archive -Path $sourcePath -DestinationPath $destinationPath + Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format $expandedItems = Get-ChildItem $destinationPath -Recurse -Name @@ -468,11 +487,12 @@ Describe("Expand-Archive Tests") { It "Expands an archive containing a file whose LastWriteTime is in the past" { New-Item -Path "TestDrive:/oldfile.txt" -ItemType File Set-ItemProperty -Path "TestDrive:/oldfile.txt" -Name "LastWriteTime" -Value '2003-01-16 14:44' - Compress-Archive -Path "TestDrive:/oldfile.txt" -DestinationPath "TestDrive:/archive_oldfile.zip" + $sourcePath = Add-FileExtensionBasedOnFormat "TestDrive:/archive_oldfile" -Format $Format + Compress-Archive -Path "TestDrive:/oldfile.txt" -DestinationPath $sourcePath -Format $Format - $sourcePath = "TestDrive:/archive_oldfile.zip" + $destinationPath = "TestDrive:/destination7" - Expand-Archive -Path $sourcePath -DestinationPath $destinationPath + Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format $lastWriteTime = Get-ItemPropertyValue -Path (Join-Path $destinationPath "oldfile.txt") -Name "LastWriteTime" @@ -488,11 +508,13 @@ Describe("Expand-Archive Tests") { It "Expands an archive containing a directory whose LastWriteTime is in the past" { New-Item -Path "TestDrive:/olddirectory" -ItemType Directory Set-ItemProperty -Path "TestDrive:/olddirectory" -Name "LastWriteTime" -Value '2003-01-16 14:44' - Compress-Archive -Path "TestDrive:/olddirectory" -DestinationPath "TestDrive:/archive_olddirectory.zip" - $sourcePath = "TestDrive:/archive_olddirectory.zip" + $sourcePath = Add-FileExtensionBasedOnFormat "TestDrive:/archive_olddirectory" -Format $Format + Compress-Archive -Path "TestDrive:/olddirectory" -DestinationPath $sourcePath -Format $Format + + $destinationPath = "TestDrive:/destination_olddirectory" - Expand-Archive -Path $sourcePath -DestinationPath $destinationPath + Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format $lastWriteTime = Get-ItemPropertyValue -Path "TestDrive:/destination_olddirectory/olddirectory" -Name "LastWriteTime" @@ -572,7 +594,7 @@ Describe("Expand-Archive Tests") { } } - Context "File permssions, attributes, etc tests" { + Context "File permssions, attributes, etc tests" -Tag td2 { BeforeAll { New-Item TestDrive:/file.txt -ItemType File "Hello, World!" | Out-File -Path TestDrive:/file.txt @@ -588,6 +610,7 @@ Describe("Expand-Archive Tests") { $fileShare = [System.IO.FileShare]::Read $archiveInUseStream = New-Object -TypeName "System.IO.FileStream" -ArgumentList "${TestDrive}/archive_in_use.zip",$fileMode,$fileAccess,$fileShare + [console]::InputEncoding = [console]::OutputEncoding = New-Object System.Text.UTF8Encoding # Create an archive containing an entry with non-latin characters New-Item TestDrive:/ملف -ItemType File "Hello, World!" | Out-File -Path TestDrive:/ملف diff --git a/src/TarArchive.cs b/src/TarArchive.cs index 56828dc..1968df1 100644 --- a/src/TarArchive.cs +++ b/src/TarArchive.cs @@ -11,7 +11,7 @@ namespace Microsoft.PowerShell.Archive { internal class TarArchive : IArchive { - private bool disposedValue; + private bool _disposedValue; private readonly ArchiveMode _mode; @@ -54,12 +54,16 @@ void IArchive.AddFileSystemEntry(ArchiveAddition entry) CreateCopyStream(); } } - else + else if (_tarWriter is null) { _tarWriter = new TarWriter(_fileStream, TarEntryFormat.Pax, true); } + + // Replace '\' with '/' + var entryName = entry.EntryName.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + Debug.Assert(_tarWriter is not null); - _tarWriter.WriteEntry(fileName: entry.FileSystemInfo.FullName, entryName: entry.EntryName); + _tarWriter.WriteEntry(fileName: entry.FileSystemInfo.FullName, entryName: entryName); } IEntry? IArchive.GetNextEntry() @@ -67,6 +71,7 @@ void IArchive.AddFileSystemEntry(ArchiveAddition entry) // If _tarReader is null, create it if (_tarReader is null) { + _fileStream.Position = 0; _tarReader = new TarReader(archiveStream: _fileStream, leaveOpen: true); } var entry = _tarReader.GetNextEntry(); @@ -113,7 +118,7 @@ private void ReplaceArchiveWithCopy() { protected virtual void Dispose(bool disposing) { - if (!disposedValue) + if (!_disposedValue) { if (disposing) { @@ -131,7 +136,7 @@ protected virtual void Dispose(bool disposing) // TODO: free unmanaged resources (unmanaged objects) and override finalizer // TODO: set large fields to null - disposedValue = true; + _disposedValue = true; } } @@ -144,7 +149,32 @@ public void Dispose() bool IArchive.HasTopLevelDirectory() { - throw new NotImplementedException(); + // Go through each entry and see if it is a top-level entry + _tarReader = new TarReader(_fileStream, leaveOpen: true); + + int topLevelDirectoriesCount = 0; + var entry = _tarReader.GetNextEntry(); + while (entry is not null) { + + if (entry.EntryType == TarEntryType.Directory) + { + topLevelDirectoriesCount++; + if (topLevelDirectoriesCount > 1) + { + break; + } + } else + { + _tarReader.Dispose(); + _tarReader = null; + return false; + } + entry = _tarReader.GetNextEntry(); + } + + _tarReader.Dispose(); + _tarReader = null; + return topLevelDirectoriesCount == 1; } internal class TarArchiveEntry : IEntry { From 4f9265cb0073f2b380b35a6b2b68c8ef5b4dea62 Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Thu, 11 Aug 2022 14:37:09 -0700 Subject: [PATCH 23/34] updated changelog and release notes --- CHANGELOG.md | 10 +++++----- src/Microsoft.PowerShell.Archive.psd1 | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6590597..a1ee678 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,19 +2,19 @@ ## 2.0.1-preview2 -- Rewrite `Expand-Archive` cmdlet in C# +- Rewrote `Expand-Archive` cmdlet in C# - Added `-Format` parameter to `Expand-Archive` - Added `-WriteMode` parameter to `Expand-Archive` -- Added support for zip64 +- Added support for zip64 to `Expand-Archive` - Fixed a bug where the entry names of files in a directory would not be correct when compressing an archive -- `Compress-Archive` skips writing an entry to an archive if an error occurs while doing so +- `Compress-Archive` by default skips writing an entry to an archive if an error occurs while doing so ## 2.0.1-preview1 -- Rewrite `Compress-Archive` cmdlet in C# +- Rewrote `Compress-Archive` cmdlet in C# - Added `-Format` parameter to `Compress-Archive` - Added `-WriteMode` parameter to `Compress-Archive` - Added support for relative path structure preservating when paths relative to the working directory are specified to `-Path` or `-LiteralPath` in `Compress-Archive` -- Added support for zip64 +- Added support for zip64 to `Compress-Archive` - Fixed a bug where empty directories would not be compressed - Fixed a bug where an abrupt stop when compressing empty directories would not delete the newly created archive diff --git a/src/Microsoft.PowerShell.Archive.psd1 b/src/Microsoft.PowerShell.Archive.psd1 index 0861e0e..da72f6b 100644 --- a/src/Microsoft.PowerShell.Archive.psd1 +++ b/src/Microsoft.PowerShell.Archive.psd1 @@ -15,19 +15,19 @@ PrivateData = @{ LicenseUri = 'https://go.microsoft.com/fwlink/?linkid=2203619' ReleaseNotes = @' ## 2.0.1-preview2 - - Rewrite `Expand-Archive` cmdlet in C# + - Rewrote `Expand-Archive` cmdlet in C# - Added `-Format` parameter to `Expand-Archive` - Added `-WriteMode` parameter to `Expand-Archive` - - Added support for zip64 + - Added support for zip64 to `Expand-Archive` - Fixed a bug where the entry names of files in a directory would not be correct when compressing an archive - - `Compress-Archive` skips writing an entry to an archive if an error occurs while doing so + - `Compress-Archive` by default skips writing an entry to an archive if an error occurs while doing so ## 2.0.1-preview1 - - Rewrite `Compress-Archive` cmdlet in C# + - Rewrote `Compress-Archive` cmdlet in C# - Added `-Format` parameter to `Compress-Archive` - Added `-WriteMode` parameter to `Compress-Archive` - Added support for relative path structure preservating when paths relative to the working directory are specified to `-Path` or `-LiteralPath` in `Compress-Archive` - - Added support for zip64 + - Added support for zip64 to `Compress-Archive` - Fixed a bug where empty directories would not be compressed - Fixed a bug where an abrupt stop when compressing empty directories would not delete the newly created archive '@ From 1d8a21ccfa09e4bb5563483f3c448d004201e20a Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Thu, 11 Aug 2022 14:39:30 -0700 Subject: [PATCH 24/34] fixed a typo in the changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1ee678..58c5e7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ - Rewrote `Compress-Archive` cmdlet in C# - Added `-Format` parameter to `Compress-Archive` - Added `-WriteMode` parameter to `Compress-Archive` -- Added support for relative path structure preservating when paths relative to the working directory are specified to `-Path` or `-LiteralPath` in `Compress-Archive` +- Added support for relative path structure preservation when paths relative to the working directory are specified to `-Path` or `-LiteralPath` in `Compress-Archive` - Added support for zip64 to `Compress-Archive` - Fixed a bug where empty directories would not be compressed - Fixed a bug where an abrupt stop when compressing empty directories would not delete the newly created archive From 4e7515f127c8c3059abf23f1cd1746f742b19811 Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Thu, 11 Aug 2022 17:35:59 -0700 Subject: [PATCH 25/34] added tests for path structure preservation, added gzip support, worked on tar.gz support --- .../Should-BeZipArchiveOnlyContaining.psm1 | 4 +- Tests/Compress-Archive.Tests.ps1 | 60 +++++++++ src/GzipArchive.cs | 125 ++++++++++++++++++ src/TarArchive.cs | 7 +- src/TarGzArchive.cs | 108 +++++++++++++++ 5 files changed, 299 insertions(+), 5 deletions(-) create mode 100644 src/GzipArchive.cs create mode 100644 src/TarGzArchive.cs diff --git a/Tests/Assertions/Should-BeZipArchiveOnlyContaining.psm1 b/Tests/Assertions/Should-BeZipArchiveOnlyContaining.psm1 index 833fc6c..52e448e 100644 --- a/Tests/Assertions/Should-BeZipArchiveOnlyContaining.psm1 +++ b/Tests/Assertions/Should-BeZipArchiveOnlyContaining.psm1 @@ -49,9 +49,9 @@ function Should-BeZipArchiveOnlyContaining { # Get 7-zip to list the contents of the archive if ($IsWindows) { - $output = 7z.exe l $ActualValue -ba + $output = 7z.exe l $ActualValue -ba -tzip } else { - $output = 7z l $ActualValue -ba + $output = 7z l $ActualValue -ba -tzip } # Check if the output is null diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index 807c6a4..67f9711 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -876,4 +876,64 @@ BeforeDiscovery { $destinationPath | Should -Not -Exist } } + + Context "CompressionLevel tests" { + BeforeAll { + New-Item -Path TestDrive:/file1.txt -ItemType File + "Hello, World!" | Out-File -FilePath TestDrive:/file1.txt + } + + It "Throws an error when an invalid value is supplied to CompressionLevel" { + try { + Compress-Archive -Path TestDrive:/file1.txt -DestinationPath TestDrive:/archive1.zip -CompressionLevel fakelevel + } catch { + $_.FullyQualifiedErrorId | Should -Be "InvalidArgument, ${CmdletClassName}" + } + } + } + + Context "Path Structure Preservation Tests" { + BeforeAll { + New-Item -Path TestDrive:/file1.txt -ItemType File + "Hello, World!" | Out-File -FilePath TestDrive:/file1.txt + + New-Item -Path TestDrive:/directory1 -ItemType Directory + New-Item -Path TestDrive:/directory1/subdir1 -ItemType Directory + New-Item -Path TestDrive:/directory1/subdir1/file.txt -ItemType File + "Hello, World!" | Out-File -FilePath TestDrive:/file.txt + } + + It "Creates an archive containing only a file when the path to that file is not relative to the working directory" { + $destinationPath = "TestDrive:/archive1.zip" + + Push-Location TestDrive:/directory1 + + Compress-Archive -Path TestDrive:/file1.txt -DestinationPath $destinationPath + $destinationPath | Should -BeArchiveOnlyContaining @("file1.txt") + + Pop-Location + } + + It "Creates an archive containing a file and its parent directories when the path to the file and its parent directories are descendents of the working directory" { + $destinationPath = "TestDrive:/archive2.zip" + + Push-Location TestDrive:/ + + Compress-Archive -Path directory1/subdir1/file.txt -DestinationPath $destinationPath + $destinationPath | Should -BeArchiveOnlyContaining @("directory1/subdir1/file.txt") + + Pop-Location + } + + It "Creates an archive containing a file and its parent directories when the path to the file and its parent directories are descendents of the working directory" { + $destinationPath = "TestDrive:/archive3.zip" + + Push-Location TestDrive:/ + + Compress-Archive -Path directory1 -DestinationPath $destinationPath + $destinationPath | Should -BeArchiveOnlyContaining @("directory1/subdir1/file.txt") + + Pop-Location + } + } } diff --git a/src/GzipArchive.cs b/src/GzipArchive.cs new file mode 100644 index 0000000..a695015 --- /dev/null +++ b/src/GzipArchive.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Formats.Tar; +using System.IO; +using System.IO.Compression; +using System.Diagnostics; + +namespace Microsoft.PowerShell.Archive +{ + internal class GzipArchive : IArchive + { + private bool _disposedValue; + + private readonly ArchiveMode _mode; + + private readonly string _path; + + private readonly FileStream _fileStream; + + private readonly CompressionLevel _compressionLevel; + + private bool _addedFile; + + private bool _didCallGetNextEntry; + + ArchiveMode IArchive.Mode => _mode; + + string IArchive.Path => _path; + + public GzipArchive(string path, ArchiveMode mode, FileStream fileStream, CompressionLevel compressionLevel) + { + _mode = mode; + _path = path; + _fileStream = fileStream; + _compressionLevel = compressionLevel; + } + + void IArchive.AddFileSystemEntry(ArchiveAddition entry) + { + if (_mode == ArchiveMode.Extract) + { + throw new ArgumentException("Adding entries to the archive is not supported on Extract mode."); + } + if (_mode == ArchiveMode.Update) + { + throw new ArgumentException("Updating a Gzip file in not supported."); + } + if (_addedFile) + { + throw new ArgumentException("Adding a Gzip file in not supported."); + } + if (entry.FileSystemInfo.Attributes.HasFlag(FileAttributes.Directory)) { + throw new ArgumentException("Compressing directories is not supported"); + } + using var gzipCompressor = new GZipStream(_fileStream, _compressionLevel, leaveOpen: true); + using var fileToCopy = new FileStream(entry.FileSystemInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.Read); + fileToCopy.CopyTo(gzipCompressor); + _addedFile = true; + } + + IEntry? IArchive.GetNextEntry() + { + // Gzip has no concept of entries + if (!_didCallGetNextEntry) { + _didCallGetNextEntry = true; + return new GzipArchiveEntry(this); + } + return null; + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + _fileStream.Dispose(); + } + + // 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); + } + + bool IArchive.HasTopLevelDirectory() + { + throw new NotSupportedException(); + } + + internal class GzipArchiveEntry : IEntry { + + private GzipArchive _gzipArchive; + + // Gzip has no concept of entries, so getting the entry name is not supported + string IEntry.Name => throw new NotSupportedException(); + + // Gzip does not compress directories, so this is always false + bool IEntry.IsDirectory => false; + + public GzipArchiveEntry(GzipArchive gzipArchive) + { + _gzipArchive = gzipArchive; + } + + void IEntry.ExpandTo(string destinationPath) + { + using var destinationFileStream = new FileStream(destinationPath, FileMode.CreateNew, FileAccess.Write, FileShare.None); + using var gzipDecompressor = new GZipStream(_gzipArchive._fileStream, CompressionMode.Decompress); + gzipDecompressor.CopyTo(destinationFileStream); + } + } + } +} diff --git a/src/TarArchive.cs b/src/TarArchive.cs index 1968df1..6cc266e 100644 --- a/src/TarArchive.cs +++ b/src/TarArchive.cs @@ -203,14 +203,15 @@ void IEntry.ExpandTo(string destinationPath) Directory.CreateDirectory(parentDirectory); } + var lastWriteTime = _entry.ModificationTime.LocalDateTime; if (_objectAsIEntry.IsDirectory) { - System.IO.Directory.CreateDirectory(destinationPath); - var lastWriteTime = _entry.ModificationTime; - System.IO.Directory.SetLastWriteTime(destinationPath, lastWriteTime.DateTime); + Directory.CreateDirectory(destinationPath); + Directory.SetLastWriteTime(destinationPath, lastWriteTime); } else { _entry.ExtractToFile(destinationPath, overwrite: false); + File.SetLastWriteTime(destinationPath, lastWriteTime); } } diff --git a/src/TarGzArchive.cs b/src/TarGzArchive.cs new file mode 100644 index 0000000..e3cb4b0 --- /dev/null +++ b/src/TarGzArchive.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Formats.Tar; +using System.IO; +using System.IO.Compression; +using System.Diagnostics; + +namespace Microsoft.PowerShell.Archive +{ + internal class TarGzArchive : IArchive + { + private bool _disposedValue; + + private readonly ArchiveMode _mode; + + private readonly string _path; + + private readonly FileStream _fileStream; + + private readonly CompressionLevel _compressionLevel; + + // Use a tar archive because .tar.gz file is a compressed tar file + private TarArchive _tarArchive; + + ArchiveMode IArchive.Mode => _mode; + + string IArchive.Path => _path; + + public TarGzArchive(string path, ArchiveMode mode, FileStream fileStream, CompressionLevel compressionLevel) + { + _mode = mode; + _path = path; + _fileStream = fileStream; + _compressionLevel = compressionLevel; + } + + void IArchive.AddFileSystemEntry(ArchiveAddition entry) + { + if (_mode == ArchiveMode.Create) { + + } + } + + IEntry? IArchive.GetNextEntry() + { + // Gzip has no concept of entries + if (!_didCallGetNextEntry) { + _didCallGetNextEntry = true; + return new TarGzArchiveEntry(this); + } + return null; + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + _fileStream.Dispose(); + } + + // 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); + } + + bool IArchive.HasTopLevelDirectory() + { + throw new NotSupportedException(); + } + + internal class TarGzArchiveEntry : IEntry { + + private TarGzArchive _gzipArchive; + + // Gzip has no concept of entries, so getting the entry name is not supported + string IEntry.Name => throw new NotSupportedException(); + + // Gzip does not compress directories, so this is always false + bool IEntry.IsDirectory => false; + + public TarGzArchiveEntry(TarGzArchive gzipArchive) + { + _gzipArchive = gzipArchive; + } + + void IEntry.ExpandTo(string destinationPath) + { + using var destinationFileStream = new FileStream(destinationPath, FileMode.CreateNew, FileAccess.Write, FileShare.None); + using var gzipDecompressor = new GZipStream(_gzipArchive._fileStream, CompressionMode.Decompress); + gzipDecompressor.CopyTo(destinationFileStream); + } + } + } +} From 0a5ca6b794dfa327223b72ba37f37a8d530265b4 Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Fri, 12 Aug 2022 17:25:16 -0700 Subject: [PATCH 26/34] worked on tar.gz support --- src/ArchiveFactory.cs | 2 +- src/TarGzArchive.cs | 28 ++++++++++++++++++++++------ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/ArchiveFactory.cs b/src/ArchiveFactory.cs index dacf713..bb91531 100644 --- a/src/ArchiveFactory.cs +++ b/src/ArchiveFactory.cs @@ -22,7 +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 Tar.gz here + ArchiveFormat.Tgz => new TarGzArchive(archivePath, archiveMode, archiveFileStream, compressionLevel), _ => throw new ArgumentOutOfRangeException(nameof(archiveMode)) }; } diff --git a/src/TarGzArchive.cs b/src/TarGzArchive.cs index e3cb4b0..ec1f2cf 100644 --- a/src/TarGzArchive.cs +++ b/src/TarGzArchive.cs @@ -23,7 +23,9 @@ internal class TarGzArchive : IArchive private readonly CompressionLevel _compressionLevel; // Use a tar archive because .tar.gz file is a compressed tar file - private TarArchive _tarArchive; + private TarArchive? _tarArchive; + + private string? _tarFilePath; ArchiveMode IArchive.Mode => _mode; @@ -39,17 +41,22 @@ public TarGzArchive(string path, ArchiveMode mode, FileStream fileStream, Compre void IArchive.AddFileSystemEntry(ArchiveAddition entry) { + if (_mode == ArchiveMode.Extract) { + throw new ArgumentException("Adding entries to the archive is not supported in extract mode"); + } + if (_mode == ArchiveMode.Create) { - + if (_tarArchive is null) { + _tarArchive = new TarArchive(_path, ArchiveMode.Create, _fileStream); + } + (_tarArchive as IArchive).AddFileSystemEntry(entry); } } IEntry? IArchive.GetNextEntry() { - // Gzip has no concept of entries - if (!_didCallGetNextEntry) { - _didCallGetNextEntry = true; - return new TarGzArchiveEntry(this); + if (_mode == ArchiveMode.Create || _mode == ArchiveMode.Update) { + throw new ArgumentException("Getting the entries in an archive is not supported in Create or Update mode"); } return null; } @@ -62,6 +69,7 @@ protected virtual void Dispose(bool disposing) { // TODO: dispose managed state (managed objects) _fileStream.Dispose(); + CompressArchive(); } // TODO: free unmanaged resources (unmanaged objects) and override finalizer @@ -82,6 +90,14 @@ bool IArchive.HasTopLevelDirectory() throw new NotSupportedException(); } + // Performs gzip compression on _path + private void CompressArchive() { + //using var destinationFileStream = new FileStream(destinationPath, FileMode.CreateNew, FileAccess.Write, FileShare.None); + _fileStream.Position = 0; + using var gzipDecompressor = new GZipStream(_fileStream, _compressionLevel, true); + _fileStream.CopyTo(gzipDecompressor); + } + internal class TarGzArchiveEntry : IEntry { private TarGzArchive _gzipArchive; From 86b7204fa8eaf671fa72d1932277100e0f3ebe45 Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Sat, 13 Aug 2022 20:09:04 -0700 Subject: [PATCH 27/34] organized files, worked on tar.gz support, filter and flatten support --- src/{ => Cmdlets}/CompressArchiveCommand.cs | 19 ++- src/{ => Cmdlets}/ExpandArchiveCommand.cs | 8 -- src/{ErrorMessages.cs => Error.cs} | 0 src/{ => Formats}/GzipArchive.cs | 14 +-- src/{ => Formats}/TarArchive.cs | 2 +- src/Formats/TarGzArchive.cs | 70 +++++++++++ src/{ => Formats}/ZipArchive.cs | 0 src/IArchive.cs | 12 +- src/PathHelper.cs | 83 ++++++++++--- src/TarGzArchive.cs | 124 -------------------- 10 files changed, 162 insertions(+), 170 deletions(-) rename src/{ => Cmdlets}/CompressArchiveCommand.cs (98%) rename src/{ => Cmdlets}/ExpandArchiveCommand.cs (97%) rename src/{ErrorMessages.cs => Error.cs} (100%) rename src/{ => Formats}/GzipArchive.cs (91%) rename src/{ => Formats}/TarArchive.cs (99%) create mode 100644 src/Formats/TarGzArchive.cs rename src/{ => Formats}/ZipArchive.cs (100%) delete mode 100644 src/TarGzArchive.cs diff --git a/src/CompressArchiveCommand.cs b/src/Cmdlets/CompressArchiveCommand.cs similarity index 98% rename from src/CompressArchiveCommand.cs rename to src/Cmdlets/CompressArchiveCommand.cs index dea909f..76679fa 100644 --- a/src/CompressArchiveCommand.cs +++ b/src/Cmdlets/CompressArchiveCommand.cs @@ -50,18 +50,25 @@ private enum ParameterSet [NotNull] public string? DestinationPath { get; set; } - [Parameter()] + [Parameter] public WriteMode WriteMode { get; set; } - [Parameter()] + [Parameter] public SwitchParameter PassThru { get; set; } - [Parameter()] + [Parameter] [ValidateNotNullOrEmpty] public CompressionLevel CompressionLevel { get; set; } - [Parameter()] - public ArchiveFormat? Format { get; set; } = null; + [Parameter] + public ArchiveFormat? Format { get; set; } + + [Parameter] + [ValidateNotNullOrEmpty] + public string? Filter { get; set; } + + [Parameter] + public SwitchParameter Flatten { get; set; } private readonly PathHelper _pathHelper; @@ -156,6 +163,8 @@ protected override void EndProcessing() // Get archive entries // If a path causes an exception (e.g., SecurityException), _pathHelper should handle it Debug.Assert(_paths is not null); + _pathHelper.Flatten = Flatten; + _pathHelper.Filter = Filter; List archiveAdditions = _pathHelper.GetArchiveAdditions(_paths); // Remove references to _paths, Path, and LiteralPath to free up memory diff --git a/src/ExpandArchiveCommand.cs b/src/Cmdlets/ExpandArchiveCommand.cs similarity index 97% rename from src/ExpandArchiveCommand.cs rename to src/Cmdlets/ExpandArchiveCommand.cs index cc41587..7c3d4ec 100644 --- a/src/ExpandArchiveCommand.cs +++ b/src/Cmdlets/ExpandArchiveCommand.cs @@ -46,8 +46,6 @@ private enum ParameterSet { private PathHelper _pathHelper; - private System.IO.FileSystemInfo? _destinationPathInfo; - private bool _didCreateOutput; private string? _sourcePath; @@ -56,9 +54,7 @@ private enum ParameterSet { public ExpandArchiveCommand() { - _didCreateOutput = false; _pathHelper = new PathHelper(cmdlet: this); - _destinationPathInfo = null; } protected override void BeginProcessing() @@ -189,10 +185,6 @@ private void ProcessArchiveEntry(IEntry entry) postExpandPath = postExpandPath.Remove(postExpandPath.Length - 1); } - // Notify the user that we are expanding the entry - //var expandingEntryMsg = string.Format(Messages.ExpandingEntryMessage, entry.Name, postExpandPath); - //WriteObject(expandingEntryMsg); - // If the entry name is invalid, write a non-terminating error and stop processing the entry if (IsPathInvalid(postExpandPath)) { diff --git a/src/ErrorMessages.cs b/src/Error.cs similarity index 100% rename from src/ErrorMessages.cs rename to src/Error.cs diff --git a/src/GzipArchive.cs b/src/Formats/GzipArchive.cs similarity index 91% rename from src/GzipArchive.cs rename to src/Formats/GzipArchive.cs index a695015..a9c0000 100644 --- a/src/GzipArchive.cs +++ b/src/Formats/GzipArchive.cs @@ -12,19 +12,19 @@ namespace Microsoft.PowerShell.Archive { internal class GzipArchive : IArchive { - private bool _disposedValue; + protected bool _disposedValue; - private readonly ArchiveMode _mode; + protected readonly ArchiveMode _mode; - private readonly string _path; + protected readonly string _path; - private readonly FileStream _fileStream; + protected readonly FileStream _fileStream; - private readonly CompressionLevel _compressionLevel; + protected readonly CompressionLevel _compressionLevel; private bool _addedFile; - private bool _didCallGetNextEntry; + protected bool _didCallGetNextEntry; ArchiveMode IArchive.Mode => _mode; @@ -38,7 +38,7 @@ public GzipArchive(string path, ArchiveMode mode, FileStream fileStream, Compres _compressionLevel = compressionLevel; } - void IArchive.AddFileSystemEntry(ArchiveAddition entry) + public virtual void AddFileSystemEntry(ArchiveAddition entry) { if (_mode == ArchiveMode.Extract) { diff --git a/src/TarArchive.cs b/src/Formats/TarArchive.cs similarity index 99% rename from src/TarArchive.cs rename to src/Formats/TarArchive.cs index 6cc266e..4188676 100644 --- a/src/TarArchive.cs +++ b/src/Formats/TarArchive.cs @@ -38,7 +38,7 @@ public TarArchive(string path, ArchiveMode mode, FileStream fileStream) _fileStream = fileStream; } - void IArchive.AddFileSystemEntry(ArchiveAddition entry) + public void AddFileSystemEntry(ArchiveAddition entry) { if (_mode == ArchiveMode.Extract) { diff --git a/src/Formats/TarGzArchive.cs b/src/Formats/TarGzArchive.cs new file mode 100644 index 0000000..acb22dd --- /dev/null +++ b/src/Formats/TarGzArchive.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Formats.Tar; +using System.IO; +using System.IO.Compression; +using System.Diagnostics; + +namespace Microsoft.PowerShell.Archive +{ + internal class TarGzArchive : GzipArchive + { + + // Use a tar archive because .tar.gz file is a compressed tar file + private TarArchive? _tarArchive; + + private string? _tarFilePath; + + private FileStream? _tarFileStream; + + public TarGzArchive(string path, ArchiveMode mode, FileStream fileStream, CompressionLevel compressionLevel) : base(path, mode, fileStream, compressionLevel) + { + } + + public override void AddFileSystemEntry(ArchiveAddition entry) + { + if (_mode == ArchiveMode.Extract || _mode == ArchiveMode.Update) { + throw new ArgumentException("Adding entries to the archive is not supported in extract or update mode"); + } + + if (_tarArchive is null) + { + var outputDirectory = Path.GetDirectoryName(_path); + var tarFilename = Path.GetRandomFileName(); + _tarFilePath = Path.Combine(outputDirectory, tarFilename); + _tarFileStream = new FileStream(_tarFilePath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None); + _tarArchive = new TarArchive(_tarFilePath, ArchiveMode.Create, _tarFileStream); + + } + _tarArchive.AddFileSystemEntry(entry); + } + + protected override void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + _fileStream.Dispose(); + CompressArchive(); + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + _disposedValue = true; + } + } + + // Performs gzip compression on _path + private void CompressArchive() { + Debug.Assert(_tarFileStream is not null); + _tarFileStream.Position = 0; + using var gzipCompressor = new GZipStream(_fileStream, _compressionLevel, true); + _tarFileStream.CopyTo(gzipCompressor); + } + } +} diff --git a/src/ZipArchive.cs b/src/Formats/ZipArchive.cs similarity index 100% rename from src/ZipArchive.cs rename to src/Formats/ZipArchive.cs diff --git a/src/IArchive.cs b/src/IArchive.cs index c0e3b77..4d284b2 100644 --- a/src/IArchive.cs +++ b/src/IArchive.cs @@ -7,22 +7,22 @@ namespace Microsoft.PowerShell.Archive { - internal interface IArchive: IDisposable + interface IArchive: IDisposable { // Get what mode the archive is in - internal ArchiveMode Mode { get; } + public ArchiveMode Mode { get; } // Get the fully qualified path of the archive - internal string Path { get; } + public string Path { 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 AddFileSystemEntry(ArchiveAddition entry); + public void AddFileSystemEntry(ArchiveAddition entry); - internal IEntry? GetNextEntry(); + public IEntry? GetNextEntry(); // Does the archive have only a top-level directory? - internal bool HasTopLevelDirectory(); + public bool HasTopLevelDirectory(); } } diff --git a/src/PathHelper.cs b/src/PathHelper.cs index 9dbb9af..87dd646 100644 --- a/src/PathHelper.cs +++ b/src/PathHelper.cs @@ -16,6 +16,12 @@ internal class PathHelper private const string FileSystemProviderName = "FileSystem"; + internal bool Flatten { get; set; } + + internal string? Filter { get; set; } + + internal WildcardPattern? _wildCardPattern; + internal PathHelper(PSCmdlet cmdlet) { _cmdlet = cmdlet; @@ -23,13 +29,16 @@ internal PathHelper(PSCmdlet cmdlet) internal List GetArchiveAdditions(HashSet fullyQualifiedPaths) { + if (Filter is not null) { + _wildCardPattern = new WildcardPattern(Filter); + } List archiveAdditions = new List(fullyQualifiedPaths.Count); foreach (var path in fullyQualifiedPaths) { // Assume each path is valid, fully qualified, and existing Debug.Assert(Path.Exists(path)); Debug.Assert(Path.IsPathFullyQualified(path)); - AddAdditionForFullyQualifiedPath(path, archiveAdditions); + AddAdditionForFullyQualifiedPath(path, archiveAdditions, entryName: null, parentMatchesFilter: false); } return archiveAdditions; } @@ -40,7 +49,7 @@ internal List GetArchiveAdditions(HashSet fullyQualifie /// 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) + private void AddAdditionForFullyQualifiedPath(string path, List additions, string? entryName, bool parentMatchesFilter) { Debug.Assert(Path.Exists(path)); FileSystemInfo fileSystemInfo; @@ -60,15 +69,38 @@ private void AddAdditionForFullyQualifiedPath(string path, List fileSystemInfo = new FileInfo(path); } - // 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)); + bool doesMatchFilter = true; + if (!parentMatchesFilter && _wildCardPattern is not null) { + doesMatchFilter = _wildCardPattern.IsMatch(fileSystemInfo.Name); + } + + // if entryName, then set it as the entry name of the file or directory in the archive + // The entry name will preserve the directory structure as long as the path is relative to the working directory + if (entryName is null) { + entryName = GetEntryName(fileSystemInfo, out bool doesPreservePathStructure); + } + + + // Number of elements in additions before adding this item and its descendents if it is a directory + int initialAdditions = additions.Count; // 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); + if (fileSystemInfo.Attributes.HasFlag(FileAttributes.Directory) && fileSystemInfo is DirectoryInfo directoryInfo) + { + AddDescendentEntries(directoryInfo, additions, doesMatchFilter); } + + // Number of elements in additions after adding this item's descendents (if directory) + int finalAdditions = additions.Count; + + // If the item being added is a file, finalAdditions - initialAdditions = 0 + // If the item being added is a directory and does not have any descendent files that match the filter, finalAdditions - initialAdditions = 0 + // If the item being added is a directory and has descendent files that match the filter, finalAdditions > initialAdditions + + if (doesMatchFilter || (!doesMatchFilter && finalAdditions - initialAdditions > 0)) { + additions.Add(new ArchiveAddition(entryName: entryName, fileSystemInfo: fileSystemInfo)); + } + } /// @@ -77,28 +109,41 @@ 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(System.IO.DirectoryInfo directoryInfo, List additions, bool shouldPreservePathStructure) + private void AddDescendentEntries(System.IO.DirectoryInfo directoryInfo, List additions, bool parentMatchesFilter) { try { // pathPrefix is used to construct the entry names of the descendents of the directory var pathPrefix = GetPrefixForPath(directoryInfo: directoryInfo); - foreach (var childFileSystemInfo in directoryInfo.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)) + // If the parent directory matches the filter, then we don't have to check if each individual descendent of the directory + // matches the filter. + // This reduces the total number of method calls + SearchOption searchOption = parentMatchesFilter ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; + foreach (var childFileSystemInfo in directoryInfo.EnumerateFileSystemInfos("*", searchOption)) { string entryName; - // If the cmdlet should preserve the path structure, then use the relative path - if (shouldPreservePathStructure) + if (Flatten) { - entryName = GetEntryName(childFileSystemInfo, out bool doesPreservePathStructure); - Debug.Assert(doesPreservePathStructure); + entryName = childFileSystemInfo.Name; + } else + { + entryName = GetEntryNameUsingPrefix(path: childFileSystemInfo.FullName, prefix: pathPrefix); } - // Otherwise, get the entry name using the prefix + + + // Add an entry for each descendent of the directory + if (parentMatchesFilter) + { + // If the parent directory matches the filter, all its contents are included in the archive + // Just add the entry for each child without needing to check whether the child matches the filter + additions.Add(new ArchiveAddition(entryName: entryName, fileSystemInfo: childFileSystemInfo)); + } else { - entryName = GetEntryNameUsingPrefix(path: childFileSystemInfo.FullName, prefix: pathPrefix); + // If the parent directory does not match the filter, we want to call this function + // because this function will check if the name of the child matches the filter and if so, will add it + AddAdditionForFullyQualifiedPath(childFileSystemInfo.FullName, additions, entryName, parentMatchesFilter: false); } - // Add an entry for each descendent of the directory - additions.Add(new ArchiveAddition(entryName: entryName, fileSystemInfo: childFileSystemInfo)); } } // Write a non-terminating error if a securityException occurs @@ -121,7 +166,7 @@ private string GetEntryName(FileSystemInfo fileSystemInfo, out bool doesPreserve string entryName; doesPreservePathStructure = false; // If the path is relative to the current working directory, return the relative path as name - if (TryGetPathRelativeToCurrentWorkingDirectory(path: fileSystemInfo.FullName, out var relativePath)) + if (!Flatten && TryGetPathRelativeToCurrentWorkingDirectory(path: fileSystemInfo.FullName, out var relativePath)) { Debug.Assert(relativePath is not null); doesPreservePathStructure = true; diff --git a/src/TarGzArchive.cs b/src/TarGzArchive.cs deleted file mode 100644 index ec1f2cf..0000000 --- a/src/TarGzArchive.cs +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Formats.Tar; -using System.IO; -using System.IO.Compression; -using System.Diagnostics; - -namespace Microsoft.PowerShell.Archive -{ - internal class TarGzArchive : IArchive - { - private bool _disposedValue; - - private readonly ArchiveMode _mode; - - private readonly string _path; - - private readonly FileStream _fileStream; - - private readonly CompressionLevel _compressionLevel; - - // Use a tar archive because .tar.gz file is a compressed tar file - private TarArchive? _tarArchive; - - private string? _tarFilePath; - - ArchiveMode IArchive.Mode => _mode; - - string IArchive.Path => _path; - - public TarGzArchive(string path, ArchiveMode mode, FileStream fileStream, CompressionLevel compressionLevel) - { - _mode = mode; - _path = path; - _fileStream = fileStream; - _compressionLevel = compressionLevel; - } - - void IArchive.AddFileSystemEntry(ArchiveAddition entry) - { - if (_mode == ArchiveMode.Extract) { - throw new ArgumentException("Adding entries to the archive is not supported in extract mode"); - } - - if (_mode == ArchiveMode.Create) { - if (_tarArchive is null) { - _tarArchive = new TarArchive(_path, ArchiveMode.Create, _fileStream); - } - (_tarArchive as IArchive).AddFileSystemEntry(entry); - } - } - - IEntry? IArchive.GetNextEntry() - { - if (_mode == ArchiveMode.Create || _mode == ArchiveMode.Update) { - throw new ArgumentException("Getting the entries in an archive is not supported in Create or Update mode"); - } - return null; - } - - protected virtual void Dispose(bool disposing) - { - if (!_disposedValue) - { - if (disposing) - { - // TODO: dispose managed state (managed objects) - _fileStream.Dispose(); - CompressArchive(); - } - - // 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); - } - - bool IArchive.HasTopLevelDirectory() - { - throw new NotSupportedException(); - } - - // Performs gzip compression on _path - private void CompressArchive() { - //using var destinationFileStream = new FileStream(destinationPath, FileMode.CreateNew, FileAccess.Write, FileShare.None); - _fileStream.Position = 0; - using var gzipDecompressor = new GZipStream(_fileStream, _compressionLevel, true); - _fileStream.CopyTo(gzipDecompressor); - } - - internal class TarGzArchiveEntry : IEntry { - - private TarGzArchive _gzipArchive; - - // Gzip has no concept of entries, so getting the entry name is not supported - string IEntry.Name => throw new NotSupportedException(); - - // Gzip does not compress directories, so this is always false - bool IEntry.IsDirectory => false; - - public TarGzArchiveEntry(TarGzArchive gzipArchive) - { - _gzipArchive = gzipArchive; - } - - void IEntry.ExpandTo(string destinationPath) - { - using var destinationFileStream = new FileStream(destinationPath, FileMode.CreateNew, FileAccess.Write, FileShare.None); - using var gzipDecompressor = new GZipStream(_gzipArchive._fileStream, CompressionMode.Decompress); - gzipDecompressor.CopyTo(destinationFileStream); - } - } - } -} From 296864452f599b14834238c89bcde1a8b97f515f Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Tue, 16 Aug 2022 10:04:12 -0700 Subject: [PATCH 28/34] added support for expanding tar.gz archives --- src/ArchiveFactory.cs | 1 + src/Cmdlets/ExpandArchiveCommand.cs | 21 ++---- src/Formats/GzipArchive.cs | 25 +++---- src/Formats/TarArchive.cs | 33 +-------- src/Formats/TarGzArchive.cs | 104 ++++++++++++++++++++++------ src/Formats/ZipArchive.cs | 22 ------ src/IArchive.cs | 3 - 7 files changed, 100 insertions(+), 109 deletions(-) diff --git a/src/ArchiveFactory.cs b/src/ArchiveFactory.cs index bb91531..6502506 100644 --- a/src/ArchiveFactory.cs +++ b/src/ArchiveFactory.cs @@ -34,6 +34,7 @@ internal static bool TryGetArchiveFormatFromExtension(string path, out ArchiveFo ".zip" => ArchiveFormat.Zip, ".tar" => ArchiveFormat.Tar, ".gz" => path.EndsWith(".tar.gz") ? ArchiveFormat.Tgz : null, + ".tgz" => ArchiveFormat.Tgz, _ => null }; return archiveFormat is not null; diff --git a/src/Cmdlets/ExpandArchiveCommand.cs b/src/Cmdlets/ExpandArchiveCommand.cs index 7c3d4ec..e59e9d9 100644 --- a/src/Cmdlets/ExpandArchiveCommand.cs +++ b/src/Cmdlets/ExpandArchiveCommand.cs @@ -324,23 +324,14 @@ private string DetermineDestinationPath(IArchive archive) var workingDirectory = SessionState.Path.CurrentFileSystemLocation.ProviderPath; string? destinationDirectory = null; - // If the archive has a single top-level directory only, the destination will be: "working directory" - // This makes it easier for the cmdlet to expand the directory without needing addition checks - if (archive.HasTopLevelDirectory()) + var filename = System.IO.Path.GetFileName(archive.Path); + // If filename does have an exension, remove the extension and set the filename minus extension as destinationDirectory + if (System.IO.Path.GetExtension(filename) != string.Empty) { - destinationDirectory = workingDirectory; - } - // Otherwise, the destination path will be: "working directory/archive file name" - else - { - var filename = System.IO.Path.GetFileName(archive.Path); - // If filename does have an exension, remove the extension and set the filename minus extension as destinationDirectory - if (System.IO.Path.GetExtension(filename) != string.Empty) - { - int indexOfLastPeriod = filename.LastIndexOf('.'); - destinationDirectory = filename.Substring(0, indexOfLastPeriod); - } + int indexOfLastPeriod = filename.LastIndexOf('.'); + destinationDirectory = filename.Substring(0, indexOfLastPeriod); } + if (destinationDirectory is null) { diff --git a/src/Formats/GzipArchive.cs b/src/Formats/GzipArchive.cs index a9c0000..d2c08d2 100644 --- a/src/Formats/GzipArchive.cs +++ b/src/Formats/GzipArchive.cs @@ -12,19 +12,19 @@ namespace Microsoft.PowerShell.Archive { internal class GzipArchive : IArchive { - protected bool _disposedValue; + private bool _disposedValue; - protected readonly ArchiveMode _mode; + private readonly ArchiveMode _mode; - protected readonly string _path; + private readonly string _path; - protected readonly FileStream _fileStream; + private readonly FileStream _fileStream; - protected readonly CompressionLevel _compressionLevel; + private readonly CompressionLevel _compressionLevel; private bool _addedFile; - protected bool _didCallGetNextEntry; + private bool _didCallGetNextEntry; ArchiveMode IArchive.Mode => _mode; @@ -38,7 +38,7 @@ public GzipArchive(string path, ArchiveMode mode, FileStream fileStream, Compres _compressionLevel = compressionLevel; } - public virtual void AddFileSystemEntry(ArchiveAddition entry) + public void AddFileSystemEntry(ArchiveAddition entry) { if (_mode == ArchiveMode.Extract) { @@ -61,7 +61,7 @@ public virtual void AddFileSystemEntry(ArchiveAddition entry) _addedFile = true; } - IEntry? IArchive.GetNextEntry() + public IEntry? GetNextEntry() { // Gzip has no concept of entries if (!_didCallGetNextEntry) { @@ -71,7 +71,7 @@ public virtual void AddFileSystemEntry(ArchiveAddition entry) return null; } - protected virtual void Dispose(bool disposing) + private void Dispose(bool disposing) { if (!_disposedValue) { @@ -94,11 +94,6 @@ public void Dispose() GC.SuppressFinalize(this); } - bool IArchive.HasTopLevelDirectory() - { - throw new NotSupportedException(); - } - internal class GzipArchiveEntry : IEntry { private GzipArchive _gzipArchive; @@ -116,7 +111,7 @@ public GzipArchiveEntry(GzipArchive gzipArchive) void IEntry.ExpandTo(string destinationPath) { - using var destinationFileStream = new FileStream(destinationPath, FileMode.CreateNew, FileAccess.Write, FileShare.None); + using var destinationFileStream = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None); using var gzipDecompressor = new GZipStream(_gzipArchive._fileStream, CompressionMode.Decompress); gzipDecompressor.CopyTo(destinationFileStream); } diff --git a/src/Formats/TarArchive.cs b/src/Formats/TarArchive.cs index 4188676..09b2938 100644 --- a/src/Formats/TarArchive.cs +++ b/src/Formats/TarArchive.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System; -using System.Collections.Generic; using System.Formats.Tar; using System.IO; using System.Diagnostics; @@ -66,7 +65,7 @@ public void AddFileSystemEntry(ArchiveAddition entry) _tarWriter.WriteEntry(fileName: entry.FileSystemInfo.FullName, entryName: entryName); } - IEntry? IArchive.GetNextEntry() + public IEntry? GetNextEntry() { // If _tarReader is null, create it if (_tarReader is null) @@ -147,36 +146,6 @@ public void Dispose() GC.SuppressFinalize(this); } - bool IArchive.HasTopLevelDirectory() - { - // Go through each entry and see if it is a top-level entry - _tarReader = new TarReader(_fileStream, leaveOpen: true); - - int topLevelDirectoriesCount = 0; - var entry = _tarReader.GetNextEntry(); - while (entry is not null) { - - if (entry.EntryType == TarEntryType.Directory) - { - topLevelDirectoriesCount++; - if (topLevelDirectoriesCount > 1) - { - break; - } - } else - { - _tarReader.Dispose(); - _tarReader = null; - return false; - } - entry = _tarReader.GetNextEntry(); - } - - _tarReader.Dispose(); - _tarReader = null; - return topLevelDirectoriesCount == 1; - } - internal class TarArchiveEntry : IEntry { // Underlying object is System.Formats.Tar.TarEntry diff --git a/src/Formats/TarGzArchive.cs b/src/Formats/TarGzArchive.cs index acb22dd..54d908f 100644 --- a/src/Formats/TarGzArchive.cs +++ b/src/Formats/TarGzArchive.cs @@ -2,55 +2,123 @@ // Licensed under the MIT License. using System; -using System.Collections.Generic; -using System.Formats.Tar; using System.IO; using System.IO.Compression; using System.Diagnostics; namespace Microsoft.PowerShell.Archive { - internal class TarGzArchive : GzipArchive + internal class TarGzArchive : IArchive { // Use a tar archive because .tar.gz file is a compressed tar file private TarArchive? _tarArchive; + private FileStream? _tarFileStream; + private string? _tarFilePath; - private FileStream? _tarFileStream; + private string _path; + + private bool _disposedValue; + + private bool _didCallGetNextEntry; + + private readonly ArchiveMode _mode; + + private readonly FileStream _fileStream; + + private readonly CompressionLevel _compressionLevel; + + public string Path => _path; - public TarGzArchive(string path, ArchiveMode mode, FileStream fileStream, CompressionLevel compressionLevel) : base(path, mode, fileStream, compressionLevel) + ArchiveMode IArchive.Mode => _mode; + + string IArchive.Path => _path; + + public TarGzArchive(string path, ArchiveMode mode, FileStream fileStream, CompressionLevel compressionLevel) { + _path = path; + _mode = mode; + _fileStream = fileStream; + _compressionLevel = compressionLevel; } - public override void AddFileSystemEntry(ArchiveAddition entry) + public void AddFileSystemEntry(ArchiveAddition entry) { - if (_mode == ArchiveMode.Extract || _mode == ArchiveMode.Update) { + if (_mode == ArchiveMode.Extract || _mode == ArchiveMode.Update) + { throw new ArgumentException("Adding entries to the archive is not supported in extract or update mode"); } if (_tarArchive is null) { - var outputDirectory = Path.GetDirectoryName(_path); - var tarFilename = Path.GetRandomFileName(); - _tarFilePath = Path.Combine(outputDirectory, tarFilename); - _tarFileStream = new FileStream(_tarFilePath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None); + // This will create a temp file and return the path + _tarFilePath = System.IO.Path.GetTempFileName(); + // When creating the stream, the file already exists + _tarFileStream = new FileStream(_tarFilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None); _tarArchive = new TarArchive(_tarFilePath, ArchiveMode.Create, _tarFileStream); } _tarArchive.AddFileSystemEntry(entry); } - protected override void Dispose(bool disposing) + public IEntry? GetNextEntry() + { + if (_mode == ArchiveMode.Create) + { + throw new ArgumentException("Getting next entry is not supported when the archive is in Create mode"); + } + + if (_tarArchive is null) + { + // Create a Gzip archive + using var gzipArchive = new GzipArchive(_path, _mode, _fileStream, _compressionLevel); + // Where to put the tar file when expanding the tar.gz archive + _tarFilePath = System.IO.Path.GetTempFileName(); + // Expand the gzip portion + var entry = gzipArchive.GetNextEntry(); + Debug.Assert(entry is not null); + entry.ExpandTo(_tarFilePath); + // Create a TarArchive pointing to the newly expanded out tar file from the tar.gz file + FileStream tarFileStream = new FileStream(_tarFilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None); + _tarArchive = new TarArchive(_tarFilePath, ArchiveMode.Extract, tarFileStream); + } + return _tarArchive?.GetNextEntry(); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + // Performs gzip compression on _path + private void CompressArchive() { + Debug.Assert(_tarFilePath is not null); + _tarFileStream = new FileStream(_tarFilePath, FileMode.Open, FileAccess.Read); + using var gzipCompressor = new GZipStream(_fileStream, _compressionLevel, true); + _tarFileStream.CopyTo(gzipCompressor); + _tarFileStream.Dispose(); + } + + private void Dispose(bool disposing) { if (!_disposedValue) { if (disposing) { - // TODO: dispose managed state (managed objects) + // Do this before compression because disposing a tar archive will add necessary EOF markers + _tarArchive?.Dispose(); + if (_mode == ArchiveMode.Create) { + CompressArchive(); + } _fileStream.Dispose(); - CompressArchive(); + if (_tarFilePath is not null) { + // Delete the tar file created in the process of created the tar.gz file + File.Delete(_tarFilePath); + } } // TODO: free unmanaged resources (unmanaged objects) and override finalizer @@ -58,13 +126,5 @@ protected override void Dispose(bool disposing) _disposedValue = true; } } - - // Performs gzip compression on _path - private void CompressArchive() { - Debug.Assert(_tarFileStream is not null); - _tarFileStream.Position = 0; - using var gzipCompressor = new GZipStream(_fileStream, _compressionLevel, true); - _tarFileStream.CopyTo(gzipCompressor); - } } } diff --git a/src/Formats/ZipArchive.cs b/src/Formats/ZipArchive.cs index 3954831..5e2a41e 100644 --- a/src/Formats/ZipArchive.cs +++ b/src/Formats/ZipArchive.cs @@ -152,28 +152,6 @@ public void Dispose() GC.SuppressFinalize(this); } - bool IArchive.HasTopLevelDirectory() - { - int topLevelDirectoriesCount = 0; - foreach (var entry in _zipArchive.Entries) - { - if (entry.FullName.EndsWith(ZipArchiveDirectoryPathTerminator) && - entry.FullName.LastIndexOf(ZipArchiveDirectoryPathTerminator, entry.FullName.Length - 2) == -1) - { - topLevelDirectoriesCount++; - if (topLevelDirectoriesCount > 1) - { - break; - } - } else - { - return false; - } - } - - return topLevelDirectoriesCount == 1; - } - internal class ZipArchiveEntry : IEntry { // Underlying object is System.IO.Compression.ZipArchiveEntry diff --git a/src/IArchive.cs b/src/IArchive.cs index 4d284b2..18cfc75 100644 --- a/src/IArchive.cs +++ b/src/IArchive.cs @@ -21,8 +21,5 @@ interface IArchive: IDisposable public void AddFileSystemEntry(ArchiveAddition entry); public IEntry? GetNextEntry(); - - // Does the archive have only a top-level directory? - public bool HasTopLevelDirectory(); } } From 964406b336148cfdeecad1bb5c0a3622a3ee0ab9 Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Tue, 16 Aug 2022 12:09:50 -0700 Subject: [PATCH 29/34] merged with preview1 branch --- .../Should-BeArchiveOnlyContaining.psm1 | 2 +- Tests/Compress-Archive.Tests.ps1 | 1857 ++++++++--------- src/Cmdlets/CompressArchiveCommand.cs | 8 +- src/Cmdlets/ExpandArchiveCommand.cs | 2 +- src/Localized/Messages.resx | 1 + src/Microsoft.PowerShell.Archive.psd1 | 64 +- 6 files changed, 963 insertions(+), 971 deletions(-) diff --git a/Tests/Assertions/Should-BeArchiveOnlyContaining.psm1 b/Tests/Assertions/Should-BeArchiveOnlyContaining.psm1 index 6df4399..0ef8d23 100644 --- a/Tests/Assertions/Should-BeArchiveOnlyContaining.psm1 +++ b/Tests/Assertions/Should-BeArchiveOnlyContaining.psm1 @@ -25,7 +25,7 @@ function Should-BeArchiveOnlyContaining { if ($Format -eq "Tar") { return Should-BeTarArchiveOnlyContaining -ActualValue $ActualValue -ExpectedValue $ExpectedValue -Negate:$Negate -Because $Because -LiteralPath:$LiteralPath -CallerSessionState $CallerSessionState } - return return [pscustomobject]@{ + return [pscustomobject]@{ Succeeded = $false FailureMessage = "Format ${Format} is not supported." } diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index 67f9711..5f56178 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -2,938 +2,929 @@ # 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 - Import-Module "$PSScriptRoot/Assertions/Should-BeTarArchiveOnlyContaining.psm1" -DisableNameChecking - Import-Module "$PSScriptRoot/Assertions/Should-BeArchiveOnlyContaining.psm1" -DisableNameChecking + # Loads and registers custom assertion. Ignores usage of unapproved verb with -DisableNameChecking + Import-Module "$PSScriptRoot/Assertions/Should-BeZipArchiveOnlyContaining.psm1" -DisableNameChecking + Import-Module "$PSScriptRoot/Assertions/Should-BeTarArchiveOnlyContaining.psm1" -DisableNameChecking + Import-Module "$PSScriptRoot/Assertions/Should-BeArchiveOnlyContaining.psm1" -DisableNameChecking } - Describe("Microsoft.PowerShell.Archive tests") { - BeforeAll { - - $originalProgressPref = $ProgressPreference - $ProgressPreference = "SilentlyContinue" - $originalPSModulePath = $env:PSModulePath - - function Add-FileExtensionBasedOnFormat { - Param ( - [string] $Path, - [string] $Format - ) - - if ($Format -eq "Zip") { - return $Path += ".zip" - } - if ($Format -eq "Tar") { - return $Path += ".tar" - } - throw "Format type is not supported" - } - } - - AfterAll { - $global:ProgressPreference = $originalProgressPref - $env:PSModulePath = $originalPSModulePath - } - - Context "Parameter set validation tests" { - BeforeAll { - # 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." - } - catch - { - $_.FullyQualifiedErrorId | Should -Be "ParameterArgumentValidationError,Microsoft.PowerShell.Archive.CompressArchiveCommand" - } - - 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 is supplied for Path or LiteralPath parameters" -ForEach @( - @{ Path = "Variable:/PWD" } - @{ Path = @("TestDrive:/", "Variable:/PWD") } - ) { - $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" - } - - 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" { - $sourcePath = @( - "TestDrive:/SourceDir/Sample-1.txt", - "TestDrive:/SourceDir/Sample-1.txt") - $destinationPath = "TestDrive:/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 "DuplicatePaths,Microsoft.PowerShell.Archive.CompressArchiveCommand" - } - } - - It "Validate error from Compress-Archive when duplicate paths are supplied as input to LiteralPath parameter" { - $sourcePath = @( - "TestDrive:/SourceDir/Sample-1.txt", - "TestDrive:/SourceDir/Sample-1.txt") - $destinationPath = "TestDrive:/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 "DuplicatePaths,Microsoft.PowerShell.Archive.CompressArchiveCommand" - } - } - - ## From 504 - It "Validate that Source Path can be at SystemDrive location" -Skip { - $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/SampleSourceFileForArchive.txt - try - { - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - Test-Path $destinationPath | Should -Be $true - } - finally - { - Remove-Item "$sourcePath" -Force -Recurse -ErrorAction SilentlyContinue - } - } - - # 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:/SourceDir/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 { - $_.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:/SourceDir/Sample-1.txt" - $destinationPath = $sourcePath - - try { - 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" { - $sourcePath = "TestDrive:/EmptyDirectory" - $destinationPath = $sourcePath - - try { - 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" -Skip { - $sourcePath = "TestDrive:/SourceDir/Sample-1.txt" - $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:/SourceDir/Sample-1.txt" - $destinationPath = $sourcePath - - try { - 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 "SameLiteralPathAndDestinationPath,Microsoft.PowerShell.Archive.CompressArchiveCommand" - } - } - - It "Throws an error when LiteralPath and DestinationPath are the same and -Overwrite is specified" { - $sourcePath = "TestDrive:/EmptyDirectory" - $destinationPath = $sourcePath - - try { - 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 "SameLiteralPathAndDestinationPath,Microsoft.PowerShell.Archive.CompressArchiveCommand" - } - } - - It "Throws an error when an invalid path is supplied to DestinationPath" { - $sourcePath = "TestDrive:/SourceDir" - $destinationPath = "Variable:/PWD" - - try { - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - throw "Failed to detect an error when an invalid path is supplied to DestinationPath" - } catch { - $_.FullyQualifiedErrorId | Should -Be "InvalidPath,Microsoft.PowerShell.Archive.CompressArchiveCommand" - } - } - } - - Context "WriteMode tests" { - BeforeAll { - New-Item TestDrive:/SourceDir -Type Directory | Out-Null - - $content = "Some Data" - $content | Out-File -FilePath TestDrive:/SourceDir/Sample-1.txt - } - - It "Throws a terminating error when an incorrect value is supplied to -WriteMode" { - $sourcePath = "TestDrive:/SourceDir" - $destinationPath = "TestDrive:/archive1.zip" - - try { - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode mode - } catch { - $_.FullyQualifiedErrorId | Should -Be "CannotConvertArgumentNoMessage,Microsoft.PowerShell.Archive.CompressArchiveCommand" - } - } - - It "-WriteMode Create works" -Tag td1 { - $sourcePath = "TestDrive:/SourceDir" - $destinationPath = "TestDrive:/archive1.zip" - 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') - - - } - } - - Context "Basic functional tests" -ForEach @( - @{Format = "Zip"}, - @{Format = "Tar"} - ) { - BeforeAll { - 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:/EmptyDir -Type Directory | Out-Null - - $content = "Some Data" - $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 - - "Hello, World!" | Out-File -FilePath $TestDrive$($DS)HelloWorld.txt - - # Create a zero-byte file - New-Item TestDrive:/EmptyFile -Type File | Out-Null - - # Create a file whose last write time is before 1980 - $content | Out-File -FilePath TestDrive:/OldFile.txt - Set-ItemProperty -Path TestDrive:/OldFile.txt -Name LastWriteTime -Value '1974-01-16 14:44' - - # Create a directory whose last write time is before 1980 - New-Item -Path "TestDrive:/olddirectory" -ItemType Directory - Set-ItemProperty -Path "TestDrive:/olddirectory" -Name "LastWriteTime" -Value '1974-01-16 14:44' - } - - It "Compresses a single file with format " { - $sourcePath = "TestDrive:/SourceDir/ChildDir-1/Sample-2.txt" - $destinationPath = Add-FileExtensionBasedOnFormat -Path "TestDrive:/archive1" -Format $Format - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format - $destinationPath | Should -BeArchiveOnlyContaining @('Sample-2.txt') -Format $Format - } - - It "Compresses a non-empty directory with format " -Tag td1 { - $sourcePath = "TestDrive:/SourceDir/ChildDir-1" - $destinationPath = Add-FileExtensionBasedOnFormat -Path "TestDrive:/archive2" -Format $Format - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format - $destinationPath | Should -BeArchiveOnlyContaining @('ChildDir-1/', 'ChildDir-1/Sample-2.txt') -Format $Format - } - - It "Compresses an empty directory with format " { - $sourcePath = "TestDrive:/EmptyDir" - $destinationPath = Add-FileExtensionBasedOnFormat -Path "TestDrive:/archive3" -Format $Format - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format - $destinationPath | Should -BeArchiveOnlyContaining @('EmptyDir/') -Format $Format - } - - It "Compresses multiple files with format " { - $sourcePath = @("TestDrive:/SourceDir/ChildDir-1/Sample-2.txt", "TestDrive:/SourceDir/Sample-1.txt") - $destinationPath = Add-FileExtensionBasedOnFormat "TestDrive:/archive4" -Format $Format - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format - $destinationPath | Should -BeArchiveOnlyContaining @('Sample-1.txt', 'Sample-2.txt') -Format $Format - } - - It "Compresses multiple files and a single empty directory with format " { - $sourcePath = @("TestDrive:/SourceDir/ChildDir-1/Sample-2.txt", "TestDrive:/SourceDir/Sample-1.txt", - "TestDrive:/SourceDir/ChildEmptyDir") - $destinationPath = Add-FileExtensionBasedOnFormat "TestDrive:/archive5" -Format $Format - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format - $destinationPath | Should -BeArchiveOnlyContaining @('Sample-1.txt', 'Sample-2.txt', 'ChildEmptyDir/') -Format $Format - } - - It "Compresses multiple files and a single non-empty directory with format " { - $sourcePath = @("TestDrive:/SourceDir/ChildDir-1/Sample-2.txt", "TestDrive:/SourceDir/Sample-1.txt", - "TestDrive:/SourceDir/ChildDir-2") - $destinationPath = Add-FileExtensionBasedOnFormat "TestDrive:/archive6.zip" -Format $Format - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format - $destinationPath | Should -BeArchiveOnlyContaining @('Sample-1.txt', 'Sample-2.txt', 'ChildDir-2/', 'ChildDir-2/Sample-3.txt') -Format $Format - } - - It "Compresses multiple files and non-empty directories with format " { - $sourcePath = @("TestDrive:/HelloWorld.txt", "TestDrive:/SourceDir/Sample-1.txt", - "TestDrive:/SourceDir/ChildDir-1", "TestDrive:/SourceDir/ChildDir-2") - $destinationPath = Add-FileExtensionBasedOnFormat "TestDrive:/archive7.zip" -Format $Format - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format - $destinationPath | Should -BeArchiveOnlyContaining @('Sample-1.txt', 'HelloWorld.txt', 'ChildDir-1/', 'ChildDir-2/', - 'ChildDir-1/Sample-2.txt', 'ChildDir-2/Sample-3.txt') -Format $Format - } - - It "Compresses multiple files, non-empty directories, and an empty directory with format " { - $sourcePath = @("TestDrive:/HelloWorld.txt", "TestDrive:/SourceDir/Sample-1.txt", - "TestDrive:/SourceDir/ChildDir-1", "TestDrive:/SourceDir/ChildDir-2", "TestDrive:/SourceDir/ChildEmptyDir") - $destinationPath = Add-FileExtensionBasedOnFormat "TestDrive:/archive8.zip" -Format $Format - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format - $destinationPath | Should -BeArchiveOnlyContaining @('Sample-1.txt', 'HelloWorld.txt', 'ChildDir-1/', 'ChildDir-2/', - 'ChildDir-1/Sample-2.txt', 'ChildDir-2/Sample-3.txt', "ChildEmptyDir/") -Format $Format - } - - It "Compresses a directory containing files, non-empty directories, and an empty directory can be compressed with format " { - $sourcePath = "TestDrive:/SourceDir" - $destinationPath = Add-FileExtensionBasedOnFormat "TestDrive:/archive9.zip" -Format $Format - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format - $contents = @('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') - $destinationPath | Should -BeArchiveOnlyContaining $contents -Format $Format - } - - It "Compresses a zero-byte file with format " { - $sourcePath = "TestDrive:/EmptyFile" - $destinationPath = Add-FileExtensionBasedOnFormat "TestDrive:/archive10.zip" -Format $Format - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format - $destinationPath | Should -BeArchiveOnlyContaining @('EmptyFile') -Format $Format - } - } - - Context "Zip-specific tests" { - BeforeAll { - # Create a file whose last write time is before 1980 - $content | Out-File -FilePath TestDrive:/OldFile.txt - Set-ItemProperty -Path TestDrive:/OldFile.txt -Name LastWriteTime -Value '1974-01-16 14:44' - - # Create a directory whose last write time is before 1980 - New-Item -Path "TestDrive:/olddirectory" -ItemType Directory - Set-ItemProperty -Path "TestDrive:/olddirectory" -Name "LastWriteTime" -Value '1974-01-16 14:44' - } - - It "Compresses a file whose last write time is before 1980" { - $sourcePath = "$TestDrive$($DS)OldFile.txt" - $destinationPath = "$TestDrive$($DS)archive11.zip" - - # Assert the last write time of the file is before 1980 - $dateProperty = Get-ItemPropertyValue -Path $sourcePath -Name "LastWriteTime" - $dateProperty.Year | Should -BeLessThan 1980 - - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - $destinationPath | Should -Exist - Test-ZipArchive $destinationPath @('OldFile.txt') - - # Get the archive - $fileMode = [System.IO.FileMode]::Open - $archiveStream = New-Object -TypeName System.IO.FileStream -ArgumentList $destinationPath,$fileMode - $zipArchiveMode = [System.IO.Compression.ZipArchiveMode]::Read - $archive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $archiveStream,$zipArchiveMode - $entry = $archive.GetEntry("OldFile.txt") - $entry | Should -Not -BeNullOrEmpty - - $entry.LastWriteTime.Year | Should -BeExactly 1980 - $entry.LastWriteTime.Month| Should -BeExactly 1 - $entry.LastWriteTime.Day | Should -BeExactly 1 - $entry.LastWriteTime.Hour | Should -BeExactly 0 - $entry.LastWriteTime.Minute | Should -BeExactly 0 - $entry.LastWriteTime.Second | Should -BeExactly 0 - $entry.LastWriteTime.Millisecond | Should -BeExactly 0 - - - $archive.Dispose() - $archiveStream.Dispose() - } - - It "Compresses a directory whose last write time is before 1980 with format " { - $sourcePath = "TestDrive:/olddirectory" - $destinationPath = "${TestDrive}/archive12.zip" - - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - $destinationPath | Should -Exist - Test-ZipArchive $destinationPath @('olddirectory/') - - # Get the archive - $fileMode = [System.IO.FileMode]::Open - $archiveStream = New-Object -TypeName System.IO.FileStream -ArgumentList $destinationPath,$fileMode - $zipArchiveMode = [System.IO.Compression.ZipArchiveMode]::Read - $archive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $archiveStream,$zipArchiveMode - $entry = $archive.GetEntry("olddirectory/") - $entry | Should -Not -BeNullOrEmpty - - $entry.LastWriteTime.Year | Should -BeExactly 1980 - $entry.LastWriteTime.Month| Should -BeExactly 1 - $entry.LastWriteTime.Day | Should -BeExactly 1 - $entry.LastWriteTime.Hour | Should -BeExactly 0 - $entry.LastWriteTime.Minute | Should -BeExactly 0 - $entry.LastWriteTime.Second | Should -BeExactly 0 - $entry.LastWriteTime.Millisecond | Should -BeExactly 0 - - - $archive.Dispose() - $archiveStream.Dispose() - } - - It "Writes a warning when compressing a file whose last write time is before 1980 with format " { - $sourcePath = "TestDrive:/OldFile.txt" - $destinationPath = "${TestDrive}/archive13.zip" - - # Assert the last write time of the file is before 1980 - $dateProperty = Get-ItemPropertyValue -Path $sourcePath -Name "LastWriteTime" - $dateProperty.Year | Should -BeLessThan 1980 - - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WarningVariable warnings - $warnings.Length | Should -Be 1 - } - - It "Writes a warning when compresing a directory whose last write time is before 1980 with format " { - $sourcePath = "TestDrive:/olddirectory" - $destinationPath = "${TestDrive}/archive14.zip" - - # Assert the last write time of the file is before 1980 - $dateProperty = Get-ItemPropertyValue -Path $sourcePath -Name "LastWriteTime" - $dateProperty.Year | Should -BeLessThan 1980 - - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WarningVariable warnings - $warnings.Length | Should -Be 1 - } - } - - Context "DestinationPath and -WriteMode Overwrite tests" { - BeforeAll { - New-Item TestDrive:/SourceDir -Type Directory | Out-Null - - $content = "Some Data" - $content | Out-File -FilePath TestDrive:/SourceDir/Sample-1.txt - - New-Item TestDrive:/archive3.zip -Type Directory | Out-Null - - New-Item TestDrive:/EmptyDirectory -Type Directory | Out-Null - - # Create a read-only archive - $readOnlyArchivePath = "TestDrive:/readonly.zip" - Compress-Archive -Path TestDrive:/SourceDir/Sample-1.txt -DestinationPath $readOnlyArchivePath - Set-ItemProperty -Path $readOnlyArchivePath -Name IsReadOnly -Value $true - - # 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:/Sample-2.txt - } - - It "Throws an error when archive file already exists and -Update and -Overwrite parameters are not specified" { - $sourcePath = "TestDrive:/SourceDir" - $destinationPath = "TestDrive:/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." - } - catch - { - $_.FullyQualifiedErrorId | Should -Be "DestinationExists,Microsoft.PowerShell.Archive.CompressArchiveCommand" - } - } - - It "Throws a terminating error when archive file exists and -Update is specified but the archive is read-only" { - $sourcePath = "TestDrive:/SourceDir" - $destinationPath = "TestDrive:/readonly.zip" - - try - { - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Update - 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 "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:/SourceDir/Sample-1.txt" - $destinationPath = "TestDrive:/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 "DestinationExistsAsDirectory,Microsoft.PowerShell.Archive.CompressArchiveCommand" - } - } - - It "Throws a terminating error when DestinationPath is a directory and -Update is specified" { - $sourcePath = "TestDrive:/SourceDir" - $destinationPath = "TestDrive:/archive3.zip" - - try - { - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Update - throw "Failed to validate that a directory $destinationPath exists and -Update switch parameter is specified." - } - catch - { - $_.FullyQualifiedErrorId | Should -Be "DestinationExistsAsDirectory,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:/SourceDir" - $destinationPath = "TestDrive:" - - try - { - 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 "DestinationIsNonEmptyDirectory,Microsoft.PowerShell.Archive.CompressArchiveCommand" - } - } - - It "Throws a terminating error when archive does not exist and -Update mode is specified" { - $sourcePath = "TestDrive:/SourceDir" - $destinationPath = "TestDrive:/archive2.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." - } - catch - { - $_.FullyQualifiedErrorId | Should -Be "ArchiveDoesNotExist,Microsoft.PowerShell.Archive.CompressArchiveCommand" - } - } - - ## Overwrite tests - It "Throws an error when trying to overwrite an empty directory, which is the working directory" { - $sourcePath = "TestDrive:/Sample-2.txt" - $destinationPath = "TestDrive:/EmptyDirectory" - - 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" { - $sourcePath = "TestDrive:/SourceDir" - $destinationPath = "TestDrive:/EmptyDirectory" - - # Ensure $destinationPath is a directory - Test-Path $destinationPath -PathType Container | Should -Be $true - - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite - - # 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" - - # 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 - $destinationPath | Should -BeZipArchiveOnlyContaining @("Sample-2.txt") - } - } - - Context "Relative Path tests" { - BeforeAll { - 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:/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 = "./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 = "./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:/SourceDir" - $destinationPath = "./RelativePathForDestinationPathParameter.zip" - try - { - Push-Location TestDrive:/ - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - Test-Path $destinationPath | Should -Be $true - } - finally - { - Pop-Location - } - } - } - - Context "Special and Wildcard Characters Tests" { - BeforeAll { - New-Item TestDrive:/SourceDir -Type Directory | Out-Null - - $content = "Some Data" - $content | Out-File -FilePath TestDrive:/SourceDir/Sample-1.txt - New-Item -LiteralPath "$TestDrive$($DS)Source[]Dir" -Type Directory | Out-Null - $content | Out-File -FilePath $TestDrive$($DS)SourceDir$($DS)file1[].txt - } - - It "Accepts DestinationPath parameter with wildcard characters that resolves to one path" { - $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:/SourceDir" - $destinationPath = "TestDrive:/archive[2.zip" - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - $destinationPath | Should -BeZipArchiveOnlyContaining @("SourceDir/", "SourceDir/Sample-1.txt") -LiteralPath - Remove-Item -LiteralPath $destinationPath - } - - It "Accepts 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" - "Some Random Content" | Out-File -LiteralPath "$sourcePath$($DS)Sample[]File.txt" - $destinationPath = "$TestDrive$($DS)archive3.zip" - try - { - Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath - $destinationPath | Should -Exist - } - finally - { - Remove-Item -LiteralPath $sourcePath -Force -Recurse - } - } - - It "Accepts LiteralPath parameter for a file with wildcards in the filename" { - $sourcePath = "$TestDrive$($DS)file1[].txt" - $destinationPath = "$TestDrive$($DS)archive4.zip" - try - { - Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath - $destinationPath | Should -Exist - } - finally - { - Remove-Item -LiteralPath $sourcePath -Force -Recurse - } - } - } - - Context "PassThru tests" { - BeforeAll { - New-Item -Path TestDrive:/file.txt -ItemType File - } - - It "Returns an object of type System.IO.FileInfo when PassThru is specified" { - $output = Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive1.zip -PassThru - $output | Should -BeOfType System.IO.FileInfo - $destinationPath = Join-Path $TestDrive "archive1.zip" - $output.FullName | Should -Be $destinationPath - } - - It "Does not return an object when PassThru is not specified" { - $output = Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive2.zip - $output | Should -BeNullOrEmpty - } - - It "Does not return an object when PassThru is false" { - $output = Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive3.zip -PassThru:$false - $output | Should -BeNullOrEmpty - } - } - - Context "File permissions, attributes, etc. tests" { - BeforeAll { - New-Item TestDrive:/file.txt -ItemType File - "Hello, World!" | Out-File -Path TestDrive:/file.txt - - # Create a read-only file - New-Item TestDrive:/readonly.txt -ItemType File - "Hello, World!" | Out-File -Path TestDrive:/readonly.txt - } - - - It "Skips archiving a file in use" { - $fileMode = [System.IO.FileMode]::Open - $fileAccess = [System.IO.FileAccess]::Write - $fileShare = [System.IO.FileShare]::None - $archiveInUseStream = New-Object -TypeName "System.IO.FileStream" -ArgumentList "${TestDrive}/file.txt",$fileMode,$fileAccess,$fileShare - - Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive_in_use.zip -ErrorAction SilentlyContinue - # Ensure it creates an empty zip archive - "TestDrive:/archive_in_use.zip" | Should -BeZipArchiveOnlyContaining @() - - $archiveInUseStream.Dispose() - } - - It "Compresses a read-only file" { - $destinationPath = "TestDrive:/archive_with_readonly_file.zip" - Compress-Archive -Path TestDrive:/readonly.txt -DestinationPath $destinationPath - $destinationPath | Should -BeArchiveOnlyContaining @("readonly.txt") -Format Zip - } - } - - # This can be difficult to test - Context "Long path tests" -Skip { - BeforeAll { - if ($IsWindows) { - $maxPathLength = 300 - } - if ($IsLinux) { - $maxPathLength = 255 - } - if ($IsMacOS) { - $maxPathLength = 1024 - } - - function Get-MaxLengthPath { - param ( - [string] $character - ) - - $path = "${TestDrive}/" - while ($path.Length -le $maxPathLength + 10) { - $path += $character - } - return $path - } - - New-Item -Path "TestDrive:/file.txt" -ItemType File - "Hello, World!" | Out-File -FilePath "TestDrive:/file.txt" - } - - - It "Throws an error when -Path is too long" { - - } - - It "Throws an error when -LiteralPath is too long" { - - } - - It "Throws an error when -DestinationPath is too long" { - $path = "TestDrive:/file.txt" - # This will generate a path like TestDrive:/aaaaaa...aaaaaa - $destinationPath = Get-MaxLengthPath -character a - Write-Warning $destinationPath.Length - try { - Compress-Archive -Path $path -DestinationPath $destinationPath -ErrorVariable err - } catch { - throw "${$_.Exception}" - } - $destinationPath | Should -Not -Exist - } - } - - Context "CompressionLevel tests" { - BeforeAll { - New-Item -Path TestDrive:/file1.txt -ItemType File - "Hello, World!" | Out-File -FilePath TestDrive:/file1.txt - } - - It "Throws an error when an invalid value is supplied to CompressionLevel" { - try { - Compress-Archive -Path TestDrive:/file1.txt -DestinationPath TestDrive:/archive1.zip -CompressionLevel fakelevel - } catch { - $_.FullyQualifiedErrorId | Should -Be "InvalidArgument, ${CmdletClassName}" - } - } - } - - Context "Path Structure Preservation Tests" { - BeforeAll { - New-Item -Path TestDrive:/file1.txt -ItemType File - "Hello, World!" | Out-File -FilePath TestDrive:/file1.txt - - New-Item -Path TestDrive:/directory1 -ItemType Directory - New-Item -Path TestDrive:/directory1/subdir1 -ItemType Directory - New-Item -Path TestDrive:/directory1/subdir1/file.txt -ItemType File - "Hello, World!" | Out-File -FilePath TestDrive:/file.txt - } - - It "Creates an archive containing only a file when the path to that file is not relative to the working directory" { - $destinationPath = "TestDrive:/archive1.zip" - - Push-Location TestDrive:/directory1 - - Compress-Archive -Path TestDrive:/file1.txt -DestinationPath $destinationPath - $destinationPath | Should -BeArchiveOnlyContaining @("file1.txt") - - Pop-Location - } - - It "Creates an archive containing a file and its parent directories when the path to the file and its parent directories are descendents of the working directory" { - $destinationPath = "TestDrive:/archive2.zip" - - Push-Location TestDrive:/ - - Compress-Archive -Path directory1/subdir1/file.txt -DestinationPath $destinationPath - $destinationPath | Should -BeArchiveOnlyContaining @("directory1/subdir1/file.txt") - - Pop-Location - } - - It "Creates an archive containing a file and its parent directories when the path to the file and its parent directories are descendents of the working directory" { - $destinationPath = "TestDrive:/archive3.zip" - - Push-Location TestDrive:/ - - Compress-Archive -Path directory1 -DestinationPath $destinationPath - $destinationPath | Should -BeArchiveOnlyContaining @("directory1/subdir1/file.txt") - - Pop-Location - } - } -} +Describe("Microsoft.PowerShell.Archive tests") { + BeforeAll { + + $originalProgressPref = $ProgressPreference + $ProgressPreference = "SilentlyContinue" + $originalPSModulePath = $env:PSModulePath + + function Add-FileExtensionBasedOnFormat { + Param ( + [string] $Path, + [string] $Format + ) + + if ($Format -eq "Zip") { + return $Path += ".zip" + } + if ($Format -eq "Tar") { + return $Path += ".tar" + } + throw "Format type is not supported" + } + } + + AfterAll { + $global:ProgressPreference = $originalProgressPref + $env:PSModulePath = $originalPSModulePath + } + + Context "Parameter set validation tests" { + BeforeAll { + # 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." + } + catch + { + $_.FullyQualifiedErrorId | Should -Be "ParameterArgumentValidationError,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + + 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 is supplied for Path or LiteralPath parameters" -ForEach @( + @{ Path = "Variable:/PWD" } + @{ Path = @("TestDrive:/", "Variable:/PWD") } + ) { + $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" + } + + 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" { + $sourcePath = @( + "TestDrive:/SourceDir/Sample-1.txt", + "TestDrive:/SourceDir/Sample-1.txt") + $destinationPath = "TestDrive:/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 "DuplicatePaths,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } + + It "Validate error from Compress-Archive when duplicate paths are supplied as input to LiteralPath parameter" { + $sourcePath = @( + "TestDrive:/SourceDir/Sample-1.txt", + "TestDrive:/SourceDir/Sample-1.txt") + $destinationPath = "TestDrive:/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 "DuplicatePaths,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } + + ## From 504 + It "Validate that Source Path can be at SystemDrive location" -Skip { + $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/SampleSourceFileForArchive.txt + try + { + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + Test-Path $destinationPath | Should -Be $true + } + finally + { + Remove-Item "$sourcePath" -Force -Recurse -ErrorAction SilentlyContinue + } + } + + # 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:/SourceDir/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 { + $_.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:/SourceDir/Sample-1.txt" + $destinationPath = $sourcePath + + try { + 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" { + $sourcePath = "TestDrive:/EmptyDirectory" + $destinationPath = $sourcePath + + try { + 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" -Skip { + $sourcePath = "TestDrive:/SourceDir/Sample-1.txt" + $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:/SourceDir/Sample-1.txt" + $destinationPath = $sourcePath + + try { + 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 "SameLiteralPathAndDestinationPath,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } + + It "Throws an error when LiteralPath and DestinationPath are the same and -Overwrite is specified" { + $sourcePath = "TestDrive:/EmptyDirectory" + $destinationPath = $sourcePath + + try { + 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 "SameLiteralPathAndDestinationPath,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } + + It "Throws an error when an invalid path is supplied to DestinationPath" { + $sourcePath = "TestDrive:/SourceDir" + $destinationPath = "Variable:/PWD" + + try { + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + throw "Failed to detect an error when an invalid path is supplied to DestinationPath" + } catch { + $_.FullyQualifiedErrorId | Should -Be "InvalidPath,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } + } + + Context "WriteMode tests" { + BeforeAll { + New-Item TestDrive:/SourceDir -Type Directory | Out-Null + + $content = "Some Data" + $content | Out-File -FilePath TestDrive:/SourceDir/Sample-1.txt + } + + It "Throws a terminating error when an incorrect value is supplied to -WriteMode" { + $sourcePath = "TestDrive:/SourceDir" + $destinationPath = "TestDrive:/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:/SourceDir" + $destinationPath = "TestDrive:/archive1.zip" + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + $destinationPath | Should -BeZipArchiveOnlyContaining @('SourceDir/', 'SourceDir/Sample-1.txt') + + + } + } + + Context "Basic functional tests" -ForEach @( + @{Format = "Zip"}, + @{Format = "Tar"} + ) { + BeforeAll { + 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:/EmptyDir -Type Directory | Out-Null + + $content = "Some Data" + $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 + + "Hello, World!" | Out-File -FilePath TestDrive:/HelloWorld.txt + + # Create a zero-byte file + New-Item TestDrive:/EmptyFile -Type File | Out-Null + } + + It "Compresses a single file with format " { + $sourcePath = "TestDrive:/SourceDir/ChildDir-1/Sample-2.txt" + $destinationPath = Add-FileExtensionBasedOnFormat -Path "TestDrive:/archive1" -Format $Format + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format + $destinationPath | Should -BeArchiveOnlyContaining @('Sample-2.txt') -Format $Format + } + + It "Compresses a non-empty directory with format " -Tag td1 { + $sourcePath = "TestDrive:/SourceDir/ChildDir-1" + $destinationPath = Add-FileExtensionBasedOnFormat -Path "TestDrive:/archive2" -Format $Format + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format + $destinationPath | Should -BeArchiveOnlyContaining @('ChildDir-1/', 'ChildDir-1/Sample-2.txt') -Format $Format + } + + It "Compresses an empty directory with format " { + $sourcePath = "TestDrive:/EmptyDir" + $destinationPath = Add-FileExtensionBasedOnFormat -Path "TestDrive:/archive3" -Format $Format + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format + $destinationPath | Should -BeArchiveOnlyContaining @('EmptyDir/') -Format $Format + } + + It "Compresses multiple files with format " { + $sourcePath = @("TestDrive:/SourceDir/ChildDir-1/Sample-2.txt", "TestDrive:/SourceDir/Sample-1.txt") + $destinationPath = Add-FileExtensionBasedOnFormat "TestDrive:/archive4" -Format $Format + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format + $destinationPath | Should -BeArchiveOnlyContaining @('Sample-1.txt', 'Sample-2.txt') -Format $Format + } + + It "Compresses multiple files and a single empty directory with format " { + $sourcePath = @("TestDrive:/SourceDir/ChildDir-1/Sample-2.txt", "TestDrive:/SourceDir/Sample-1.txt", + "TestDrive:/SourceDir/ChildEmptyDir") + $destinationPath = Add-FileExtensionBasedOnFormat "TestDrive:/archive5" -Format $Format + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format + $destinationPath | Should -BeArchiveOnlyContaining @('Sample-1.txt', 'Sample-2.txt', 'ChildEmptyDir/') -Format $Format + } + + It "Compresses multiple files and a single non-empty directory with format " { + $sourcePath = @("TestDrive:/SourceDir/ChildDir-1/Sample-2.txt", "TestDrive:/SourceDir/Sample-1.txt", + "TestDrive:/SourceDir/ChildDir-2") + $destinationPath = Add-FileExtensionBasedOnFormat "TestDrive:/archive6.zip" -Format $Format + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format + $destinationPath | Should -BeArchiveOnlyContaining @('Sample-1.txt', 'Sample-2.txt', 'ChildDir-2/', 'ChildDir-2/Sample-3.txt') -Format $Format + } + + It "Compresses multiple files and non-empty directories with format " { + $sourcePath = @("TestDrive:/HelloWorld.txt", "TestDrive:/SourceDir/Sample-1.txt", + "TestDrive:/SourceDir/ChildDir-1", "TestDrive:/SourceDir/ChildDir-2") + $destinationPath = Add-FileExtensionBasedOnFormat "TestDrive:/archive7.zip" -Format $Format + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format + $destinationPath | Should -BeArchiveOnlyContaining @('Sample-1.txt', 'HelloWorld.txt', 'ChildDir-1/', 'ChildDir-2/', + 'ChildDir-1/Sample-2.txt', 'ChildDir-2/Sample-3.txt') -Format $Format + } + + It "Compresses multiple files, non-empty directories, and an empty directory with format " { + $sourcePath = @("TestDrive:/HelloWorld.txt", "TestDrive:/SourceDir/Sample-1.txt", + "TestDrive:/SourceDir/ChildDir-1", "TestDrive:/SourceDir/ChildDir-2", "TestDrive:/SourceDir/ChildEmptyDir") + $destinationPath = Add-FileExtensionBasedOnFormat "TestDrive:/archive8.zip" -Format $Format + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format + $destinationPath | Should -BeArchiveOnlyContaining @('Sample-1.txt', 'HelloWorld.txt', 'ChildDir-1/', 'ChildDir-2/', + 'ChildDir-1/Sample-2.txt', 'ChildDir-2/Sample-3.txt', "ChildEmptyDir/") -Format $Format + } + + It "Compresses a directory containing files, non-empty directories, and an empty directory can be compressed with format " { + $sourcePath = "TestDrive:/SourceDir" + $destinationPath = Add-FileExtensionBasedOnFormat "TestDrive:/archive9.zip" -Format $Format + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format + $contents = @('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') + $destinationPath | Should -BeArchiveOnlyContaining $contents -Format $Format + } + + It "Compresses a zero-byte file with format " { + $sourcePath = "TestDrive:/EmptyFile" + $destinationPath = Add-FileExtensionBasedOnFormat "TestDrive:/archive10.zip" -Format $Format + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format + $destinationPath | Should -BeArchiveOnlyContaining @('EmptyFile') -Format $Format + } + } + + Context "Zip-specific tests" { + BeforeAll { + # Create a file whose last write time is before 1980 + $content | Out-File -FilePath TestDrive:/OldFile.txt + Set-ItemProperty -Path TestDrive:/OldFile.txt -Name LastWriteTime -Value '1974-01-16 14:44' + + # Create a directory whose last write time is before 1980 + New-Item -Path "TestDrive:/olddirectory" -ItemType Directory + Set-ItemProperty -Path "TestDrive:/olddirectory" -Name "LastWriteTime" -Value '1974-01-16 14:44' + } + + It "Compresses a file whose last write time is before 1980" { + $sourcePath = "TestDrive:/OldFile.txt" + $destinationPath = "${TestDrive}/archive11.zip" + + # Assert the last write time of the file is before 1980 + $dateProperty = Get-ItemPropertyValue -Path $sourcePath -Name "LastWriteTime" + $dateProperty.Year | Should -BeLessThan 1980 + + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + $destinationPath | Should -BeZipArchiveOnlyContaining @('OldFile.txt') + + # Get the archive + $fileMode = [System.IO.FileMode]::Open + $archiveStream = New-Object -TypeName System.IO.FileStream -ArgumentList $destinationPath,$fileMode + $zipArchiveMode = [System.IO.Compression.ZipArchiveMode]::Read + $archive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $archiveStream,$zipArchiveMode + $entry = $archive.GetEntry("OldFile.txt") + $entry | Should -Not -BeNullOrEmpty + + $entry.LastWriteTime.Year | Should -BeExactly 1980 + $entry.LastWriteTime.Month| Should -BeExactly 1 + $entry.LastWriteTime.Day | Should -BeExactly 1 + $entry.LastWriteTime.Hour | Should -BeExactly 0 + $entry.LastWriteTime.Minute | Should -BeExactly 0 + $entry.LastWriteTime.Second | Should -BeExactly 0 + $entry.LastWriteTime.Millisecond | Should -BeExactly 0 + + + $archive.Dispose() + $archiveStream.Dispose() + } + + It "Compresses a directory whose last write time is before 1980 with format " { + $sourcePath = "TestDrive:/olddirectory" + $destinationPath = "${TestDrive}/archive12.zip" + + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + $destinationPath | Should -BeZipArchiveOnlyContaining @('olddirectory/') + + # Get the archive + $fileMode = [System.IO.FileMode]::Open + $archiveStream = New-Object -TypeName System.IO.FileStream -ArgumentList $destinationPath,$fileMode + $zipArchiveMode = [System.IO.Compression.ZipArchiveMode]::Read + $archive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $archiveStream,$zipArchiveMode + $entry = $archive.GetEntry("olddirectory/") + $entry | Should -Not -BeNullOrEmpty + + $entry.LastWriteTime.Year | Should -BeExactly 1980 + $entry.LastWriteTime.Month| Should -BeExactly 1 + $entry.LastWriteTime.Day | Should -BeExactly 1 + $entry.LastWriteTime.Hour | Should -BeExactly 0 + $entry.LastWriteTime.Minute | Should -BeExactly 0 + $entry.LastWriteTime.Second | Should -BeExactly 0 + $entry.LastWriteTime.Millisecond | Should -BeExactly 0 + + + $archive.Dispose() + $archiveStream.Dispose() + } + + It "Writes a warning when compressing a file whose last write time is before 1980 with format " { + $sourcePath = "TestDrive:/OldFile.txt" + $destinationPath = "${TestDrive}/archive13.zip" + + # Assert the last write time of the file is before 1980 + $dateProperty = Get-ItemPropertyValue -Path $sourcePath -Name "LastWriteTime" + $dateProperty.Year | Should -BeLessThan 1980 + + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WarningVariable warnings + $warnings.Length | Should -Be 1 + } + + It "Writes a warning when compresing a directory whose last write time is before 1980 with format " { + $sourcePath = "TestDrive:/olddirectory" + $destinationPath = "${TestDrive}/archive14.zip" + + # Assert the last write time of the file is before 1980 + $dateProperty = Get-ItemPropertyValue -Path $sourcePath -Name "LastWriteTime" + $dateProperty.Year | Should -BeLessThan 1980 + + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WarningVariable warnings + $warnings.Length | Should -Be 1 + } + } + + Context "DestinationPath and -WriteMode Overwrite tests" { + BeforeAll { + New-Item TestDrive:/SourceDir -Type Directory | Out-Null + + $content = "Some Data" + $content | Out-File -FilePath TestDrive:/SourceDir/Sample-1.txt + + New-Item TestDrive:/archive3.zip -Type Directory | Out-Null + + New-Item TestDrive:/EmptyDirectory -Type Directory | Out-Null + + # Create a read-only archive + $readOnlyArchivePath = "TestDrive:/readonly.zip" + Compress-Archive -Path TestDrive:/SourceDir/Sample-1.txt -DestinationPath $readOnlyArchivePath + Set-ItemProperty -Path $readOnlyArchivePath -Name IsReadOnly -Value $true + + # 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:/Sample-2.txt + } + + It "Throws an error when archive file already exists and -Update and -Overwrite parameters are not specified" { + $sourcePath = "TestDrive:/SourceDir" + $destinationPath = "TestDrive:/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." + } + catch + { + $_.FullyQualifiedErrorId | Should -Be "DestinationExists,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } + + It "Throws a terminating error when archive file exists and -Update is specified but the archive is read-only" { + $sourcePath = "TestDrive:/SourceDir" + $destinationPath = "TestDrive:/readonly.zip" + + try + { + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Update + 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 "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:/SourceDir/Sample-1.txt" + $destinationPath = "TestDrive:/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 "DestinationExistsAsDirectory,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } + + It "Throws a terminating error when DestinationPath is a directory and -Update is specified" { + $sourcePath = "TestDrive:/SourceDir" + $destinationPath = "TestDrive:/archive3.zip" + + try + { + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Update + throw "Failed to validate that a directory $destinationPath exists and -Update switch parameter is specified." + } + catch + { + $_.FullyQualifiedErrorId | Should -Be "DestinationExistsAsDirectory,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:/SourceDir" + $destinationPath = "TestDrive:" + + try + { + 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 "DestinationIsNonEmptyDirectory,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } + + It "Throws a terminating error when archive does not exist and -Update mode is specified" { + $sourcePath = "TestDrive:/SourceDir" + $destinationPath = "TestDrive:/archive2.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." + } + catch + { + $_.FullyQualifiedErrorId | Should -Be "ArchiveDoesNotExist,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } + + ## Overwrite tests + It "Throws an error when trying to overwrite an empty directory, which is the working directory" { + $sourcePath = "TestDrive:/Sample-2.txt" + $destinationPath = "TestDrive:/EmptyDirectory" + + 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" { + $sourcePath = "TestDrive:/SourceDir" + $destinationPath = "TestDrive:/EmptyDirectory" + + # Ensure $destinationPath is a directory + Test-Path $destinationPath -PathType Container | Should -Be $true + + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WriteMode Overwrite + + # 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" + + # 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 + $destinationPath | Should -BeZipArchiveOnlyContaining @("Sample-2.txt") + } + } + + Context "Relative Path tests" { + BeforeAll { + 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:/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 = "./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 = "./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:/SourceDir" + $destinationPath = "./RelativePathForDestinationPathParameter.zip" + try + { + Push-Location TestDrive:/ + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + Test-Path $destinationPath | Should -Be $true + } + finally + { + Pop-Location + } + } + } + + Context "Special and Wildcard Characters Tests" { + BeforeAll { + New-Item TestDrive:/SourceDir -Type Directory + + New-Item -Path "TestDrive:/Source`[`]Dir" -Type Directory + + $content = "Some Data" + $content | Out-File -FilePath TestDrive:/SourceDir/Sample-1.txt + $content | Out-File -LiteralPath TestDrive:/file1[].txt + + $content = "Some Data" + $content | Out-File -FilePath TestDrive:/SourceDir/Sample-1.txt + } + + It "Accepts DestinationPath parameter with wildcard characters that resolves to one path" { + $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:/SourceDir" + $destinationPath = "TestDrive:/archive[2.zip" + + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + $destinationPath | Should -BeZipArchiveOnlyContaining @("SourceDir/", "SourceDir/Sample-1.txt") -LiteralPath + Remove-Item -LiteralPath $destinationPath -Force + } + + It "Accepts 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:/Source[]Dir" + "Some Random Content" | Out-File -LiteralPath "$sourcePath/Sample[]File.txt" + $destinationPath = "TestDrive:/archive3.zip" + try + { + Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath + $destinationPath | Should -Exist + } + finally + { + Remove-Item -LiteralPath $sourcePath -Force -Recurse + } + } + + It "Accepts LiteralPath parameter for a file with wildcards in the filename" { + $sourcePath = "TestDrive:/file1[].txt" + $destinationPath = "TestDrive:/archive4.zip" + try + { + Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath + $destinationPath | Should -BeZipArchiveOnlyContaining @("file1[].txt") + } + finally + { + Remove-Item -LiteralPath $sourcePath -Force -Recurse + } + } + } + + Context "PassThru tests" { + BeforeAll { + New-Item -Path TestDrive:/file.txt -ItemType File + } + + It "Returns an object of type System.IO.FileInfo when PassThru is specified" { + $output = Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive1.zip -PassThru + $output | Should -BeOfType System.IO.FileInfo + $destinationPath = Join-Path $TestDrive "archive1.zip" + $output.FullName | Should -Be $destinationPath + } + + It "Does not return an object when PassThru is not specified" { + $output = Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive2.zip + $output | Should -BeNullOrEmpty + } + + It "Does not return an object when PassThru is false" { + $output = Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive3.zip -PassThru:$false + $output | Should -BeNullOrEmpty + } + } + + Context "File permissions, attributes, etc. tests" { + BeforeAll { + New-Item TestDrive:/file.txt -ItemType File + "Hello, World!" | Out-File -Path TestDrive:/file.txt + + # Create a read-only file + New-Item TestDrive:/readonly.txt -ItemType File + "Hello, World!" | Out-File -Path TestDrive:/readonly.txt + } + + + It "Skips archiving a file in use" { + $fileMode = [System.IO.FileMode]::Open + $fileAccess = [System.IO.FileAccess]::Write + $fileShare = [System.IO.FileShare]::None + $archiveInUseStream = New-Object -TypeName "System.IO.FileStream" -ArgumentList "${TestDrive}/file.txt",$fileMode,$fileAccess,$fileShare + + Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive_in_use.zip -ErrorAction SilentlyContinue + # Ensure it creates an empty zip archive + "TestDrive:/archive_in_use.zip" | Should -BeZipArchiveOnlyContaining @() + + $archiveInUseStream.Dispose() + } + + It "Compresses a read-only file" { + $destinationPath = "TestDrive:/archive_with_readonly_file.zip" + Compress-Archive -Path TestDrive:/readonly.txt -DestinationPath $destinationPath + $destinationPath | Should -BeArchiveOnlyContaining @("readonly.txt") -Format Zip + } + } + + # This can be difficult to test + Context "Long path tests" -Skip { + BeforeAll { + if ($IsWindows) { + $maxPathLength = 300 + } + if ($IsLinux) { + $maxPathLength = 255 + } + if ($IsMacOS) { + $maxPathLength = 1024 + } + + function Get-MaxLengthPath { + param ( + [string] $character + ) + + $path = "${TestDrive}/" + while ($path.Length -le $maxPathLength + 10) { + $path += $character + } + return $path + } + + New-Item -Path "TestDrive:/file.txt" -ItemType File + "Hello, World!" | Out-File -FilePath "TestDrive:/file.txt" + } + + + It "Throws an error when -Path is too long" { + + } + + It "Throws an error when -LiteralPath is too long" { + + } + + It "Throws an error when -DestinationPath is too long" { + $path = "TestDrive:/file.txt" + # This will generate a path like TestDrive:/aaaaaa...aaaaaa + $destinationPath = Get-MaxLengthPath -character a + Write-Warning $destinationPath.Length + try { + Compress-Archive -Path $path -DestinationPath $destinationPath -ErrorVariable err + } catch { + throw "${$_.Exception}" + } + $destinationPath | Should -Not -Exist + } + } + + Context "CompressionLevel tests" { + BeforeAll { + New-Item -Path TestDrive:/file1.txt -ItemType File + "Hello, World!" | Out-File -FilePath TestDrive:/file1.txt + } + + It "Throws an error when an invalid value is supplied to CompressionLevel" { + try { + Compress-Archive -Path TestDrive:/file1.txt -DestinationPath TestDrive:/archive1.zip -CompressionLevel fakelevel + } catch { + $_.FullyQualifiedErrorId | Should -Be "CannotConvertArgumentNoMessage,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } + } + + Context "Path Structure Preservation Tests" { + BeforeAll { + New-Item -Path TestDrive:/file1.txt -ItemType File + "Hello, World!" | Out-File -FilePath TestDrive:/file1.txt + + New-Item -Path TestDrive:/directory1 -ItemType Directory + New-Item -Path TestDrive:/directory1/subdir1 -ItemType Directory + New-Item -Path TestDrive:/directory1/subdir1/file.txt -ItemType File + "Hello, World!" | Out-File -FilePath TestDrive:/file.txt + } + + It "Creates an archive containing only a file when the path to that file is not relative to the working directory" { + $destinationPath = "TestDrive:/archive1.zip" + + Push-Location TestDrive:/directory1 + + Compress-Archive -Path TestDrive:/file1.txt -DestinationPath $destinationPath + $destinationPath | Should -BeArchiveOnlyContaining @("file1.txt") -Format Zip + + Pop-Location + } + + It "Creates an archive containing a file and its parent directories when the path to the file and its parent directories are descendents of the working directory" { + $destinationPath = "TestDrive:/archive2.zip" + + Push-Location TestDrive:/ + + Compress-Archive -Path directory1/subdir1/file.txt -DestinationPath $destinationPath + $destinationPath | Should -BeArchiveOnlyContaining @("directory1/subdir1/file.txt") -Format Zip + + Pop-Location + } + + It "Creates an archive containing a file and its parent directories when the path to the file and its parent directories are descendents of the working directory" { + $destinationPath = "TestDrive:/archive3.zip" + + Push-Location TestDrive:/ + + Compress-Archive -Path directory1 -DestinationPath $destinationPath + $destinationPath | Should -BeArchiveOnlyContaining @("directory1/subdir1/file.txt") -Format Zip + + Pop-Location + } + } +} \ No newline at end of file diff --git a/src/Cmdlets/CompressArchiveCommand.cs b/src/Cmdlets/CompressArchiveCommand.cs index 76679fa..6aac79c 100644 --- a/src/Cmdlets/CompressArchiveCommand.cs +++ b/src/Cmdlets/CompressArchiveCommand.cs @@ -297,12 +297,12 @@ private void ValidateDestinationPath() // 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; + errorCode = ErrorCode.DestinationExistsAsDirectory; } // Throw an error if the DestinationPath is a directory and the cmdlet is in Update mode else if (WriteMode == WriteMode.Update) { - errorCode = ErrorCode.ArchiveExistsAsDirectory; + errorCode = ErrorCode.DestinationExistsAsDirectory; } // Throw an error if the DestinationPath is the current working directory and the cmdlet is in Overwrite mode else if (WriteMode == WriteMode.Overwrite && DestinationPath == SessionState.Path.CurrentFileSystemLocation.ProviderPath) @@ -312,7 +312,7 @@ private void ValidateDestinationPath() // 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 && Directory.GetFileSystemEntries(DestinationPath).Length > 0) { - errorCode = ErrorCode.ArchiveIsNonEmptyDirectory; + errorCode = ErrorCode.DestinationIsNonEmptyDirectory; } } // If DestinationPath is an existing file @@ -321,7 +321,7 @@ private void ValidateDestinationPath() // 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; + errorCode = ErrorCode.DestinationExists; } // Throw an error if the cmdlet is in Update mode but the archive is read only else if (WriteMode == WriteMode.Update && File.GetAttributes(DestinationPath).HasFlag(FileAttributes.ReadOnly)) diff --git a/src/Cmdlets/ExpandArchiveCommand.cs b/src/Cmdlets/ExpandArchiveCommand.cs index e59e9d9..3405d0a 100644 --- a/src/Cmdlets/ExpandArchiveCommand.cs +++ b/src/Cmdlets/ExpandArchiveCommand.cs @@ -177,7 +177,7 @@ private void ProcessArchiveEntry(IEntry entry) Debug.Assert(DestinationPath is not null); // The location of the entry post-expanding of the archive - string postExpandPath = GetPostExpansionPath(entryName: entry.Name, destinationPath: _destinationPathInfo.FullName); + string postExpandPath = GetPostExpansionPath(entryName: entry.Name, destinationPath: DestinationPath); // If postExpandPath has a terminating `/`, remove it (there is case where overwriting a file may fail because of this) if (postExpandPath.EndsWith(System.IO.Path.DirectorySeparatorChar)) diff --git a/src/Localized/Messages.resx b/src/Localized/Messages.resx index 24471bc..2fd8d20 100644 --- a/src/Localized/Messages.resx +++ b/src/Localized/Messages.resx @@ -143,6 +143,7 @@ Create + The destination path {0} is a directory. diff --git a/src/Microsoft.PowerShell.Archive.psd1 b/src/Microsoft.PowerShell.Archive.psd1 index da72f6b..2d79fbc 100644 --- a/src/Microsoft.PowerShell.Archive.psd1 +++ b/src/Microsoft.PowerShell.Archive.psd1 @@ -1,36 +1,36 @@ @{ -ModuleVersion = '2.0.1' -GUID = '06a335eb-dd10-4d25-b753-4f6a80163516' -Author = 'Microsoft' -CompanyName = 'Microsoft' -Copyright = '(c) Microsoft. All rights reserved.' -Description = 'PowerShell module for creating and expanding archives.' -PowerShellVersion = '7.2.5' -NestedModules = @('Microsoft.PowerShell.Archive.dll') -CmdletsToExport = @('Compress-Archive', 'Expand-Archive') -PrivateData = @{ - PSData = @{ - Tags = @('Archive', 'Zip', 'Compress') - ProjectUri = 'https://github.com/PowerShell/Microsoft.PowerShell.Archive' - LicenseUri = 'https://go.microsoft.com/fwlink/?linkid=2203619' - ReleaseNotes = @' - ## 2.0.1-preview2 - - Rewrote `Expand-Archive` cmdlet in C# - - Added `-Format` parameter to `Expand-Archive` - - Added `-WriteMode` parameter to `Expand-Archive` - - Added support for zip64 to `Expand-Archive` - - Fixed a bug where the entry names of files in a directory would not be correct when compressing an archive - - `Compress-Archive` by default skips writing an entry to an archive if an error occurs while doing so - - ## 2.0.1-preview1 - - Rewrote `Compress-Archive` cmdlet in C# - - Added `-Format` parameter to `Compress-Archive` - - Added `-WriteMode` parameter to `Compress-Archive` - - Added support for relative path structure preservating when paths relative to the working directory are specified to `-Path` or `-LiteralPath` in `Compress-Archive` - - Added support for zip64 to `Compress-Archive` - - Fixed a bug where empty directories would not be compressed - - Fixed a bug where an abrupt stop when compressing empty directories would not delete the newly created archive + ModuleVersion = '2.0.1' + GUID = '06a335eb-dd10-4d25-b753-4f6a80163516' + Author = 'Microsoft' + CompanyName = 'Microsoft' + Copyright = '(c) Microsoft. All rights reserved.' + Description = 'PowerShell module for creating and expanding archives.' + PowerShellVersion = '7.3.0' + NestedModules = @('Microsoft.PowerShell.Archive.dll') + CmdletsToExport = @('Compress-Archive', 'Expand-Archive') + PrivateData = @{ + PSData = @{ + Tags = @('Archive', 'Zip', 'Compress') + ProjectUri = 'https://github.com/PowerShell/Microsoft.PowerShell.Archive' + LicenseUri = 'https://go.microsoft.com/fwlink/?linkid=2203619' + ReleaseNotes = @' + ## 2.0.1-preview2 + - Rewrote `Expand-Archive` cmdlet in C# + - Added `-Format` parameter to `Expand-Archive` + - Added `-WriteMode` parameter to `Expand-Archive` + - Added support for zip64 to `Expand-Archive` + - Fixed a bug where the entry names of files in a directory would not be correct when compressing an archive + - `Compress-Archive` by default skips writing an entry to an archive if an error occurs while doing so + ## 2.0.1-preview1 + - Rewrote `Compress-Archive` cmdlet in C# + - Added `-Format` parameter to `Compress-Archive` + - Added `-WriteMode` parameter to `Compress-Archive` + - Added support for relative path structure preservating when paths relative to the working directory are specified to `-Path` or `-LiteralPath` in `Compress-Archive` + - Added support for zip64 to `Compress-Archive` + - Fixed a bug where empty directories would not be compressed + - Fixed a bug where an abrupt stop when compressing empty directories would not delete the newly created archive '@ - Prerelease = 'preview2' + Prerelease = 'preview2' + } } } \ No newline at end of file From 6bc0787aa955ec97b2279b2e2bb5971dda2ac0af Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Tue, 16 Aug 2022 16:22:58 -0700 Subject: [PATCH 30/34] added tests for Compress-Archive --- Tests/Compress-Archive.Tests.ps1 | 697 ++++++++++++++++++------------- Tests/Expand-Archive.Tests.ps1 | 41 +- src/PathHelper.cs | 17 +- 3 files changed, 453 insertions(+), 302 deletions(-) diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index 5f56178..f04a720 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -643,288 +643,425 @@ Describe("Microsoft.PowerShell.Archive tests") { } Context "Relative Path tests" { - BeforeAll { - 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:/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 = "./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 = "./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:/SourceDir" - $destinationPath = "./RelativePathForDestinationPathParameter.zip" - try - { - Push-Location TestDrive:/ - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - Test-Path $destinationPath | Should -Be $true - } - finally - { - Pop-Location - } - } + } - Context "Special and Wildcard Characters Tests" { - BeforeAll { - New-Item TestDrive:/SourceDir -Type Directory - - New-Item -Path "TestDrive:/Source`[`]Dir" -Type Directory + Context "Special and Wildcard Characters Tests" { + BeforeAll { + New-Item TestDrive:/SourceDir -Type Directory + + New-Item -Path "TestDrive:/Source`[`]Dir" -Type Directory + + $content = "Some Data" + $content | Out-File -FilePath TestDrive:/SourceDir/Sample-1.txt + $content | Out-File -LiteralPath TestDrive:/file1[].txt + + $content = "Some Data" + $content | Out-File -FilePath TestDrive:/SourceDir/Sample-1.txt + } + + It "Accepts DestinationPath parameter with wildcard characters that resolves to one path" { + $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:/SourceDir" + $destinationPath = "TestDrive:/archive[2.zip" + + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + $destinationPath | Should -BeZipArchiveOnlyContaining @("SourceDir/", "SourceDir/Sample-1.txt") -LiteralPath + Remove-Item -LiteralPath $destinationPath -Force + } + + It "Accepts 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:/Source[]Dir" + "Some Random Content" | Out-File -LiteralPath "$sourcePath/Sample[]File.txt" + $destinationPath = "TestDrive:/archive3.zip" + try + { + Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath + $destinationPath | Should -Exist + } + finally + { + Remove-Item -LiteralPath $sourcePath -Force -Recurse + } + } + + It "Accepts LiteralPath parameter for a file with wildcards in the filename" { + $sourcePath = "TestDrive:/file1[].txt" + $destinationPath = "TestDrive:/archive4.zip" + try + { + Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath + $destinationPath | Should -BeZipArchiveOnlyContaining @("file1[].txt") + } + finally + { + Remove-Item -LiteralPath $sourcePath -Force -Recurse + } + } + } + + Context "PassThru tests" { + BeforeAll { + New-Item -Path TestDrive:/file.txt -ItemType File + } + + It "Returns an object of type System.IO.FileInfo when PassThru is specified" { + $output = Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive1.zip -PassThru + $output | Should -BeOfType System.IO.FileInfo + $destinationPath = Join-Path $TestDrive "archive1.zip" + $output.FullName | Should -Be $destinationPath + } + + It "Does not return an object when PassThru is not specified" { + $output = Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive2.zip + $output | Should -BeNullOrEmpty + } + + It "Does not return an object when PassThru is false" { + $output = Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive3.zip -PassThru:$false + $output | Should -BeNullOrEmpty + } + } + + Context "File permissions, attributes, etc. tests" { + BeforeAll { + New-Item TestDrive:/file.txt -ItemType File + "Hello, World!" | Out-File -Path TestDrive:/file.txt + + # Create a read-only file + New-Item TestDrive:/readonly.txt -ItemType File + "Hello, World!" | Out-File -Path TestDrive:/readonly.txt + + # Create a hidden file + New-Item TestDrive:/.hiddenfile -ItemType File + "Hello, World!" | Out-File -Path TestDrive:/.hiddenfile + + # Create a hidden directory + New-Item TestDrive:/.hiddendirectory -ItemType Directory + New-Item TestDrive:/.hiddendirectory/file.txt -ItemType File -Force + "Hello, World!" | Out-File -Path TestDrive:/.hiddendirectory/file.txt + + # Create a directory containing a hidden file and directory + New-Item "TestDrive:/directory_with_hidden_items" -ItemType Directory + New-Item "TestDrive:/directory_with_hidden_items/.hiddenfile" -ItemType File + New-Item "TestDrive:/directory_with_hidden_items/.hiddendirectory" -ItemType Directory + if ($IsWindows) { + (Get-Item "TestDrive:/directory_with_hidden_items/.hiddenfile").Attributes += 'Hidden' + (Get-Item "TestDrive:/directory_with_hidden_items/.hiddendirectory").Attributes += 'Hidden' + } + } + + + It "Skips archiving a file in use" { + $fileMode = [System.IO.FileMode]::Open + $fileAccess = [System.IO.FileAccess]::Write + $fileShare = [System.IO.FileShare]::None + $archiveInUseStream = New-Object -TypeName "System.IO.FileStream" -ArgumentList "${TestDrive}/file.txt",$fileMode,$fileAccess,$fileShare + + Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive_in_use.zip -ErrorAction SilentlyContinue + # Ensure it creates an empty zip archive + "TestDrive:/archive_in_use.zip" | Should -BeZipArchiveOnlyContaining @() + + $archiveInUseStream.Dispose() + } + + It "Compresses a read-only file" { + $destinationPath = "TestDrive:/archive_with_readonly_file.zip" + Compress-Archive -Path TestDrive:/readonly.txt -DestinationPath $destinationPath + $destinationPath | Should -BeArchiveOnlyContaining @("readonly.txt") -Format Zip + } + + It "Compresses a hidden file" { + $path = "TestDrive:/.hiddenfile" + if ($IsWindows) { + (Get-Item $path).Attributes += 'Hidden' + } + $destinationPath = "TestDrive:/archive3.zip" + Compress-Archive -Path $path -DestinationPath $destinationPath + $destinationPath | Should -BeArchiveOnlyContaining @(".hiddenfile") -Format Zip + } + + It "Compresses a hidden directory" { + $path = "TestDrive:/.hiddendirectory" + if ($IsWindows) { + (Get-Item $path).Attributes += 'Hidden' + } + $destinationPath = "TestDrive:/archive4.zip" + Compress-Archive -Path $path -DestinationPath $destinationPath + $destinationPath | Should -BeArchiveOnlyContaining @(".hiddendirectory/", ".hiddendirectory/file.txt") -Format Zip + } + + It "Compresses a directory containing a hidden file and directory" { + $path = "TestDrive:/directory_with_hidden_items" + $destinationPath = "TestDrive:/archive5.zip" + Compress-Archive -Path $path -DestinationPath $destinationPath + $destinationPath | Should -BeArchiveOnlyContaining @("directory_with_hidden_items/", "directory_with_hidden_items/.hiddendirectory/", "directory_with_hidden_items/.hiddenfile") -Format Zip + } + } + + Context "CompressionLevel tests" { + BeforeAll { + New-Item -Path TestDrive:/file1.txt -ItemType File + "Hello, World!" | Out-File -FilePath TestDrive:/file1.txt + } + + It "Throws an error when an invalid value is supplied to CompressionLevel" { + try { + Compress-Archive -Path TestDrive:/file1.txt -DestinationPath TestDrive:/archive1.zip -CompressionLevel fakelevel + } catch { + $_.FullyQualifiedErrorId | Should -Be "CannotConvertArgumentNoMessage,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } + } + + Context "Path Structure Preservation Tests" { + BeforeAll { + New-Item -Path TestDrive:/file1.txt -ItemType File + "Hello, World!" | Out-File -FilePath TestDrive:/file1.txt + + New-Item -Path TestDrive:/directory1 -ItemType Directory + New-Item -Path TestDrive:/directory1/subdir1 -ItemType Directory + New-Item -Path TestDrive:/directory1/subdir1/file.txt -ItemType File + "Hello, World!" | Out-File -FilePath TestDrive:/file.txt + + 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:/SourceDir/Sample-1.txt + $content | Out-File -FilePath TestDrive:/SourceDir/ChildDir-1/Sample-2.txt + } + + It "Creates an archive containing only a file when the path to that file is not relative to the working directory" { + $destinationPath = "TestDrive:/archive1.zip" + + Push-Location TestDrive:/directory1 + + Compress-Archive -Path TestDrive:/file1.txt -DestinationPath $destinationPath + $destinationPath | Should -BeArchiveOnlyContaining @("file1.txt") -Format Zip + + Pop-Location + } + + It "Creates an archive containing a file and its parent directories when the path to the file and its parent directories are descendents of the working directory" { + $destinationPath = "TestDrive:/archive2.zip" + + Push-Location TestDrive:/ + + Compress-Archive -Path directory1/subdir1/file.txt -DestinationPath $destinationPath + $destinationPath | Should -BeArchiveOnlyContaining @("directory1/subdir1/file.txt") -Format Zip + + Pop-Location + } + + It "Compressing a relative path containing .. preserves the relative path structure" { + $destinationPath = "TestDrive:/archive3.zip" + + Push-Location TestDrive:/ + + Compress-Archive -Path directory1/../directory1/subdir1/file.txt -DestinationPath $destinationPath + $destinationPath | Should -BeArchiveOnlyContaining @("directory1/subdir1/file.txt") -Format Zip + + Pop-Location + } + + It "Compressing a relative path containing ~ works when the home directory is relative to the working directory" { + $destinationPath = "TestDrive:/archive4.zip" + $path = "~/file.txt" + New-Item -Path $path -ItemType File + + $homeDirectory = New-Object -TypeName System.IO.DirectoryInfo -ArgumentList $HOME + $homeDirectoryName = $homeDirectory.Name + + Push-Location "~/.." + + Compress-Archive -Path $path -DestinationPath $destinationPath + $destinationPath | Should -BeArchiveOnlyContaining @("${homeDirectoryName}/file.txt") -Format Zip + + Remove-Item $path + Pop-Location + + } + + it "Compressing a relative path containing wildcard characters preserves the relative directory structure" { + $destinationPath = "TestDrive:/archive5.zip" + + Push-Location TestDrive:/ + + Compress-Archive -Path directory1/subdir1/* -DestinationPath $destinationPath + $destinationPath | Should -BeArchiveOnlyContaining @("directory1/subdir1/file.txt") -Format Zip + + Pop-Location + } - $content = "Some Data" - $content | Out-File -FilePath TestDrive:/SourceDir/Sample-1.txt - $content | Out-File -LiteralPath TestDrive:/file1[].txt + It "Validate that relative path can be specified as Path parameter of Compress-Archive cmdlet" { + $sourcePath = "./SourceDir" + $destinationPath = "RelativePathForPathParameter.zip" + try + { + Push-Location TestDrive:/ + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + Test-Path $destinationPath | Should -Be $true + } + finally + { + Pop-Location + } + } - $content = "Some Data" - $content | Out-File -FilePath TestDrive:/SourceDir/Sample-1.txt - } - - It "Accepts DestinationPath parameter with wildcard characters that resolves to one path" { - $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:/SourceDir" - $destinationPath = "TestDrive:/archive[2.zip" - - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - $destinationPath | Should -BeZipArchiveOnlyContaining @("SourceDir/", "SourceDir/Sample-1.txt") -LiteralPath - Remove-Item -LiteralPath $destinationPath -Force - } - - It "Accepts 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:/Source[]Dir" - "Some Random Content" | Out-File -LiteralPath "$sourcePath/Sample[]File.txt" - $destinationPath = "TestDrive:/archive3.zip" - try - { - Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath - $destinationPath | Should -Exist - } - finally - { - Remove-Item -LiteralPath $sourcePath -Force -Recurse - } - } - - It "Accepts LiteralPath parameter for a file with wildcards in the filename" { - $sourcePath = "TestDrive:/file1[].txt" - $destinationPath = "TestDrive:/archive4.zip" - try - { - Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath - $destinationPath | Should -BeZipArchiveOnlyContaining @("file1[].txt") - } - finally - { - Remove-Item -LiteralPath $sourcePath -Force -Recurse - } - } - } - - Context "PassThru tests" { - BeforeAll { - New-Item -Path TestDrive:/file.txt -ItemType File - } - - It "Returns an object of type System.IO.FileInfo when PassThru is specified" { - $output = Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive1.zip -PassThru - $output | Should -BeOfType System.IO.FileInfo - $destinationPath = Join-Path $TestDrive "archive1.zip" - $output.FullName | Should -Be $destinationPath - } - - It "Does not return an object when PassThru is not specified" { - $output = Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive2.zip - $output | Should -BeNullOrEmpty - } - - It "Does not return an object when PassThru is false" { - $output = Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive3.zip -PassThru:$false - $output | Should -BeNullOrEmpty - } - } - - Context "File permissions, attributes, etc. tests" { - BeforeAll { - New-Item TestDrive:/file.txt -ItemType File - "Hello, World!" | Out-File -Path TestDrive:/file.txt - - # Create a read-only file - New-Item TestDrive:/readonly.txt -ItemType File - "Hello, World!" | Out-File -Path TestDrive:/readonly.txt - } - - - It "Skips archiving a file in use" { - $fileMode = [System.IO.FileMode]::Open - $fileAccess = [System.IO.FileAccess]::Write - $fileShare = [System.IO.FileShare]::None - $archiveInUseStream = New-Object -TypeName "System.IO.FileStream" -ArgumentList "${TestDrive}/file.txt",$fileMode,$fileAccess,$fileShare - - Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive_in_use.zip -ErrorAction SilentlyContinue - # Ensure it creates an empty zip archive - "TestDrive:/archive_in_use.zip" | Should -BeZipArchiveOnlyContaining @() - - $archiveInUseStream.Dispose() - } - - It "Compresses a read-only file" { - $destinationPath = "TestDrive:/archive_with_readonly_file.zip" - Compress-Archive -Path TestDrive:/readonly.txt -DestinationPath $destinationPath - $destinationPath | Should -BeArchiveOnlyContaining @("readonly.txt") -Format Zip - } - } - - # This can be difficult to test - Context "Long path tests" -Skip { - BeforeAll { - if ($IsWindows) { - $maxPathLength = 300 - } - if ($IsLinux) { - $maxPathLength = 255 - } - if ($IsMacOS) { - $maxPathLength = 1024 - } - - function Get-MaxLengthPath { - param ( - [string] $character - ) - - $path = "${TestDrive}/" - while ($path.Length -le $maxPathLength + 10) { - $path += $character - } - return $path - } - - New-Item -Path "TestDrive:/file.txt" -ItemType File - "Hello, World!" | Out-File -FilePath "TestDrive:/file.txt" - } - - - It "Throws an error when -Path is too long" { - - } - - It "Throws an error when -LiteralPath is too long" { - - } - - It "Throws an error when -DestinationPath is too long" { - $path = "TestDrive:/file.txt" - # This will generate a path like TestDrive:/aaaaaa...aaaaaa - $destinationPath = Get-MaxLengthPath -character a - Write-Warning $destinationPath.Length - try { - Compress-Archive -Path $path -DestinationPath $destinationPath -ErrorVariable err - } catch { - throw "${$_.Exception}" - } - $destinationPath | Should -Not -Exist - } - } - - Context "CompressionLevel tests" { - BeforeAll { - New-Item -Path TestDrive:/file1.txt -ItemType File - "Hello, World!" | Out-File -FilePath TestDrive:/file1.txt - } - - It "Throws an error when an invalid value is supplied to CompressionLevel" { - try { - Compress-Archive -Path TestDrive:/file1.txt -DestinationPath TestDrive:/archive1.zip -CompressionLevel fakelevel - } catch { - $_.FullyQualifiedErrorId | Should -Be "CannotConvertArgumentNoMessage,Microsoft.PowerShell.Archive.CompressArchiveCommand" - } - } - } - - Context "Path Structure Preservation Tests" { - BeforeAll { - New-Item -Path TestDrive:/file1.txt -ItemType File - "Hello, World!" | Out-File -FilePath TestDrive:/file1.txt - - New-Item -Path TestDrive:/directory1 -ItemType Directory - New-Item -Path TestDrive:/directory1/subdir1 -ItemType Directory - New-Item -Path TestDrive:/directory1/subdir1/file.txt -ItemType File - "Hello, World!" | Out-File -FilePath TestDrive:/file.txt - } - - It "Creates an archive containing only a file when the path to that file is not relative to the working directory" { - $destinationPath = "TestDrive:/archive1.zip" - - Push-Location TestDrive:/directory1 - - Compress-Archive -Path TestDrive:/file1.txt -DestinationPath $destinationPath - $destinationPath | Should -BeArchiveOnlyContaining @("file1.txt") -Format Zip - - Pop-Location - } - - It "Creates an archive containing a file and its parent directories when the path to the file and its parent directories are descendents of the working directory" { - $destinationPath = "TestDrive:/archive2.zip" - - Push-Location TestDrive:/ - - Compress-Archive -Path directory1/subdir1/file.txt -DestinationPath $destinationPath - $destinationPath | Should -BeArchiveOnlyContaining @("directory1/subdir1/file.txt") -Format Zip - - Pop-Location - } - - It "Creates an archive containing a file and its parent directories when the path to the file and its parent directories are descendents of the working directory" { - $destinationPath = "TestDrive:/archive3.zip" - - Push-Location TestDrive:/ - - Compress-Archive -Path directory1 -DestinationPath $destinationPath - $destinationPath | Should -BeArchiveOnlyContaining @("directory1/subdir1/file.txt") -Format Zip - - Pop-Location - } - } + It "Validate that relative path can be specified as LiteralPath parameter of Compress-Archive cmdlet" { + $sourcePath = "./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:/SourceDir" + $destinationPath = "./RelativePathForDestinationPathParameter.zip" + try + { + Push-Location TestDrive:/ + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + Test-Path $destinationPath | Should -Be $true + } + finally + { + Pop-Location + } + } + } + + Context "-Format tests" { + BeforeAll { + New-Item -Path TestDrive:/file1.txt -ItemType File + "Hello, World!" | Out-File -FilePath TestDrive:/file1.txt + } + + It "Throws an error when an invalid value is supplied to -Format" { + try { + Compress-Archive -Path TestDrive:/file1.txt -DestinationPath TestDrive:/archive1 -Format fakeformat + } catch { + $_.FullyQualifiedErrorId | Should -Be "CannotConvertArgumentNoMessage,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } + + It "Creates a zip archive when -Format is not specified and the destination path does not have a matching extension" { + Compress-Archive -Path TestDrive:/file1.txt -DestinationPath TestDrive:/archive1 + "TestDrive:/archive1" | Should -BeArchiveOnlyContaining @("file1.txt") -Format Zip + } + + It "Emits a warning when DestinationPath does not have an extension that matches the value of -Format" { + Compress-Archive -Path TestDrive:/file1.txt -DestinationPath TestDrive:/archive2.tar -Format Zip -WarningVariable warnings + "TestDrive:/archive2.tar" | Should -BeArchiveOnlyContaining @("file1.txt") -Format Zip + $warnings.Count | Should -Be 1 + } + + It "Emits a warning when DestinationPath does not have an extension that matches any extension for a supported archive format" { + Compress-Archive -Path TestDrive:/file1.txt -DestinationPath TestDrive:/archive3.notmatching -WarningVariable warnings + "TestDrive:/archive3.notmatching" | Should -BeArchiveOnlyContaining @("file1.txt") -Format Zip + $warnings.Count | Should -Be 1 + } + + It "Emits a warning when DestinationPath has no extension and -Format is specified" { + Compress-Archive -Path TestDrive:/file1.txt -DestinationPath TestDrive:/archive4 -Format Zip -WarningVariable warnings + "TestDrive:/archive4" | Should -BeArchiveOnlyContaining @("file1.txt") -Format Zip + $warnings.Count | Should -Be 1 + } + + It "Emits a warning when DestinationPath has no extension and -Format is not specified" { + Compress-Archive -Path TestDrive:/file1.txt -DestinationPath TestDrive:/archive5 -WarningVariable warnings + "TestDrive:/archive5" | Should -BeArchiveOnlyContaining @("file1.txt") -Format Zip + $warnings.Count | Should -Be 1 + } + + It "Does not emit a warning when DestinationPath has an extension that matches the value supplied to -Format" -ForEach @( + @{DestinationPath = "TestDrive:/archive6.zip"; Format = "Zip"} + @{DestinationPath = "TestDrive:/archive6.tar"; Format = "Tar"} + ) { + Compress-Archive -Path TestDrive:/file1.txt -DestinationPath $DestinationPath -Format $Format -WarningVariable warnings + $DestinationPath | Should -BeArchiveOnlyContaining @("file1.txt") -Format $Format + $warnings.Count | Should -Be 0 + } + + It "Does not emit a warning when DestinationPath has a .zip extension and -Format is not specified" { + Compress-Archive -Path TestDrive:/file1.txt -DestinationPath TestDrive:/archive7.zip -WarningVariable warnings + "TestDrive:/archive4" | Should -BeArchiveOnlyContaining @("file1.txt") -Format Zip + $warnings.Count | Should -Be 0 + } + } + + Context "Pipeline tests" { + BeforeAll { + New-Item -Path TestDrive:/file1.txt -ItemType File + "Hello, World!" | Out-File -FilePath TestDrive:/file1.txt + New-Item -Path TestDrive:/file2.txt -ItemType File + "Hello, PowerShell!" | Out-File -FilePath TestDrive:/file2.txt + } + + It "Creates an archives when paths are passed via pipeline" { + $destinationPath = "TestDrive:/archive1.zip" + @("TestDrive:/file1.txt", "TestDrive:/file2.txt") | Compress-Archive -DestinationPath $destinationPath + $destinationPath | Should -BeArchiveOnlyContaining @("file1.txt", "file2.txt") -Format Zip + } + + It "Creates an archives when paths are passed to -Path via pipeline by name" { + $destinationPath = "TestDrive:/archive2.zip" + $path = [pscustomobject]@{Path = @("TestDrive:/file1.txt", "TestDrive:/file2.txt")} + $path | Compress-Archive -DestinationPath $destinationPath + $destinationPath | Should -BeArchiveOnlyContaining @("file1.txt", "file2.txt") -Format Zip + } + + It "Creates an archives when paths are passed to -LiteralPath via pipeline by name" { + $destinationPath = "TestDrive:/archive3.zip" + $path = [pscustomobject]@{LiteralPath = @("TestDrive:/file1.txt", "TestDrive:/file2.txt")} + $path | Compress-Archive -DestinationPath $destinationPath + $destinationPath | Should -BeArchiveOnlyContaining @("file1.txt", "file2.txt") -Format Zip + } + } + + Context "Large file tests" -Tag Slow { + BeforeAll { + $numberOfBytes = 512 + $bytes = [byte[]]::new($numberOfBytes) + for ($i = 0; $i -lt $numberOfBytes; $i++) { + $bytes[$i] = 1 + } + + # Create a large file containing 1's + $largeFilePath = Join-Path $TestDrive "file1" + $fileWith1s = [System.IO.File]::Create($largeFilePath) + + $numberOfTimesToWrite = 5GB / $numberOfBytes + for ($i=0; $i -lt $numberOfTimesToWrite; $i++) { + $fileWith1s.Write($bytes, 0, $numberOfBytes) + } + $fileWith1s.Close() + $f= Get-Item "TestDrive:/file1" + $f.Length | Write-Verbose -Verbose + $f.Length / 1GB | Write-Verbose -Verbose + } + + It "Creates an archive containing files > 4GB" { + Compress-Archive -Path "TestDrive:/file1" -DestinationPath "TestDrive:/archive1.zip" + $hash = Get-FileHash -Path "TestDrive:/file1" -Algorithm SHA512 + # We are comparing hashes to see if the archive matches the desired archive + $hash.Hash | Should -Be "E3676A06DFFD348EC48B56C0AA2D3BC6BB52AF6903F21AA221FF01493BB4360E58ACEC2D369F954F0739851679F1891AFC31FD630E7863105285FD6595F1E7B5" + } + } } \ No newline at end of file diff --git a/Tests/Expand-Archive.Tests.ps1 b/Tests/Expand-Archive.Tests.ps1 index 5d4d65a..206f6c1 100644 --- a/Tests/Expand-Archive.Tests.ps1 +++ b/Tests/Expand-Archive.Tests.ps1 @@ -374,7 +374,7 @@ Describe("Expand-Archive Tests") { Compress-Archive -Path "TestDrive:/DirectoryToArchive" -DestinationPath (Add-FileExtensionBasedOnFormat "TestDrive:/archive2" -Format $Format) # Create an archive containing a file and an empty folder - Compress-Archive -Path "TestDrive:/file1.txt","TestDrive:/DirectoryToArchive" -DestinationPath (Add-FileExtensionBasedOnFormat "TestDrive:/archive3" -Format $Format) + Compress-Archive -Path "TestDrive:/file1.txt","TestDrive:/DirectoryToArchive" -DestinationPath "TestDrive:/archive3" } It "Expands an archive when a non-existent directory is specified as -DestinationPath with format " { @@ -414,7 +414,7 @@ Describe("Expand-Archive Tests") { Pop-Location } - It "Expands an archive containing a single top-level directory and no other top-level items to a directory with that directory's name when -DestinationPath is not specified" { + It "Expands an archive to a directory with that archive's name when -DestinationPath is not specified" { $sourcePath = Add-FileExtensionBasedOnFormat "TestDrive:/archive2" -Format $Format $destinationPath = "TestDrive:/directory4" @@ -423,30 +423,31 @@ Describe("Expand-Archive Tests") { Expand-Archive -Path $sourcePath $itemsInDestinationPath = Get-ChildItem $destinationPath -Recurse - $itemsInDestinationPath.Count | Should -Be 1 - $itemsInDestinationPath[0].Name | Should -Be "DirectoryToArchive" - - Pop-Location - } + $itemsInDestinationPath.Count | Should -Be 2 - It "Expands an archive containing multiple top-level items to a directory with that archive's name when -DestinationPath is not specified" { - $sourcePath = Add-FileExtensionBasedOnFormat "TestDrive:/archive3" -Format $Format - $destinationPath = "TestDrive:/directory5" - - Push-Location $destinationPath - - Expand-Archive -Path $sourcePath + $directoryContents = @() + $directoryContents += $itemsInDestinationPath[0].FullName + $directoryContents += $itemsInDestinationPath[1].FullName - $itemsInDestinationPath = Get-ChildItem $destinationPath -Name -Recurse - $itemsInDestinationPath.Count | Should -Be 3 - "archive3" | Should -BeIn $itemsInDestinationPath - (Join-Path "archive3" "DirectoryToArchive") | Should -BeIn $itemsInDestinationPath - (Join-Path "archive3" "file1.txt") | Should -BeIn $itemsInDestinationPath - + $directoryContents | Should -Contain (Join-Path $TestDrive "directory4/archive2") + $directoryContents | Should -Contain (Join-Path $TestDrive "directory4/archive2/DirectoryToArchive") Pop-Location } + It "Throws an error when expanding an archive whose name does not have an extension and -DestinationPath is not specified" { + Push-Location "TestDrive:/" + try { + Expand-Archive -Path "TestDrive:/archive3" + } + catch { + $_.FullyQualifiedErrorId | Should -Be "CannotDetermineDestinationPath,${CmdletClassName}" + } + finally { + Pop-Location + } + } + It "Expands an archive containing multiple files, non-empty directories, and empty directories" { # Create an archive containing multiple files, non-empty directories, and empty directories diff --git a/src/PathHelper.cs b/src/PathHelper.cs index 87dd646..c3ceeec 100644 --- a/src/PathHelper.cs +++ b/src/PathHelper.cs @@ -22,6 +22,9 @@ internal class PathHelper internal WildcardPattern? _wildCardPattern; + // These are the paths to add + internal HashSet? _fullyQualifiedPaths; + internal PathHelper(PSCmdlet cmdlet) { _cmdlet = cmdlet; @@ -40,6 +43,7 @@ internal List GetArchiveAdditions(HashSet fullyQualifie Debug.Assert(Path.IsPathFullyQualified(path)); AddAdditionForFullyQualifiedPath(path, archiveAdditions, entryName: null, parentMatchesFilter: false); } + return archiveAdditions; } @@ -171,6 +175,9 @@ private string GetEntryName(FileSystemInfo fileSystemInfo, out bool doesPreserve Debug.Assert(relativePath is not null); doesPreservePathStructure = true; entryName = relativePath; + + // In case the relative path contains parent directories that have not been entered by the user, + // check for these paths and add them } // Otherwise, return the name of the directory or file else @@ -246,17 +253,23 @@ private static string GetPrefixForPath(System.IO.DirectoryInfo directoryInfo) private bool TryGetPathRelativeToCurrentWorkingDirectory(string path, out string? relativePathToWorkingDirectory) { Debug.Assert(!string.IsNullOrEmpty(path)); - string? workingDirectoryRoot = Path.GetPathRoot(_cmdlet.SessionState.Path.CurrentFileSystemLocation.Path); + string workingDirectory = _cmdlet.SessionState.Path.CurrentFileSystemLocation.ProviderPath; + string? workingDirectoryRoot = Path.GetPathRoot(workingDirectory); string? pathRoot = Path.GetPathRoot(path); if (workingDirectoryRoot != pathRoot) { relativePathToWorkingDirectory = null; return false; } - string relativePath = Path.GetRelativePath(_cmdlet.SessionState.Path.CurrentFileSystemLocation.Path, path); + string relativePath = Path.GetRelativePath(workingDirectory, path); relativePathToWorkingDirectory = relativePath.Contains("..") ? null : relativePath; return relativePathToWorkingDirectory is not null; } + // Adds the parent directories in a path to the list of fully qualified paths + private void AddParentDirectoriesToFullyQualifiedPaths(string path) { + + } + internal System.Collections.ObjectModel.Collection? GetResolvedPathFromPSProviderPath(string path, bool pathMustExist) { // Keep the exception at the top, then when an error occurs, use the exception to create an ErrorRecord Exception? exception = null; From bb3d9092c641152647ffeac7123d2fcbf908d668 Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Tue, 16 Aug 2022 18:56:39 -0700 Subject: [PATCH 31/34] added more tests for Compress-Archive, large file tests, Flatten tests, etc --- Tests/Compress-Archive.Tests.ps1 | 339 ++++++++++++++++++------------- src/PathHelper.cs | 30 ++- 2 files changed, 226 insertions(+), 143 deletions(-) diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index f04a720..21a8ead 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -169,20 +169,6 @@ Describe("Microsoft.PowerShell.Archive tests") { } } - # 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:/SourceDir/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 { - $_.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:/SourceDir/Sample-1.txt" $destinationPath = $sourcePath @@ -207,18 +193,6 @@ Describe("Microsoft.PowerShell.Archive tests") { } } - It "Throws an error when LiteralPath and DestinationPath are the same" -Skip { - $sourcePath = "TestDrive:/SourceDir/Sample-1.txt" - $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:/SourceDir/Sample-1.txt" $destinationPath = $sourcePath @@ -388,101 +362,131 @@ Describe("Microsoft.PowerShell.Archive tests") { } } - Context "Zip-specific tests" { - BeforeAll { - # Create a file whose last write time is before 1980 - $content | Out-File -FilePath TestDrive:/OldFile.txt - Set-ItemProperty -Path TestDrive:/OldFile.txt -Name LastWriteTime -Value '1974-01-16 14:44' + Context "Zip-specific tests" -Tag Slow { + BeforeAll { + # Create a file whose last write time is before 1980 + $content | Out-File -FilePath TestDrive:/OldFile.txt + Set-ItemProperty -Path TestDrive:/OldFile.txt -Name LastWriteTime -Value '1974-01-16 14:44' - # Create a directory whose last write time is before 1980 - New-Item -Path "TestDrive:/olddirectory" -ItemType Directory - Set-ItemProperty -Path "TestDrive:/olddirectory" -Name "LastWriteTime" -Value '1974-01-16 14:44' - } + # Create a directory whose last write time is before 1980 + New-Item -Path "TestDrive:/olddirectory" -ItemType Directory + Set-ItemProperty -Path "TestDrive:/olddirectory" -Name "LastWriteTime" -Value '1974-01-16 14:44' - It "Compresses a file whose last write time is before 1980" { - $sourcePath = "TestDrive:/OldFile.txt" - $destinationPath = "${TestDrive}/archive11.zip" + + $numberOfBytes = 512 + $bytes = [byte[]]::new($numberOfBytes) + for ($i = 0; $i -lt $numberOfBytes; $i++) { + $bytes[$i] = 1 + } - # Assert the last write time of the file is before 1980 - $dateProperty = Get-ItemPropertyValue -Path $sourcePath -Name "LastWriteTime" - $dateProperty.Year | Should -BeLessThan 1980 + # Create a large file containing 1's + $largeFilePath = Join-Path $TestDrive "file1" + $fileWith1s = [System.IO.File]::Create($largeFilePath) + + $numberOfTimesToWrite = 5GB / $numberOfBytes + for ($i=0; $i -lt $numberOfTimesToWrite; $i++) { + $fileWith1s.Write($bytes, 0, $numberOfBytes) + } + $fileWith1s.Close() + } - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - $destinationPath | Should -BeZipArchiveOnlyContaining @('OldFile.txt') - - # Get the archive - $fileMode = [System.IO.FileMode]::Open - $archiveStream = New-Object -TypeName System.IO.FileStream -ArgumentList $destinationPath,$fileMode - $zipArchiveMode = [System.IO.Compression.ZipArchiveMode]::Read - $archive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $archiveStream,$zipArchiveMode - $entry = $archive.GetEntry("OldFile.txt") - $entry | Should -Not -BeNullOrEmpty - - $entry.LastWriteTime.Year | Should -BeExactly 1980 - $entry.LastWriteTime.Month| Should -BeExactly 1 - $entry.LastWriteTime.Day | Should -BeExactly 1 - $entry.LastWriteTime.Hour | Should -BeExactly 0 - $entry.LastWriteTime.Minute | Should -BeExactly 0 - $entry.LastWriteTime.Second | Should -BeExactly 0 - $entry.LastWriteTime.Millisecond | Should -BeExactly 0 - - - $archive.Dispose() - $archiveStream.Dispose() - } + It "Compresses a file whose last write time is before 1980" { + $sourcePath = "TestDrive:/OldFile.txt" + $destinationPath = "${TestDrive}/archive11.zip" - It "Compresses a directory whose last write time is before 1980 with format " { - $sourcePath = "TestDrive:/olddirectory" - $destinationPath = "${TestDrive}/archive12.zip" + # Assert the last write time of the file is before 1980 + $dateProperty = Get-ItemPropertyValue -Path $sourcePath -Name "LastWriteTime" + $dateProperty.Year | Should -BeLessThan 1980 - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath - $destinationPath | Should -BeZipArchiveOnlyContaining @('olddirectory/') - - # Get the archive - $fileMode = [System.IO.FileMode]::Open - $archiveStream = New-Object -TypeName System.IO.FileStream -ArgumentList $destinationPath,$fileMode - $zipArchiveMode = [System.IO.Compression.ZipArchiveMode]::Read - $archive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $archiveStream,$zipArchiveMode - $entry = $archive.GetEntry("olddirectory/") - $entry | Should -Not -BeNullOrEmpty - - $entry.LastWriteTime.Year | Should -BeExactly 1980 - $entry.LastWriteTime.Month| Should -BeExactly 1 - $entry.LastWriteTime.Day | Should -BeExactly 1 - $entry.LastWriteTime.Hour | Should -BeExactly 0 - $entry.LastWriteTime.Minute | Should -BeExactly 0 - $entry.LastWriteTime.Second | Should -BeExactly 0 - $entry.LastWriteTime.Millisecond | Should -BeExactly 0 - - - $archive.Dispose() - $archiveStream.Dispose() - } + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + $destinationPath | Should -BeZipArchiveOnlyContaining @('OldFile.txt') - It "Writes a warning when compressing a file whose last write time is before 1980 with format " { - $sourcePath = "TestDrive:/OldFile.txt" - $destinationPath = "${TestDrive}/archive13.zip" + # Get the archive + $fileMode = [System.IO.FileMode]::Open + $archiveStream = New-Object -TypeName System.IO.FileStream -ArgumentList $destinationPath,$fileMode + $zipArchiveMode = [System.IO.Compression.ZipArchiveMode]::Read + $archive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $archiveStream,$zipArchiveMode + $entry = $archive.GetEntry("OldFile.txt") + $entry | Should -Not -BeNullOrEmpty + + $entry.LastWriteTime.Year | Should -BeExactly 1980 + $entry.LastWriteTime.Month| Should -BeExactly 1 + $entry.LastWriteTime.Day | Should -BeExactly 1 + $entry.LastWriteTime.Hour | Should -BeExactly 0 + $entry.LastWriteTime.Minute | Should -BeExactly 0 + $entry.LastWriteTime.Second | Should -BeExactly 0 + $entry.LastWriteTime.Millisecond | Should -BeExactly 0 + + + $archive.Dispose() + $archiveStream.Dispose() + } - # Assert the last write time of the file is before 1980 - $dateProperty = Get-ItemPropertyValue -Path $sourcePath -Name "LastWriteTime" - $dateProperty.Year | Should -BeLessThan 1980 + It "Compresses a directory whose last write time is before 1980 with format " { + $sourcePath = "TestDrive:/olddirectory" + $destinationPath = "${TestDrive}/archive12.zip" - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WarningVariable warnings - $warnings.Length | Should -Be 1 - } + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + $destinationPath | Should -BeZipArchiveOnlyContaining @('olddirectory/') - It "Writes a warning when compresing a directory whose last write time is before 1980 with format " { - $sourcePath = "TestDrive:/olddirectory" - $destinationPath = "${TestDrive}/archive14.zip" + # Get the archive + $fileMode = [System.IO.FileMode]::Open + $archiveStream = New-Object -TypeName System.IO.FileStream -ArgumentList $destinationPath,$fileMode + $zipArchiveMode = [System.IO.Compression.ZipArchiveMode]::Read + $archive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $archiveStream,$zipArchiveMode + $entry = $archive.GetEntry("olddirectory/") + $entry | Should -Not -BeNullOrEmpty + + $entry.LastWriteTime.Year | Should -BeExactly 1980 + $entry.LastWriteTime.Month| Should -BeExactly 1 + $entry.LastWriteTime.Day | Should -BeExactly 1 + $entry.LastWriteTime.Hour | Should -BeExactly 0 + $entry.LastWriteTime.Minute | Should -BeExactly 0 + $entry.LastWriteTime.Second | Should -BeExactly 0 + $entry.LastWriteTime.Millisecond | Should -BeExactly 0 + + + $archive.Dispose() + $archiveStream.Dispose() + } - # Assert the last write time of the file is before 1980 - $dateProperty = Get-ItemPropertyValue -Path $sourcePath -Name "LastWriteTime" - $dateProperty.Year | Should -BeLessThan 1980 + It "Writes a warning when compressing a file whose last write time is before 1980 with format " { + $sourcePath = "TestDrive:/OldFile.txt" + $destinationPath = "${TestDrive}/archive13.zip" - Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WarningVariable warnings - $warnings.Length | Should -Be 1 - } - } + # Assert the last write time of the file is before 1980 + $dateProperty = Get-ItemPropertyValue -Path $sourcePath -Name "LastWriteTime" + $dateProperty.Year | Should -BeLessThan 1980 + + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WarningVariable warnings + $warnings.Length | Should -Be 1 + } + + It "Writes a warning when compresing a directory whose last write time is before 1980 with format " { + $sourcePath = "TestDrive:/olddirectory" + $destinationPath = "${TestDrive}/archive14.zip" + + # Assert the last write time of the file is before 1980 + $dateProperty = Get-ItemPropertyValue -Path $sourcePath -Name "LastWriteTime" + $dateProperty.Year | Should -BeLessThan 1980 + + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -WarningVariable warnings + $warnings.Length | Should -Be 1 + } + + It "Creates an archive containing files > 4GB" { + (Get-Item "TestDrive:/file1").Length | Should -BeGreaterThan 4GB + Compress-Archive -Path "TestDrive:/file1" -DestinationPath "TestDrive:/archive1.zip" + "TestDrive:/archive1.zip" | Should -BeArchiveOnlyContaining @("file1") -Format Zip + } + + It "Creates an an archive > 4GB in size" { + $destinationPath = "TestDrive:/archive2.zip" + Compress-Archive -Path "TestDrive:/file1" -DestinationPath $destinationPath -CompressionLevel NoCompression + $destinationPath | Should -BeArchiveOnlyContaining @("file1") -Format Zip + (Get-Item $destinationPath).Length | Should -BeGreaterThan 4GB + } + } Context "DestinationPath and -WriteMode Overwrite tests" { BeforeAll { @@ -640,10 +644,6 @@ Describe("Microsoft.PowerShell.Archive tests") { # Ensure the original entries and different than the new entries $destinationPath | Should -BeZipArchiveOnlyContaining @("Sample-2.txt") } - } - - Context "Relative Path tests" { - } Context "Special and Wildcard Characters Tests" { @@ -758,7 +758,6 @@ Describe("Microsoft.PowerShell.Archive tests") { } } - It "Skips archiving a file in use" { $fileMode = [System.IO.FileMode]::Open $fileAccess = [System.IO.FileAccess]::Write @@ -810,6 +809,13 @@ Describe("Microsoft.PowerShell.Archive tests") { BeforeAll { New-Item -Path TestDrive:/file1.txt -ItemType File "Hello, World!" | Out-File -FilePath TestDrive:/file1.txt + + # Compress a file with different CompressionLevel values + $path = Join-Path $ScriptRoot "Sample-File" + Compress-Archive -Path $path -DestinationPath TestDrive:/archive1.zip -CompressionLevel Optimal + Compress-Archive -Path $path -DestinationPath TestDrive:/archive2.zip -CompressionLevel NoCompression + Compress-Archive -Path $path -DestinationPath TestDrive:/archive3.zip -CompressionLevel Fastest + Compress-Archive -Path $path -DestinationPath TestDrive:/archive4.zip -CompressionLevel SmallestSize } It "Throws an error when an invalid value is supplied to CompressionLevel" { @@ -819,6 +825,14 @@ Describe("Microsoft.PowerShell.Archive tests") { $_.FullyQualifiedErrorId | Should -Be "CannotConvertArgumentNoMessage,Microsoft.PowerShell.Archive.CompressArchiveCommand" } } + + It "Creates an archive with -CompressionLevel" -ForEach @( + @{CompressionLevel = [System.IO.Compression.CompressionLevel]::Optimal} + ) { + + } + + } Context "Path Structure Preservation Tests" { @@ -1036,32 +1050,85 @@ Describe("Microsoft.PowerShell.Archive tests") { } Context "Large file tests" -Tag Slow { + + } + + Context "Update tests" { BeforeAll { - $numberOfBytes = 512 - $bytes = [byte[]]::new($numberOfBytes) - for ($i = 0; $i -lt $numberOfBytes; $i++) { - $bytes[$i] = 1 - } + New-Item -Path TestDrive:/file1.txt -ItemType File + "Hello, World!" | Out-File -FilePath TestDrive:/file1.txt + New-Item -Path TestDrive:/file2.txt -ItemType File + "Hello, PowerShell!" | Out-File -FilePath TestDrive:/file2.txt - # Create a large file containing 1's - $largeFilePath = Join-Path $TestDrive "file1" - $fileWith1s = [System.IO.File]::Create($largeFilePath) - - $numberOfTimesToWrite = 5GB / $numberOfBytes - for ($i=0; $i -lt $numberOfTimesToWrite; $i++) { - $fileWith1s.Write($bytes, 0, $numberOfBytes) - } - $fileWith1s.Close() - $f= Get-Item "TestDrive:/file1" - $f.Length | Write-Verbose -Verbose - $f.Length / 1GB | Write-Verbose -Verbose + Compress-Archive -Path TestDrive:/file1.txt -DestinationPath TestDrive:/archive1.zip + Compress-Archive -Path TestDrive:/file1.txt -DestinationPath TestDrive:/archive_to_append.zip + Compress-Archive -Path TestDrive:/file1.txt -DestinationPath TestDrive:/archive_to_update.zip + + # Create an archive containing a directory for updating + New-Item -Path TestDrive:/directory1 -ItemType Directory + New-Item -Path TestDrive:/directory1/file1.txt -ItemType File + + Compress-Archive -Path TestDrive:/directory1 -DestinationPath TestDrive:/archive_with_directory.zip + + New-Item -Path TestDrive:/directory1/file2.txt -ItemType File } - It "Creates an archive containing files > 4GB" { - Compress-Archive -Path "TestDrive:/file1" -DestinationPath "TestDrive:/archive1.zip" - $hash = Get-FileHash -Path "TestDrive:/file1" -Algorithm SHA512 - # We are comparing hashes to see if the archive matches the desired archive - $hash.Hash | Should -Be "E3676A06DFFD348EC48B56C0AA2D3BC6BB52AF6903F21AA221FF01493BB4360E58ACEC2D369F954F0739851679F1891AFC31FD630E7863105285FD6595F1E7B5" + It "Does not throw an error when -Update is specified and the archive already exists" { + Compress-Archive -Path TestDrive:/file2.txt -DestinationPath TestDrive:/archive1.zip -WriteMode Update -ErrorVariable errors + $errors.Count | Should -Be 0 + } + + It "Appends a file to an archive when -WriteMode Update is specified" { + Compress-Archive -Path TestDrive:/file2.txt -DestinationPath TestDrive:/archive_to_append.zip -WriteMode Update + "TestDrive:/archive_to_append.zip" | Should -BeArchiveOnlyContaining @("file1.txt", "file2.txt") -Format Zip + } + + It "Modifies a pre-existing file in an archive when -WriteMode Update is specified" { + "File has been modified." | Out-File -FilePath TestDrive:/file1.txt + Compress-Archive -Path TestDrive:/file1.txt -DestinationPath TestDrive:/archive_to_update.zip -WriteMode Update + "TestDrive:/archive_to_update.zip" | Should -BeArchiveOnlyContaining @("file1.txt") -Format Zip + + # Expand the archive and ensure it has the new contents + Expand-Archive -Path TestDrive:/archive_to_update.zip -DestinationPath TestDrive:/archive_to_update_contents + + Get-Content TestDrive:/archive_to_update_contents/file1.txt | Should -Be "File has been modified." + } + + It "Adds directory's children when updating a directory in the archive" { + $archivePath = "TestDrive:/archive_with_directory.zip" + Compress-Archive -Path TestDrive:/directory1 -DestinationPath $archivePath -WriteMode Update + $archivePath | Should -BeArchiveOnlyContaining @("directory1/", "directory1/file1.txt", "directory1/file2.txt") -Format Zip + } + } + + Context "Flatten tests" { + BeforeAll { + New-Item -Path TestDrive:/directory1 -ItemType Directory + New-Item -Path TestDrive:/directory1/file1.txt -ItemType File + "Hello, World!" | Out-File -FilePath TestDrive:/directory1/file1.txt + + New-Item -Path TestDrive:/directory2 -ItemType Directory + New-Item -Path TestDrive:/directory2/file1.txt -ItemType File + "Hello, PowerShell!" | Out-File -FilePath TestDrive:/directory2/file1.txt + } + + It "Creates a flat archive with -Flatten" { + $path = "TestDrive:/directory1" + $destinationPath = "TestDrive:/archive1.zip" + + Compress-Archive -Path $path -DestinationPath $destinationPath -Flatten + $x = Convert-Path $destinationPath + 7z l "${x}" | Write-Verbose -Verbose + $destinationPath | Should -BeArchiveOnlyContaining @("file1.txt") -Format Zip + } + + It "Does not throw an error when multiple files have the same entry name when -Flatten is specified" { + $path = "TestDrive:/directory1","TestDrive:/directory2" + $destinationPath = "TestDrive:/archive2.zip" + + Compress-Archive -Path $path -DestinationPath $destinationPath -Flatten -ErrorVariable errors + errors.Count | Should -Be 0 + $destinationPath | Should -BeArchiveOnlyContaining @("file1.txt") -Format Zip } } } \ No newline at end of file diff --git a/src/PathHelper.cs b/src/PathHelper.cs index c3ceeec..1bff480 100644 --- a/src/PathHelper.cs +++ b/src/PathHelper.cs @@ -25,6 +25,9 @@ internal class PathHelper // These are the paths to add internal HashSet? _fullyQualifiedPaths; + // This is used only when flattening to track entry names, so duplicate entry names can be removed + internal HashSet? _entryNames; + internal PathHelper(PSCmdlet cmdlet) { _cmdlet = cmdlet; @@ -34,6 +37,7 @@ internal List GetArchiveAdditions(HashSet fullyQualifie { if (Filter is not null) { _wildCardPattern = new WildcardPattern(Filter); + _entryNames = new HashSet(); } List archiveAdditions = new List(fullyQualifiedPaths.Count); foreach (var path in fullyQualifiedPaths) @@ -44,6 +48,8 @@ internal List GetArchiveAdditions(HashSet fullyQualifie AddAdditionForFullyQualifiedPath(path, archiveAdditions, entryName: null, parentMatchesFilter: false); } + // If the mode is flatten, there could be + return archiveAdditions; } @@ -74,13 +80,15 @@ private void AddAdditionForFullyQualifiedPath(string path, List } bool doesMatchFilter = true; - if (!parentMatchesFilter && _wildCardPattern is not null) { + if (!parentMatchesFilter && _wildCardPattern is not null) + { doesMatchFilter = _wildCardPattern.IsMatch(fileSystemInfo.Name); } // if entryName, then set it as the entry name of the file or directory in the archive // The entry name will preserve the directory structure as long as the path is relative to the working directory - if (entryName is null) { + if (entryName is null) + { entryName = GetEntryName(fileSystemInfo, out bool doesPreservePathStructure); } @@ -101,8 +109,12 @@ private void AddAdditionForFullyQualifiedPath(string path, List // If the item being added is a directory and does not have any descendent files that match the filter, finalAdditions - initialAdditions = 0 // If the item being added is a directory and has descendent files that match the filter, finalAdditions > initialAdditions - if (doesMatchFilter || (!doesMatchFilter && finalAdditions - initialAdditions > 0)) { - additions.Add(new ArchiveAddition(entryName: entryName, fileSystemInfo: fileSystemInfo)); + if (doesMatchFilter || (!doesMatchFilter && finalAdditions - initialAdditions > 0) && (Flatten && fileSystemInfo is not DirectoryInfo)) { + if (!Flatten || (_entryNames is not null && _entryNames.Add(entryName))) + { + additions.Add(new ArchiveAddition(entryName: entryName, fileSystemInfo: fileSystemInfo)); + } + } } @@ -140,13 +152,17 @@ private void AddDescendentEntries(System.IO.DirectoryInfo directoryInfo, List? GetResolvedPathFromPSProviderPath(string path, bool pathMustExist) { From cb4829ca7194767678298dc62abf7a9c9edf755f Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Tue, 16 Aug 2022 21:47:26 -0700 Subject: [PATCH 32/34] added tests for Expand-Archive --- .../Should-BeArchiveOnlyContaining.psm1 | 8 + Tests/Compress-Archive.Tests.ps1 | 31 ++-- Tests/Expand-Archive.Tests.ps1 | 137 ++++++++++++++++++ src/Cmdlets/ExpandArchiveCommand.cs | 10 +- src/Error.cs | 5 +- src/Localized/Messages.resx | 3 + src/PathHelper.cs | 23 +-- 7 files changed, 182 insertions(+), 35 deletions(-) diff --git a/Tests/Assertions/Should-BeArchiveOnlyContaining.psm1 b/Tests/Assertions/Should-BeArchiveOnlyContaining.psm1 index 0ef8d23..3772ffc 100644 --- a/Tests/Assertions/Should-BeArchiveOnlyContaining.psm1 +++ b/Tests/Assertions/Should-BeArchiveOnlyContaining.psm1 @@ -24,6 +24,14 @@ function Should-BeArchiveOnlyContaining { } if ($Format -eq "Tar") { return Should-BeTarArchiveOnlyContaining -ActualValue $ActualValue -ExpectedValue $ExpectedValue -Negate:$Negate -Because $Because -LiteralPath:$LiteralPath -CallerSessionState $CallerSessionState + } + if ($Format -eq "Tgz") { + # Get a temp file + $gzipFolderPath = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName()) + New-Item -Path $gzipFolderPath -ItemType Directory + "7z e $ActualValue -o${gzipFolderPath} -tgzip" | Invoke-Expression + $tarFilePath = (Get-ChildItem $gzipFolderPath)[0].FullName + return Should-BeTarArchiveOnlyContaining -ActualValue $tarFilePath -ExpectedValue $ExpectedValue -Negate:$Negate -Because $Because -LiteralPath:$LiteralPath -CallerSessionState $CallerSessionState } return [pscustomobject]@{ Succeeded = $false diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index 21a8ead..bd98576 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -22,10 +22,13 @@ Describe("Microsoft.PowerShell.Archive tests") { ) if ($Format -eq "Zip") { - return $Path += ".zip" + return $Path += ".zip" } if ($Format -eq "Tar") { - return $Path += ".tar" + return $Path += ".tar" + } + if ($Format -eq "Tgz") { + return $Path += ".tar.gz" } throw "Format type is not supported" } @@ -261,7 +264,8 @@ Describe("Microsoft.PowerShell.Archive tests") { Context "Basic functional tests" -ForEach @( @{Format = "Zip"}, - @{Format = "Tar"} + @{Format = "Tar"}, + @{Format = "Tgz"} ) { BeforeAll { New-Item TestDrive:/SourceDir -Type Directory | Out-Null @@ -290,7 +294,7 @@ Describe("Microsoft.PowerShell.Archive tests") { $destinationPath | Should -BeArchiveOnlyContaining @('Sample-2.txt') -Format $Format } - It "Compresses a non-empty directory with format " -Tag td1 { + It "Compresses a non-empty directory with format " { $sourcePath = "TestDrive:/SourceDir/ChildDir-1" $destinationPath = Add-FileExtensionBasedOnFormat -Path "TestDrive:/archive2" -Format $Format Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Format $Format @@ -809,13 +813,6 @@ Describe("Microsoft.PowerShell.Archive tests") { BeforeAll { New-Item -Path TestDrive:/file1.txt -ItemType File "Hello, World!" | Out-File -FilePath TestDrive:/file1.txt - - # Compress a file with different CompressionLevel values - $path = Join-Path $ScriptRoot "Sample-File" - Compress-Archive -Path $path -DestinationPath TestDrive:/archive1.zip -CompressionLevel Optimal - Compress-Archive -Path $path -DestinationPath TestDrive:/archive2.zip -CompressionLevel NoCompression - Compress-Archive -Path $path -DestinationPath TestDrive:/archive3.zip -CompressionLevel Fastest - Compress-Archive -Path $path -DestinationPath TestDrive:/archive4.zip -CompressionLevel SmallestSize } It "Throws an error when an invalid value is supplied to CompressionLevel" { @@ -825,14 +822,6 @@ Describe("Microsoft.PowerShell.Archive tests") { $_.FullyQualifiedErrorId | Should -Be "CannotConvertArgumentNoMessage,Microsoft.PowerShell.Archive.CompressArchiveCommand" } } - - It "Creates an archive with -CompressionLevel" -ForEach @( - @{CompressionLevel = [System.IO.Compression.CompressionLevel]::Optimal} - ) { - - } - - } Context "Path Structure Preservation Tests" { @@ -1117,8 +1106,6 @@ Describe("Microsoft.PowerShell.Archive tests") { $destinationPath = "TestDrive:/archive1.zip" Compress-Archive -Path $path -DestinationPath $destinationPath -Flatten - $x = Convert-Path $destinationPath - 7z l "${x}" | Write-Verbose -Verbose $destinationPath | Should -BeArchiveOnlyContaining @("file1.txt") -Format Zip } @@ -1127,7 +1114,7 @@ Describe("Microsoft.PowerShell.Archive tests") { $destinationPath = "TestDrive:/archive2.zip" Compress-Archive -Path $path -DestinationPath $destinationPath -Flatten -ErrorVariable errors - errors.Count | Should -Be 0 + $errors.Count | Should -Be 0 $destinationPath | Should -BeArchiveOnlyContaining @("file1.txt") -Format Zip } } diff --git a/Tests/Expand-Archive.Tests.ps1 b/Tests/Expand-Archive.Tests.ps1 index 206f6c1..1620f27 100644 --- a/Tests/Expand-Archive.Tests.ps1 +++ b/Tests/Expand-Archive.Tests.ps1 @@ -645,6 +645,143 @@ Describe("Expand-Archive Tests") { } Context "Large File Tests" { + BeforeAll { + $numberOfBytes = 512 + $bytes = [byte[]]::new($numberOfBytes) + for ($i = 0; $i -lt $numberOfBytes; $i++) { + $bytes[$i] = 1 + } + + # Create a large file containing 1's + $largeFilePath = Join-Path $TestDrive "file1" + $fileWith1s = [System.IO.File]::Create($largeFilePath) + + $numberOfTimesToWrite = 5GB / $numberOfBytes + for ($i=0; $i -lt $numberOfTimesToWrite; $i++) { + $fileWith1s.Write($bytes, 0, $numberOfBytes) + } + $fileWith1s.Close() + + Compress-Archive -Path TestDrive:/file1 -DestinationPath TestDrive:/large_entry_archive.zip + Compress-Archive -Path TestDrive:/file1 -DestinationPath TestDrive:/large_archive.zip -CompressionLevel NoCompression + } + + It "Expands an archive whose size is > 4GB" { + $destinationPath = "TestDrive:/large_archive_output" + { Expand-Archive -Path TestDrive:/large_archive.zip -DestinationPath $destinationPath } | Should -Not -Throw + $childItems = Get-ChildItem $destinationPath + $childItems.Count | Should -Be 1 + $childItems[0].Name | Should -Be "file1" + } + } + + Context "Pipeline tests" { + BeforeAll { + New-Item TestDrive:/file.txt -ItemType File + "Hello, World!" | Out-File -Path TestDrive:/file.txt + + + Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive1.zip + Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive2.zip + } + + It "Expands an archive when -Path is passed by pipeline" { + $destinationPath = "TestDrive:/archive_output1" + "TestDrive:/archive1.zip" | Expand-Archive -DestinationPath $destinationPath + $childItems = Get-ChildItem $destinationPath + $childItems.Count | Should -Be 1 + $childItems[0].FullName | Should -Be (Join-Path $TestDrive "archive_output1" "file.txt") + } + + It "Expands an archive when -Path is passed by pipeline by name" { + $destinationPath = "TestDrive:/archive_output2" + $path = [pscustomobject]@{Path = "TestDrive:/archive1.zip"} + $path | Expand-Archive -DestinationPath $destinationPath + $childItems = Get-ChildItem $destinationPath -Verbose + $childItems.Count | Should -Be 1 + $childItems[0].FullName | Should -Be (Join-Path $TestDrive "archive_output2" "file.txt") + } + + It "Throws an error when multiple paths are passed by pipeline" { + try { + $destinationPath = "TestDrive:/archive_output3" + $path = @("TestDrive:/archive1.zip", "TestDrive:/archive2.zip") + $path | Expand-Archive -DestinationPath $destinationPath + } + catch { + $_.FullyQualifiedErrorId | Should -Be "MultplePathsPassed,${CmdletClassName}" + } + } + } + + Context "Relative Path Tests" { + BeforeAll { + New-Item TestDrive:/file.txt -ItemType File + "Hello, World!" | Out-File -Path TestDrive:/file.txt + + New-Item -Path TestDrive:/directory1 -ItemType Directory + Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/directory1/archive1.zip + Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive2.zip + New-Item -Path TestDrive:/directory2 -ItemType Directory + } + + It "Expands an archive when -Path is a relative path" { + Push-Location TestDrive:/directory1 + $destinationPath = "TestDrive:/relative_path_directory" + Expand-Archive -Path archive1.zip -DestinationPath $destinationPath + $childItems = Get-ChildItem $destinationPath -Verbose + $childItems.Count | Should -Be 1 + $childItems[0].Name | Should -Be "file.txt" + Pop-Location + } + It "Expands an archive when -LiteralPath is a relative path" { + Push-Location TestDrive:/directory1 + $destinationPath = "TestDrive:/relative_literal_path_directory" + Expand-Archive -LiteralPath archive1.zip -DestinationPath $destinationPath + $childItems = Get-ChildItem $destinationPath -Verbose + $childItems.Count | Should -Be 1 + $childItems[0].Name | Should -Be "file.txt" + Pop-Location + } + + It "Expands an archive when -DestinationPath is a relative path" { + Push-Location TestDrive:/directory2 + $destinationPath = "destination_path_output" + Expand-Archive -Path "TestDrive:/directory1/archive1.zip" -DestinationPath $destinationPath + $childItems = Get-ChildItem $destinationPath -Verbose + $childItems.Count | Should -Be 1 + $childItems[0].Name | Should -Be "file.txt" + Pop-Location + } + } + + Context "-Format tests" { + BeforeAll { + New-Item TestDrive:/file.txt -ItemType File + "Hello, World!" | Out-File -Path TestDrive:/file.txt + Compress-Archive -Path TestDrive:/file.txt -DestinationPath TestDrive:/archive1.zip + } + + It "Throws an error when an invalid value is supplied to -Format" { + try { + Expand-Archive -Path TestDrive:/archive1.zip -DestinationPath TestDrive:/output_directory + } catch { + $_.FullyQualifiedErrorId | Should -Be "CannotConvertArgumentNoMessage,${CmdletClassName}" + } + } + } + + Context "Module tests" { + It "Validate module can be imported when current language is not en-US" { + $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/src/Cmdlets/ExpandArchiveCommand.cs b/src/Cmdlets/ExpandArchiveCommand.cs index 3405d0a..c8a75fb 100644 --- a/src/Cmdlets/ExpandArchiveCommand.cs +++ b/src/Cmdlets/ExpandArchiveCommand.cs @@ -50,6 +50,8 @@ private enum ParameterSet { private string? _sourcePath; + private bool _didCallProcessRecord; + #endregion public ExpandArchiveCommand() @@ -64,7 +66,13 @@ protected override void BeginProcessing() protected override void ProcessRecord() { - + if (_didCallProcessRecord) { + // Throw a terminating error if ProcessRecord was called multiple times (if multiple passed were passed by pipeline) + var errorRecord = ErrorMessages.GetErrorRecord(ErrorCode.MultiplePathsSpecified); + ThrowTerminatingError(errorRecord); + } else { + _didCallProcessRecord = true; + } } protected override void EndProcessing() diff --git a/src/Error.cs b/src/Error.cs index d79ab66..d8be106 100644 --- a/src/Error.cs +++ b/src/Error.cs @@ -42,6 +42,7 @@ internal static string GetErrorMessage(ErrorCode errorCode) ErrorCode.CannotOverwriteWorkingDirectory => Messages.CannotOverwriteWorkingDirectoryMessage, ErrorCode.PathResolvedToMultiplePaths => Messages.PathResolvedToMultiplePathsMessage, ErrorCode.CannotDetermineDestinationPath => Messages.CannotDetermineDestinationPath, + ErrorCode.MultiplePathsSpecified => Messages.MultiplePathsSpecified, _ => throw new ArgumentOutOfRangeException(nameof(errorCode)) }; } @@ -81,6 +82,8 @@ internal enum ErrorCode // Expand-Archive: used when the DestinationPath could not be determined CannotDetermineDestinationPath, // Compress-Archive: Used when an exception occurs when adding an entry to an archive - ExceptionOccuredWhileAddingEntry + ExceptionOccuredWhileAddingEntry, + // Expand:Archive: Used when multiple paths are passed by pipeline + MultiplePathsSpecified } } diff --git a/src/Localized/Messages.resx b/src/Localized/Messages.resx index 2fd8d20..311d31a 100644 --- a/src/Localized/Messages.resx +++ b/src/Localized/Messages.resx @@ -201,4 +201,7 @@ Expanding archive entry {0} to destination {1} + + Multiple paths were specified to the cmdlet, but only one is allowed. + \ No newline at end of file diff --git a/src/PathHelper.cs b/src/PathHelper.cs index 1bff480..a799ff5 100644 --- a/src/PathHelper.cs +++ b/src/PathHelper.cs @@ -35,10 +35,14 @@ internal PathHelper(PSCmdlet cmdlet) internal List GetArchiveAdditions(HashSet fullyQualifiedPaths) { - if (Filter is not null) { + if (Filter is not null) + { _wildCardPattern = new WildcardPattern(Filter); - _entryNames = new HashSet(); } + if (Flatten) + { + _entryNames = new HashSet(); + } List archiveAdditions = new List(fullyQualifiedPaths.Count); foreach (var path in fullyQualifiedPaths) { @@ -109,12 +113,11 @@ private void AddAdditionForFullyQualifiedPath(string path, List // If the item being added is a directory and does not have any descendent files that match the filter, finalAdditions - initialAdditions = 0 // If the item being added is a directory and has descendent files that match the filter, finalAdditions > initialAdditions - if (doesMatchFilter || (!doesMatchFilter && finalAdditions - initialAdditions > 0) && (Flatten && fileSystemInfo is not DirectoryInfo)) { - if (!Flatten || (_entryNames is not null && _entryNames.Add(entryName))) + if (doesMatchFilter || (!doesMatchFilter && finalAdditions - initialAdditions > 0)) { + if (!Flatten || (Flatten && fileSystemInfo is not DirectoryInfo && _entryNames is not null && _entryNames.Add(entryName))) { additions.Add(new ArchiveAddition(entryName: entryName, fileSystemInfo: fileSystemInfo)); } - } } @@ -144,15 +147,15 @@ private void AddDescendentEntries(System.IO.DirectoryInfo directoryInfo, List Date: Wed, 17 Aug 2022 00:39:24 -0700 Subject: [PATCH 33/34] added filter support and tests for Expand-Archive --- Tests/Compress-Archive.Tests.ps1 | 10 ++++++ Tests/Expand-Archive.Tests.ps1 | 49 +++++++++++++++++++++++++-- src/Cmdlets/CompressArchiveCommand.cs | 17 +++++++--- src/Cmdlets/ExpandArchiveCommand.cs | 26 ++++++++++---- src/Error.cs | 7 ++-- src/Formats/GzipArchive.cs | 2 ++ src/Formats/TarArchive.cs | 19 +++++++---- src/Formats/TarGzArchive.cs | 2 ++ src/Formats/ZipArchive.cs | 2 ++ src/IArchive.cs | 3 ++ src/Localized/Messages.resx | 3 ++ 11 files changed, 119 insertions(+), 21 deletions(-) diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index bd98576..2eac4fb 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -1060,6 +1060,8 @@ Describe("Microsoft.PowerShell.Archive tests") { Compress-Archive -Path TestDrive:/directory1 -DestinationPath TestDrive:/archive_with_directory.zip New-Item -Path TestDrive:/directory1/file2.txt -ItemType File + + Compress-Archive -Path TestDrive:/file1.txt -DestinationPath TestDrive:/cantupdate.tar.gz -Format Tgz } It "Does not throw an error when -Update is specified and the archive already exists" { @@ -1088,6 +1090,14 @@ Describe("Microsoft.PowerShell.Archive tests") { Compress-Archive -Path TestDrive:/directory1 -DestinationPath $archivePath -WriteMode Update $archivePath | Should -BeArchiveOnlyContaining @("directory1/", "directory1/file1.txt", "directory1/file2.txt") -Format Zip } + + It "Throws an error when trying to update a tgz archive" { + try { + Compress-Archive -Path TestDrive:/directory1 -DestinationPath TestDrive:/cantupdate.tar.gz -WriteMode Update + } catch { + $_.FullyQualifiedErrorId | Should -Be "ArchiveIsNotUpdateable,Microsoft.PowerShell.Archive.CompressArchiveCommand" + } + } } Context "Flatten tests" { diff --git a/Tests/Expand-Archive.Tests.ps1 b/Tests/Expand-Archive.Tests.ps1 index 1620f27..0d9d16a 100644 --- a/Tests/Expand-Archive.Tests.ps1 +++ b/Tests/Expand-Archive.Tests.ps1 @@ -20,6 +20,9 @@ Describe("Expand-Archive Tests") { if ($Format -eq "Tar") { return $Path += ".tar" } + if (Format -eq "Tgz") { + return $Path += ".tar.gz" + } throw "Format type is not supported" } } @@ -352,7 +355,8 @@ Describe("Expand-Archive Tests") { Context "Basic functionality tests" -ForEach @( @{Format = "Zip"}, - @{Format = "Tar"} + @{Format = "Tar"}, + @{Format = "Tgz"} ) { # extract to a directory works # extract to working directory works when DestinationPath is specified @@ -673,6 +677,14 @@ Describe("Expand-Archive Tests") { $childItems.Count | Should -Be 1 $childItems[0].Name | Should -Be "file1" } + + It "Expands an archive containing an entry whose size is > 4GB" { + $destinationPath = "TestDrive:/large_archive_output2" + { Expand-Archive -Path TestDrive:/large_entry_archive.zip -DestinationPath $destinationPath } | Should -Not -Throw + $childItems = Get-ChildItem $destinationPath + $childItems.Count | Should -Be 1 + $childItems[0].Name | Should -Be "file1" + } } Context "Pipeline tests" { @@ -709,7 +721,7 @@ Describe("Expand-Archive Tests") { $path | Expand-Archive -DestinationPath $destinationPath } catch { - $_.FullyQualifiedErrorId | Should -Be "MultplePathsPassed,${CmdletClassName}" + $_.FullyQualifiedErrorId | Should -Be "MultiplePathsPassed,${CmdletClassName}" } } } @@ -784,4 +796,37 @@ Describe("Expand-Archive Tests") { } } } + + Context "Filter tests" { + BeforeAll { + New-Item TestDrive:/file1.txt -ItemType File + "Hello, World!" | Out-File -Path TestDrive:/file.txt + New-Item TestDrive:/file2.rtf -ItemType File + "Content" | Out-File -Path TestDrive:/file.rtf + Compress-Archive -Path TestDrive:/file1.txt,TestDrive:/file2.rtf -DestinationPath TestDrive:/archive1.zip + + # Create an archive containing a directory + New-Item TestDrive:/directory1 -ItemType Directory + New-Item TestDrive:/directory1/file1.txt -ItemType File + "Hello, World!" | Out-File -Path TestDrive:/directory1/file1.txt + Compress-Archive -Path TestDrive:/directory1 -DestinationPath TestDrive:/archive_containing_directory1 + } + + It "Filter works" { + $destinationPath = "TestDrive:/filter_works_output" + Expand-Archive -Path TestDrive:/archive1.zip -DestinationPath $destinationPath -Filter *.txt + $childItems = Get-ChildItem $destinationPath + $childItems.Count | Should -Be 1 + $childItems[0].Name | Should -Be "file1.txt" + } + + It "Expands parent directory of a file that matches filter when the parent directory does not match the filter" { + $destinationPath = "TestDrive:/expand_parent_directory_output" + Expand-Archive -Path TestDrive:/archive_containing_directory1 -DestinationPath $destinationPath -Filter *.txt + $childItems = Get-ChildItem $destinationPath -Recurse -Name + $childItems.Count | Should -Be 2 + $childItems | Should -Contain "directory1" + $childItems | Should -Contain (Join-Path "directory1" "file1.txt") + } + } } \ No newline at end of file diff --git a/src/Cmdlets/CompressArchiveCommand.cs b/src/Cmdlets/CompressArchiveCommand.cs index 6aac79c..9a32cc4 100644 --- a/src/Cmdlets/CompressArchiveCommand.cs +++ b/src/Cmdlets/CompressArchiveCommand.cs @@ -186,7 +186,9 @@ protected override void EndProcessing() IArchive? archive = null; try { - if (ShouldProcess(target: DestinationPath, action: Messages.Create)) + // If the archive is in Update mode, we want to skip the ShouldProcess check + // This is necessary if we want to check if the archive is updateable + if (WriteMode == WriteMode.Update || ShouldProcess(target: DestinationPath, action: Messages.Create)) { // If the WriteMode is overwrite, delete the existing archive if (WriteMode == WriteMode.Overwrite) @@ -198,6 +200,13 @@ protected override void EndProcessing() archive = ArchiveFactory.GetArchive(format: Format ?? ArchiveFormat.Zip, archivePath: DestinationPath, archiveMode: archiveMode, compressionLevel: CompressionLevel); _didCreateNewArchive = archiveMode != ArchiveMode.Update; } + + // If the cmdlet is in Update mode and the archive does not support updates, throw an error + if (archive is not null && !archive.IsUpdateable) + { + var errorRecord = ErrorMessages.GetErrorRecord(ErrorCode.ArchiveIsNotUpdateable, DestinationPath); + ThrowTerminatingError(errorRecord); + } long numberOfAdditions = archiveAdditions.Count; long numberOfAddedItems = 0; @@ -208,8 +217,8 @@ protected override void EndProcessing() { // Update progress var percentComplete = numberOfAddedItems / (float)numberOfAdditions * 100f; - - progressRecord.StatusDescription = string.Format(Messages.ProgressDisplay, $"{percentComplete:0.0}"); + var statusDescription = string.Format(Messages.ProgressDisplay, $"{percentComplete:0.0}"); + progressRecord = new ProgressRecord(activityId: 1, activity: "Compress-Archive", statusDescription: statusDescription); progressRecord.PercentComplete = (int)percentComplete; WriteProgress(progressRecord); @@ -257,7 +266,7 @@ protected override void EndProcessing() // 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"); + progressRecord = new ProgressRecord(1, "Compress-Archive", string.Format(Messages.ProgressDisplay, "100.0")); progressRecord.PercentComplete = 100; WriteProgress(progressRecord); } diff --git a/src/Cmdlets/ExpandArchiveCommand.cs b/src/Cmdlets/ExpandArchiveCommand.cs index c8a75fb..1e69a8c 100644 --- a/src/Cmdlets/ExpandArchiveCommand.cs +++ b/src/Cmdlets/ExpandArchiveCommand.cs @@ -42,6 +42,10 @@ private enum ParameterSet { [Parameter] public SwitchParameter PassThru { get; set; } + [Parameter] + [ValidateNotNullOrEmpty] + public string? Filter { get; set; } + #region PrivateMembers private PathHelper _pathHelper; @@ -52,6 +56,8 @@ private enum ParameterSet { private bool _didCallProcessRecord; + private WildcardPattern? _wildcardPattern; + #endregion public ExpandArchiveCommand() @@ -78,9 +84,7 @@ protected override void ProcessRecord() protected override void EndProcessing() { // Resolve Path or LiteralPath - bool checkForWildcards = ParameterSetName == nameof(ParameterSet.Path); - string path = checkForWildcards ? Path : LiteralPath; - ValidateSourcePath(path); + ValidateSourcePath(ParameterSetName == nameof(ParameterSet.Path) ? Path : LiteralPath); Debug.Assert(_sourcePath is not null); // Determine archive format based on sourcePath @@ -132,13 +136,21 @@ protected override void EndProcessing() // Write a verbose message saying "Expanding archive ..." WriteVerbose(string.Format(Messages.ExpandingArchiveMessage, DestinationPath)); + // If a value has been supplied to -Filter, create the object that will perform wildcard matching + if (Filter is not null) { + _wildcardPattern = new WildcardPattern(Filter); + } + // Get the next entry in the archive and process it var nextEntry = archive.GetNextEntry(); while (nextEntry != null) { - // The process function will write the progress - ProcessArchiveEntry(nextEntry); - nextEntry = archive.GetNextEntry(); + // If a value has been supplied to -Filter and the entry name of nextEntry does not match the filter + // skip the entry + if ((Filter is null) || (_wildcardPattern is not null && _wildcardPattern.IsMatch(nextEntry.Name))) + { + ProcessArchiveEntry(nextEntry); + } // Update progress info numberOfExpandedItems++; @@ -149,6 +161,8 @@ protected override void EndProcessing() progressRecord.PercentComplete = (int)percentComplete; WriteProgress(progressRecord); } + + nextEntry = archive.GetNextEntry(); } // Show progress as 100% complete diff --git a/src/Error.cs b/src/Error.cs index d8be106..84adc58 100644 --- a/src/Error.cs +++ b/src/Error.cs @@ -42,7 +42,8 @@ internal static string GetErrorMessage(ErrorCode errorCode) ErrorCode.CannotOverwriteWorkingDirectory => Messages.CannotOverwriteWorkingDirectoryMessage, ErrorCode.PathResolvedToMultiplePaths => Messages.PathResolvedToMultiplePathsMessage, ErrorCode.CannotDetermineDestinationPath => Messages.CannotDetermineDestinationPath, - ErrorCode.MultiplePathsSpecified => Messages.MultiplePathsSpecified, + ErrorCode.MultiplePathsSpecified => Messages.MultiplePathsSpecifiedMessage, + ErrorCode.ArchiveIsNotUpdateable => Messages.ArchiveIsNotUpdateableMessage, _ => throw new ArgumentOutOfRangeException(nameof(errorCode)) }; } @@ -84,6 +85,8 @@ internal enum ErrorCode // Compress-Archive: Used when an exception occurs when adding an entry to an archive ExceptionOccuredWhileAddingEntry, // Expand:Archive: Used when multiple paths are passed by pipeline - MultiplePathsSpecified + MultiplePathsSpecified, + // Compress-Archive: Used when the archive does not support being updated + ArchiveIsNotUpdateable } } diff --git a/src/Formats/GzipArchive.cs b/src/Formats/GzipArchive.cs index d2c08d2..0838686 100644 --- a/src/Formats/GzipArchive.cs +++ b/src/Formats/GzipArchive.cs @@ -30,6 +30,8 @@ internal class GzipArchive : IArchive string IArchive.Path => _path; + public bool IsUpdateable => false; + public GzipArchive(string path, ArchiveMode mode, FileStream fileStream, CompressionLevel compressionLevel) { _mode = mode; diff --git a/src/Formats/TarArchive.cs b/src/Formats/TarArchive.cs index 09b2938..6749446 100644 --- a/src/Formats/TarArchive.cs +++ b/src/Formats/TarArchive.cs @@ -30,6 +30,8 @@ internal class TarArchive : IArchive string IArchive.Path => _path; + public bool IsUpdateable => true; + public TarArchive(string path, ArchiveMode mode, FileStream fileStream) { _mode = mode; @@ -151,16 +153,13 @@ internal class TarArchiveEntry : IEntry { // Underlying object is System.Formats.Tar.TarEntry private TarEntry _entry; - private IEntry _objectAsIEntry; - - string IEntry.Name => _entry.Name; + public string Name => _entry.Name; - bool IEntry.IsDirectory => _entry.EntryType == TarEntryType.Directory; + public bool IsDirectory => _entry.EntryType == TarEntryType.Directory; public TarArchiveEntry(TarEntry entry) { _entry = entry; - _objectAsIEntry = this; } void IEntry.ExpandTo(string destinationPath) @@ -173,7 +172,7 @@ void IEntry.ExpandTo(string destinationPath) } var lastWriteTime = _entry.ModificationTime.LocalDateTime; - if (_objectAsIEntry.IsDirectory) + if (IsDirectory) { Directory.CreateDirectory(destinationPath); Directory.SetLastWriteTime(destinationPath, lastWriteTime); @@ -182,10 +181,16 @@ void IEntry.ExpandTo(string destinationPath) _entry.ExtractToFile(destinationPath, overwrite: false); File.SetLastWriteTime(destinationPath, lastWriteTime); } + + SetFileAttributes(destinationPath); } private void SetFileAttributes(string destinationPath) { - + if (System.Environment.OSVersion.Platform == System.PlatformID.Unix + || System.Environment.OSVersion.Platform == System.PlatformID.MacOSX) { + + File.SetUnixFileMode(destinationPath, _entry.Mode); + } } } } diff --git a/src/Formats/TarGzArchive.cs b/src/Formats/TarGzArchive.cs index 54d908f..cc45250 100644 --- a/src/Formats/TarGzArchive.cs +++ b/src/Formats/TarGzArchive.cs @@ -36,6 +36,8 @@ internal class TarGzArchive : IArchive string IArchive.Path => _path; + public bool IsUpdateable => false; + public TarGzArchive(string path, ArchiveMode mode, FileStream fileStream, CompressionLevel compressionLevel) { _path = path; diff --git a/src/Formats/ZipArchive.cs b/src/Formats/ZipArchive.cs index 5e2a41e..a406928 100644 --- a/src/Formats/ZipArchive.cs +++ b/src/Formats/ZipArchive.cs @@ -30,6 +30,8 @@ internal class ZipArchive : IArchive string IArchive.Path => _archivePath; + public bool IsUpdateable => true; + internal int NumberOfEntries => _zipArchive.Entries.Count; public ZipArchive(string archivePath, ArchiveMode mode, FileStream archiveStream, CompressionLevel compressionLevel) diff --git a/src/IArchive.cs b/src/IArchive.cs index 18cfc75..a94389a 100644 --- a/src/IArchive.cs +++ b/src/IArchive.cs @@ -9,6 +9,9 @@ namespace Microsoft.PowerShell.Archive { interface IArchive: IDisposable { + // Can the archive be updated? + public bool IsUpdateable { get; } + // Get what mode the archive is in public ArchiveMode Mode { get; } diff --git a/src/Localized/Messages.resx b/src/Localized/Messages.resx index 311d31a..42fe119 100644 --- a/src/Localized/Messages.resx +++ b/src/Localized/Messages.resx @@ -204,4 +204,7 @@ Multiple paths were specified to the cmdlet, but only one is allowed. + + The archive {0} could not be updated because the format does not support updates. + \ No newline at end of file From 716fd39b20dcb5508a1fbb1f851f565d408cb9ad Mon Sep 17 00:00:00 2001 From: ayousuf3 <23.abdullah.y@gmail.com> Date: Wed, 17 Aug 2022 12:56:10 -0700 Subject: [PATCH 34/34] fixed failing tar.gz tests --- Tests/Compress-Archive.Tests.ps1 | 2 +- src/Cmdlets/CompressArchiveCommand.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index 2eac4fb..95d5d06 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -1091,7 +1091,7 @@ Describe("Microsoft.PowerShell.Archive tests") { $archivePath | Should -BeArchiveOnlyContaining @("directory1/", "directory1/file1.txt", "directory1/file2.txt") -Format Zip } - It "Throws an error when trying to update a tgz archive" { + It "Throws an error when trying to update a tgz archive" -Tag hi { try { Compress-Archive -Path TestDrive:/directory1 -DestinationPath TestDrive:/cantupdate.tar.gz -WriteMode Update } catch { diff --git a/src/Cmdlets/CompressArchiveCommand.cs b/src/Cmdlets/CompressArchiveCommand.cs index 9a32cc4..05f7662 100644 --- a/src/Cmdlets/CompressArchiveCommand.cs +++ b/src/Cmdlets/CompressArchiveCommand.cs @@ -202,7 +202,7 @@ protected override void EndProcessing() } // If the cmdlet is in Update mode and the archive does not support updates, throw an error - if (archive is not null && !archive.IsUpdateable) + if (WriteMode == WriteMode.Update && archive is not null && !archive.IsUpdateable) { var errorRecord = ErrorMessages.GetErrorRecord(ErrorCode.ArchiveIsNotUpdateable, DestinationPath); ThrowTerminatingError(errorRecord);