diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/UnblockFile.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/UnblockFile.cs
index 0f1ff4af371..e1d8f86c8ac 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=217450")]
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 @@ protected override void ProcessRecord()
}
}
}
+#if !UNIX
// Unblock files
foreach (string path in pathsToProcess)
@@ -124,10 +131,34 @@ protected override void ProcessRecord()
}
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 @@ private bool IsValidFileForUnblocking(string resolvedpath)
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 00000000000..b2af7eba67e
--- /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 da57841b21d..41705fb7bd7 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 a62d38d4a77..2b19f370bc2 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 {
+
+ 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
- } else {
- $testfilepath = Join-Path -Path $TestDrive -ChildPath testunblockfile.ttt
+ }
+ }
+
+ if ( $IsLinux )
+ {
+ $origDefaults = $PSDefaultParameterValues.Clone()
+ $PSDefaultParameterValues['it:skip'] = $true
+
+ } else {
+ $testfilepath = Join-Path -Path $TestDrive -ChildPath testunblockfile.ttt
+ }
}
- }
- AfterAll {
- if ( ! $IsWindows ){
- $global:PSDefaultParameterValues = $origDefaults
+ AfterAll {
+ if ( $IsLinux ){
+ $global:PSDefaultParameterValues = $origDefaults
+ }
}
- }
- BeforeEach {
- if ( $IsWindows ){
- Set-Content -Value "[ZoneTransfer]`r`nZoneId=4" -Path $testfilepath -Stream Zone.Identifier
+ 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 '-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 '-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
+ 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
- }
+ # 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 "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
+ }
}
- 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
+ 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
+ }
+ }
+
+ AfterAll {
+ if ( ! $IsLinux ){
+ $global:PSDefaultParameterValues = $origDefaults
+ }
}
- 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
- { Unblock-File -LiteralPath $TestFile -ErrorAction Stop } | Should -Throw -ErrorId "RemoveItemUnableToAccessFile,Microsoft.PowerShell.Commands.UnblockFileCommand"
+ 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 '-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 ea19c7d4130..feaf1b0548f 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 8e1bffaf40d..08e7ee9b3a9 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"