Add Unblock-File for macOS (#11137)

This commit is contained in:
Travis Plunk 2019-12-02 17:51:36 -08:00 committed by GitHub
parent 7f564a2d12
commit 3cdab0d18d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 302 additions and 57 deletions

View file

@ -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
/// <summary>
/// The path of the file to unblock.
/// </summary>
@ -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
}
/// <summary>
@ -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

View file

@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="https://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="https://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="LinuxNotSupported" xml:space="preserve">
<value>The cmdlet does not support Linux.</value>
</data>
<data name="UnblockError" xml:space="preserve">
<value>There was an error unblocking {0}.</value>
</data>
</root>

View file

@ -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')

View file

@ -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"
}
}
}

View file

@ -3,7 +3,6 @@
Describe "Unimplemented Utility Cmdlet Tests" -Tags "CI" {
$Commands = @(
"Unblock-File",
"ConvertFrom-SddlString"
)

View file

@ -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"