diff --git a/.gitignore b/.gitignore index 6fbb448..a2e8680 100644 --- a/.gitignore +++ b/.gitignore @@ -88,4 +88,5 @@ StyleCop.Cache test/tools/Modules/SelfSignedCertificate/ # BenchmarkDotNet artifacts -test/perf/BenchmarkDotNet.Artifacts/ \ No newline at end of file +test/perf/BenchmarkDotNet.Artifacts/ +.idea/ \ No newline at end of file diff --git a/Tests/Assertions/Should-BeZipArchiveWithUnixPermissions.psm1 b/Tests/Assertions/Should-BeZipArchiveWithUnixPermissions.psm1 new file mode 100644 index 0000000..97c3868 --- /dev/null +++ b/Tests/Assertions/Should-BeZipArchiveWithUnixPermissions.psm1 @@ -0,0 +1,136 @@ +function Should-BeZipArchiveWithUnixPermissions { + <# + .SYNOPSIS + Checks if a zip archive contains entries with the expected Unix permissions + .EXAMPLE + "C:\Users\\archive.zip" | Should -BeZipArchiveWithUnixPermissions "d---------" "-rw-------" + + Checks if archive.zip only contains file1.txt + #> + + [CmdletBinding()] + Param ( + [string] $ActualValue, + [string] $TempDirectory, + [string] $ExpectedDirectoryPermissions, + [string] $ExpectedFilePermissions, + [switch] $Negate, + [string] $Because, + [switch] $LiteralPath, + $CallerSessionState + ) + + # We need to ensure that ls won't run Get-ChildItem instead + $previousAlias = Get-Alias ls -ErrorAction SilentlyContinue + if ($previousAlias -ne $null) { + Remove-Alias ls + } + + try { + # 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 + if (-not $testPathResult) { + return [pscustomobject]@{ + Succeeded = $false + FailureMessage = $failureMessage + } + } + + $unzipPath = "$TempDirectory/unzipped" + + unzip $ActualValue -d $unzipPath + + # Get ls to list the unzipped contents of the archive with permissions + chmod 775 $unzipPath + $output = ls -Rl $unzipPath + + # Check if the output is null + if ($null -eq $output) { + return [pscustomobject]@{ + Succeeded = $false + FailureMessage = "Archive {0} contains nothing, but it was expected to contain something" + } + } + + # Filter the output line by line + $lines = $output -split [System.Environment]::NewLine + + # Go through each line and split it by whitespace + foreach ($line in $lines) { + + #Skip non-file/directory lines from recursive output + #eg. directory path and total blocks count + #./src/obj/Release/ref: + #total 12 + if (-not $line.StartsWith("-") -and -not $line.StartsWith("d")) { + continue; + } + Write-Host $line + $lineComponents = $line -split " +" + + # Example of some lines: + #-rw-r--r-- 1 owner group 26112 Mar 22 00:36 Microsoft.PowerShell.Archive.dll + #drwxr-xr-x 2 owner group 4096 Mar 22 00:19 ref + + # First component contains attributes + # 2nd component is link count + # 3rd componnent is owner + # 4th component is group + # 5th component is file size + # 6th component is last modified month + # 7th component is last modified day + # 8th component is last modified time + # 9th component is file name + + $permissionString = $lineComponents[0]; + + + if ($permissionString[0] -eq 'd') { + if ($permissionString -ne $expectedDirectoryPermissions) { + return [pscustomobject]@{ + Succeeded = $false + FailureMessage = "Expected directory permissions '$expectedDirectoryPermissions' but got '$permissionString'" + } + } + } + else { + if ($permissionString -ne $expectedFilePermissions) { + return [pscustomobject]@{ + Succeeded = $false + FailureMessage = "Expected directory permissions '$expectedFilePermissions' but got '$permissionString'" + } + } + } + } + + + $ObjProperties = @{ + Succeeded = $true + } + return New-Object PSObject -Property $ObjProperties + } + finally { + if($previousAlias -ne $null) { + Set-Alias ls $previousAlias.Definition + } + } +} + +Add-ShouldOperator -Name BeZipArchiveWithUnixPermissions -InternalName 'Should-BeZipArchiveWithUnixPermissions' -Test ${function:Should-BeZipArchiveWithUnixPermissions} \ No newline at end of file diff --git a/Tests/Compress-Archive.Tests.ps1 b/Tests/Compress-Archive.Tests.ps1 index cfa3ad9..0f698ff 100644 --- a/Tests/Compress-Archive.Tests.ps1 +++ b/Tests/Compress-Archive.Tests.ps1 @@ -4,6 +4,7 @@ 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-BeZipArchiveWithUnixPermissions.psm1" -DisableNameChecking } Describe("Microsoft.PowerShell.Archive tests") { @@ -296,6 +297,31 @@ BeforeDiscovery { 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 "Validate Unix file permissions are preserved" -Skip:$IsWindows { + Remove-Alias "ls" -Force -ErrorAction Ignore + $testDriveRoot = (Get-PsDrive "TestDrive").Root + Write-Host $testFileRoot + $sourcePath = "TestDrive:/SourceDir" + $sourcePathAbsolute = Join-Path $testDriveRoot "SourceDir" + $tempUnzipPath = Join-Path $testDriveRoot "unzip" + $destinationPath = "TestDrive:/archive4.zip" + New-Item $tempUnzipPath -Type Directory | Out-Null + + $ExpectedDirectoryPermissions = 'drwxr-xr-x' + $ExpectedFilePermissions = "-rwxr--r--" + + if ($env:TF_BUILD -ne $null) { + $ExpectedDirectoryPermissions = 'd---------' + $ExpectedFilePermissions = "-rwx------" + } + + find $sourcePathAbsolute -type d -print0 | xargs -0 chmod 775 + find $sourcePathAbsolute -type f -print0 | xargs -0 chmod 700 + 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') + $destinationPath | Should -BeZipArchiveWithUnixPermissions $tempUnzipPath $ExpectedDirectoryPermissions $ExpectedFilePermissions + } } Context "Update tests" -Skip { diff --git a/src/ZipArchive.cs b/src/ZipArchive.cs index 4c74147..933e144 100644 --- a/src/ZipArchive.cs +++ b/src/ZipArchive.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; +using System.IO; using System.IO.Compression; +using System.Runtime.InteropServices; using System.Text; namespace Microsoft.PowerShell.Archive @@ -68,7 +70,9 @@ void IArchive.AddFileSystemEntry(ArchiveAddition addition) entryName += ZipArchiveDirectoryPathTerminator; } - _zipArchive.CreateEntry(entryName); + var entry = _zipArchive.CreateEntry(entryName); + + CopyUnixFilePermissions(entry, addition.FileSystemInfo, entryName.EndsWith(Path.DirectorySeparatorChar) || entryName.EndsWith(Path.AltDirectorySeparatorChar)); } } else @@ -80,7 +84,9 @@ void IArchive.AddFileSystemEntry(ArchiveAddition addition) } // TODO: Add exception handling - _zipArchive.CreateEntryFromFile(sourceFileName: addition.FileSystemInfo.FullName, entryName: entryName, compressionLevel: _compressionLevel); + var entry = _zipArchive.CreateEntryFromFile(sourceFileName: addition.FileSystemInfo.FullName, entryName: entryName, compressionLevel: _compressionLevel); + + CopyUnixFilePermissions(entry, addition.FileSystemInfo, entryName.EndsWith(Path.DirectorySeparatorChar) || entryName.EndsWith(Path.AltDirectorySeparatorChar)); } } @@ -105,6 +111,17 @@ private static System.IO.Compression.ZipArchiveMode ConvertToZipArchiveMode(Arch } } + private static void CopyUnixFilePermissions(ZipArchiveEntry archiveEntry, FileSystemInfo fileSystemInfo, bool isDirectory) + { + const int S_IFREG = 0x8000; + const int S_IFDIR = 0x4000; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + archiveEntry.ExternalAttributes |= (isDirectory ? S_IFDIR : S_IFREG) | (int)fileSystemInfo.UnixFileMode; + } + } + protected virtual void Dispose(bool disposing) { if (!_disposedValue)