From 3cdab0d18dc24b609cef3889e6d085c5bc89c3dc Mon Sep 17 00:00:00 2001 From: Travis Plunk Date: Mon, 2 Dec 2019 17:51:36 -0800 Subject: [PATCH] Add `Unblock-File` for macOS (#11137) --- .../commands/utility/UnblockFile.cs | 69 +++++++- .../resources/UnblockFileStrings.resx | 126 ++++++++++++++ .../Microsoft.PowerShell.Utility.psd1 | 2 +- .../Unblock-File.Tests.ps1 | 159 ++++++++++++------ .../Unimplemented-Cmdlet.Tests.ps1 | 1 - .../engine/Basic/DefaultCommands.Tests.ps1 | 2 +- 6 files changed, 302 insertions(+), 57 deletions(-) create mode 100644 src/Microsoft.PowerShell.Commands.Utility/resources/UnblockFileStrings.resx diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/UnblockFile.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/UnblockFile.cs index 2cc929b4f..8e4a92ea3 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/UnblockFile.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/UnblockFile.cs @@ -1,18 +1,19 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -#if !UNIX - #region Using directives using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.IO; using System.Management.Automation; using System.Management.Automation.Internal; +using System.Runtime.InteropServices; #endregion @@ -23,6 +24,11 @@ namespace Microsoft.PowerShell.Commands HelpUri = "https://go.microsoft.com/fwlink/?LinkID=2097033")] public sealed class UnblockFileCommand : PSCmdlet { +#if UNIX + private const string MacBlockAttribute = "com.apple.quarantine"; + private const int RemovexattrFollowSymLink = 0; +#endif + /// /// The path of the file to unblock. /// @@ -112,6 +118,7 @@ namespace Microsoft.PowerShell.Commands } } } +#if !UNIX // Unblock files foreach (string path in pathsToProcess) @@ -124,10 +131,34 @@ namespace Microsoft.PowerShell.Commands } catch (Exception e) { - WriteError(new ErrorRecord(e, "RemoveItemUnableToAccessFile", ErrorCategory.ResourceUnavailable, path)); + WriteError(new ErrorRecord(exception: e, errorId: "RemoveItemUnableToAccessFile", ErrorCategory.ResourceUnavailable, targetObject: path)); } } } +#else + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + string errorMessage = UnblockFileStrings.LinuxNotSupported; + Exception e = new PlatformNotSupportedException(errorMessage); + ThrowTerminatingError(new ErrorRecord(exception: e, errorId: "LinuxNotSupported", ErrorCategory.NotImplemented, targetObject: null)); + return; + } + + foreach (string path in pathsToProcess) + { + if(IsBlocked(path)) + { + UInt32 result = RemoveXattr(path, MacBlockAttribute, RemovexattrFollowSymLink); + if(result != 0) + { + string errorMessage = string.Format(CultureInfo.CurrentUICulture, UnblockFileStrings.UnblockError, path); + Exception e = new InvalidOperationException(errorMessage); + WriteError(new ErrorRecord(exception: e, errorId: "UnblockError", ErrorCategory.InvalidResult, targetObject: path)); + } + } + } + +#endif } /// @@ -163,6 +194,36 @@ namespace Microsoft.PowerShell.Commands return isValidUnblockableFile; } + +#if UNIX + private bool IsBlocked(string path) + { + uint valueSize = 1024; + IntPtr value = Marshal.AllocHGlobal((int)valueSize); + try + { + var resultSize = GetXattr(path, MacBlockAttribute, value, valueSize, 0, RemovexattrFollowSymLink); + return resultSize != -1; + } + finally + { + Marshal.FreeHGlobal(value); + } + } + + // Ansi means UTF8 on Unix + // https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/RemoveXattr.2.html + [DllImport("libc", SetLastError = true, EntryPoint = "removexattr", CharSet = CharSet.Ansi)] + private static extern UInt32 RemoveXattr(string path, string name, int options); + + [DllImport("libc", EntryPoint = "getxattr", CharSet = CharSet.Ansi)] + private static extern long GetXattr( + [MarshalAs(UnmanagedType.LPStr)] string path, + [MarshalAs(UnmanagedType.LPStr)] string name, + IntPtr value, + ulong size, + uint position, + int options); +#endif } } -#endif diff --git a/src/Microsoft.PowerShell.Commands.Utility/resources/UnblockFileStrings.resx b/src/Microsoft.PowerShell.Commands.Utility/resources/UnblockFileStrings.resx new file mode 100644 index 000000000..b2af7eba6 --- /dev/null +++ b/src/Microsoft.PowerShell.Commands.Utility/resources/UnblockFileStrings.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The cmdlet does not support Linux. + + + There was an error unblocking {0}. + + diff --git a/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 b/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 index da57841b2..41705fb7b 100644 --- a/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 +++ b/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 @@ -25,7 +25,7 @@ CmdletsToExport = @( 'Get-TraceSource', 'Set-TraceSource', 'Add-Type', 'Get-TypeData', 'Remove-TypeData', 'Update-TypeData', 'Get-UICulture', 'Get-Unique', 'Get-Uptime', 'Clear-Variable', 'Get-Variable', 'New-Variable', 'Remove-Variable', 'Set-Variable', 'Get-Verb', 'Write-Verbose', 'Write-Warning', 'Invoke-WebRequest', - 'Format-Wide', 'ConvertTo-Xml', 'Select-Xml', 'Get-Error', 'Update-List' + 'Format-Wide', 'ConvertTo-Xml', 'Select-Xml', 'Get-Error', 'Update-List', 'Unblock-File' ) FunctionsToExport = @() AliasesToExport = @('fhx') diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Unblock-File.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Unblock-File.Tests.ps1 index a62d38d4a..2b19f370b 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Unblock-File.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Unblock-File.Tests.ps1 @@ -1,69 +1,128 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -function Test-UnblockFile { - { Get-Content -Path $testfilepath -Stream Zone.Identifier -ErrorAction Stop | Out-Null } | - Should -Throw -ErrorId "GetContentReaderFileNotFoundError,Microsoft.PowerShell.Commands.GetContentCommand" -} Describe "Unblock-File" -Tags "CI" { - BeforeAll { - if ( ! $IsWindows ) - { - $origDefaults = $PSDefaultParameterValues.Clone() - $PSDefaultParameterValues['it:skip'] = $true + Context "Windows and macOS" { + BeforeAll { - } else { - $testfilepath = Join-Path -Path $TestDrive -ChildPath testunblockfile.ttt + if ($IsWindows) { + function Test-UnblockFile { + { Get-Content -Path $testfilepath -Stream Zone.Identifier -ErrorAction Stop | Out-Null } | + Should -Throw -ErrorId "GetContentReaderFileNotFoundError,Microsoft.PowerShell.Commands.GetContentCommand" + } + + function Block-File { + param($path) + Set-Content -Value "[ZoneTransfer]`r`nZoneId=4" -Path $path -Stream Zone.Identifier + } + } + else { + function Test-UnblockFile { + $result = (xattr $testfilepath | Select-String 'com.apple.com.quarantine') + $result | Should -BeNullOrEmpty + } + + function Block-File { + param($path) + Set-Content -Path $path -value 'test' + xattr -w com.apple.quarantine '0081;5dd5c373;Microsoft Edge;1A9A933D-619A-4036-BAF3-17A7966A1BA8' $path + + } + } + + if ( $IsLinux ) + { + $origDefaults = $PSDefaultParameterValues.Clone() + $PSDefaultParameterValues['it:skip'] = $true + + } else { + $testfilepath = Join-Path -Path $TestDrive -ChildPath testunblockfile.ttt + } + } + + AfterAll { + if ( $IsLinux ){ + $global:PSDefaultParameterValues = $origDefaults + } + } + + BeforeEach { + Block-File -Path $testfilepath + } + + It "With '-Path': no file exist" { + { Unblock-File -Path nofileexist.ttt -ErrorAction Stop } | Should -Throw -ErrorId "FileNotFound,Microsoft.PowerShell.Commands.UnblockFileCommand" + } + + It "With '-LiteralPath': no file exist" { + { Unblock-File -LiteralPath nofileexist.ttt -ErrorAction Stop } | Should -Throw -ErrorId "FileNotFound,Microsoft.PowerShell.Commands.UnblockFileCommand" + } + + It "With '-Path': file exist" { + Unblock-File -Path $testfilepath + Test-UnblockFile + + # If a file is not blocked we silently return without an error. + { Unblock-File -Path $testfilepath -ErrorAction Stop } | Should -Not -Throw + } + + It "With '-LiteralPath': file exist" { + Unblock-File -LiteralPath $testfilepath + Test-UnblockFile + } + + It "Write an error if a file is read only" { + $TestFile = Join-Path $TestDrive "testfileunlock.ps1" + Block-File -Path $TestFile + Set-ItemProperty -Path $TestFile -Name IsReadOnly -Value $True + + $TestFileCreated = Get-ChildItem $TestFile + $TestFileCreated.IsReadOnly | Should -BeTrue + if ($IsWindows) { + $expectedError = "RemoveItemUnableToAccessFile,Microsoft.PowerShell.Commands.UnblockFileCommand" + } else { + $expectedError = "UnblockError,Microsoft.PowerShell.Commands.UnblockFileCommand" + } + + { Unblock-File -LiteralPath $TestFile -ErrorAction Stop } | Should -Throw -ErrorId $expectedError } } - AfterAll { - if ( ! $IsWindows ){ - $global:PSDefaultParameterValues = $origDefaults + Context "Linux" { + BeforeAll { + if ( ! $IsLinux ) + { + $origDefaults = $PSDefaultParameterValues.Clone() + $PSDefaultParameterValues['it:skip'] = $true + + } else { + $testfilepath = Join-Path -Path $TestDrive -ChildPath testunblockfile.ttt + $null = New-Item -Path $testfilepath -ItemType File + } } - } - BeforeEach { - if ( $IsWindows ){ - Set-Content -Value "[ZoneTransfer]`r`nZoneId=4" -Path $testfilepath -Stream Zone.Identifier + AfterAll { + if ( ! $IsLinux ){ + $global:PSDefaultParameterValues = $origDefaults + } } - } - It "With '-Path': no file exist" { - { Unblock-File -Path nofileexist.ttt -ErrorAction Stop } | Should -Throw -ErrorId "FileNotFound,Microsoft.PowerShell.Commands.UnblockFileCommand" - } - It "With '-LiteralPath': no file exist" { - { Unblock-File -LiteralPath nofileexist.ttt -ErrorAction Stop } | Should -Throw -ErrorId "FileNotFound,Microsoft.PowerShell.Commands.UnblockFileCommand" - } - - It "With '-Path': file exist" { - Unblock-File -Path $testfilepath - Test-UnblockFile - - # If a file is not blocked we silently return without an error. - { Unblock-File -Path $testfilepath -ErrorAction Stop } | Should -Not -Throw - } - - It "With '-LiteralPath': file exist" { - Unblock-File -LiteralPath $testfilepath - Test-UnblockFile - } - - It "Write an error if a file is read only" { - $TestFile = Join-Path $TestDrive "testfileunlock.ps1" - Set-Content -Path $TestFile -value 'test' - $ZoneIdentifier = { - [ZoneTransfer] - ZoneId=3 + It "With '-Path': no file exist" { + { Unblock-File -Path nofileexist.ttt -ErrorAction Stop } | Should -Throw -ErrorId "FileNotFound,Microsoft.PowerShell.Commands.UnblockFileCommand" } - Set-Content -Path $TestFile -Value $ZoneIdentifier -Stream 'Zone.Identifier' - Set-ItemProperty -Path $TestFile -Name IsReadOnly -Value $True - $TestFileCreated = Get-ChildItem $TestFile - $TestFileCreated.IsReadOnly | Should -BeTrue + It "With '-LiteralPath': no file exist" { + { Unblock-File -LiteralPath nofileexist.ttt -ErrorAction Stop } | Should -Throw -ErrorId "FileNotFound,Microsoft.PowerShell.Commands.UnblockFileCommand" + } - { Unblock-File -LiteralPath $TestFile -ErrorAction Stop } | Should -Throw -ErrorId "RemoveItemUnableToAccessFile,Microsoft.PowerShell.Commands.UnblockFileCommand" + It "With '-LiteralPath': file exist" { + { Unblock-File -LiteralPath $testfilepath -ErrorAction Stop } | Should -Throw -ErrorId "LinuxNotSupported,Microsoft.PowerShell.Commands.UnblockFileCommand" + } + + It "With '-Path': file exist" { + { Unblock-File -Path $testfilepath -ErrorAction Stop } | Should -Throw -ErrorId "LinuxNotSupported,Microsoft.PowerShell.Commands.UnblockFileCommand" + } } } diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Unimplemented-Cmdlet.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Unimplemented-Cmdlet.Tests.ps1 index ea19c7d41..feaf1b054 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Unimplemented-Cmdlet.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Unimplemented-Cmdlet.Tests.ps1 @@ -3,7 +3,6 @@ Describe "Unimplemented Utility Cmdlet Tests" -Tags "CI" { $Commands = @( - "Unblock-File", "ConvertFrom-SddlString" ) diff --git a/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 b/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 index 8d1e285cd..d70b1b294 100644 --- a/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 +++ b/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 @@ -468,7 +468,7 @@ Describe "Verify approved aliases list" -Tags "CI" { "Cmdlet", "Test-PSSessionConfigurationFile", "", $($FullCLR -or $CoreWindows ), "", "", "None" "Cmdlet", "Test-WSMan", "", $($FullCLR -or $CoreWindows ), "", "", "None" "Cmdlet", "Trace-Command", "", $($FullCLR -or $CoreWindows -or $CoreUnix), "", "", "None" -"Cmdlet", "Unblock-File", "", $($FullCLR -or $CoreWindows ), "", "", "Medium" +"Cmdlet", "Unblock-File", "", $($FullCLR -or $CoreWindows -or $CoreUnix), "", "", "Medium" "Cmdlet", "Undo-Transaction", "", $($FullCLR ), "", "", "" "Cmdlet", "Unprotect-CmsMessage", "", $($FullCLR -or $CoreWindows ), "", "", "None" "Cmdlet", "Unregister-Event", "", $($FullCLR -or $CoreWindows -or $CoreUnix), "", "", "Medium"