PureInvoke.psm1
# Copyright WebMD Health Services # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License using namespace System.ComponentModel using namespace System.Runtime.InteropServices #Requires -Version 5.1 Set-StrictMode -Version 'Latest' # Functions should use $script:moduleRoot as the relative root from which to find # things. A published module has its function appended to this file, while a # module in development has its functions in the Functions directory. $script:moduleRoot = $PSScriptRoot foreach ($csFile in (Get-ChildItem -Path (Join-Path -Path $script:moduleRoot -ChildPath 'src') -Filter '*.cs')) { Add-Type -TypeDefinition (Get-Content -Raw -Path $csFile.FullName) } # Store each of your module's functions in its own file in the Functions # directory. On the build server, your module's functions will be appended to # this file, so only dot-source files that exist on the file system. This allows # developers to work on a module without having to build it first. Grab all the # functions that are in their own files. $functionsPath = Join-Path -Path $script:moduleRoot -ChildPath 'Functions\*.ps1' if( (Test-Path -Path $functionsPath) ) { foreach( $functionPath in (Get-Item $functionsPath) ) { . $functionPath.FullName } } function Invoke-AdvapiLookupAccountName { <# .SYNOPSIS Calls the Advanced Windows 32 Base API (advapi32.dll) `LookupAccountName` function. .DESCRIPTION The `Invoke-AdvapiLookupAccountName` function calls the advapi32.dll API's `LookupAccountName` function, which looks up an account name and returns its domain, SID, and use. Pass the account name to the `AccountName` parameter and the system name to the `SystemName` parameter, which are passed to `LookupAccountName` as the `lpAccountName` and `lpSystemName` arguments, respectively. The function returns an object with properties for each of the `LookupAccountName` function's out parameters: `ReferencedDomainName`, `Sid`, and `Use`. .LINK https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-lookupaccountnamea .EXAMPLE Invoke-AdvapiLookupAccountName -AccountName ([Environment]::UserName) Demonstrates how to call this function by passing a username to the `AccountName` parameter. #> [CmdletBinding()] param( # The name of the system. [String] $SystemName, # The account name to lookup. [Parameter(Mandatory)] [String] $AccountName ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState $result = [pscustomobject]@{ ReferencedDomainName = ''; Sid = [byte[]]::New(0); Use = [PureInvoke.AdvApi32+SidNameUse]::Unknown } [byte[]] $sid = [byte[]]::New(0); # cb = count of bytes [UInt32] $cbSid = 0; [Text.StringBuilder] $domainName = [Text.StringBuilder]::New() # cch = count of chars [UInt32] $cchDomainName = $domainName.Capacity; [PureInvoke.AdvApi32+SidNameUse] $sidNameUse = [PureInvoke.AdvApi32+SidNameUse]::Unknown; [PureInvoke.ErrorCode]$errCode = [PureInvoke.ErrorCode]::Ok $result = [PureInvoke.AdvApi32]::LookupAccountName($SystemName, $AccountName, $sid, [ref] $cbSid, $domainName, [ref] $cchDomainName, [ref]$sidNameUse) $errCode = [Marshal]::GetLastWin32Error() if ($result) { Write-Win32Error -ErrorCode $errCode return } if ($errCode -eq [PureInvoke.ErrorCode]::InsufficientBuffer -or $errCode -eq [PureInvoke.ErrorCode]::InvalidFlags) { $sid = [byte[]]::New($cbSid); [void]$domainName.EnsureCapacity([int]$cchDomainName); $result = [PureInvoke.AdvApi32]::LookupAccountName($SystemName, $AccountName, $sid, [ref] $cbSid, $domainName, [ref] $cchDomainName, [ref] $sidNameUse) $errCode = [Marshal]::GetLastWin32Error() if (-not $result) { Write-Win32Error -ErrorCode $errCode return } } else { Write-Win32Error -ErrorCode $errCode return } $result.ReferencedDomainName = $domainName.ToString() $result.Sid = $sid $result.Use = $sidNameUse return $result } function Invoke-AdvapiLookupAccountSid { <# .SYNOPSIS Calls the Advanced Windows 32 Base API (advapi32.dll) `LookupAccountSid` function. .DESCRIPTION The `Invoke-AdvapiLookupAccountSid` function calls the advapi32.dll API's `LookupAccountSid` function, which looks up a SID and returns its account name, domain name, and use. Pass the SID as a byte array to the `Sid` parameter and the system name to the `SystemName` parameter, which are passed to `LookupAccountSid` as the `Sid` and `lpSystemName` arguments, respectively. The function returns an object with properties for each of the `LookupAccountSid` function's out parameters: `Name`, `ReferencedDomainName`, and `Use`. .LINK https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-lookupaccountsida .EXAMPLE Invoke-AdvapiLookupAccountSid -Sid $sid Demonstrates how to call this function by passing a sid to the `Sid` parameter. #> [CmdletBinding()] param( [String] $SystemName, [Parameter(Mandatory)] [byte[]] $Sid ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState $result = [pscustomobject]@{ Name = ''; ReferencedDomainName = '' Use = [PureInvoke.AdvApi32+SidNameUse]::Unknown } [Text.StringBuilder] $name = [Text.StringBuilder]::New() # cch = count of chars [UInt32] $cchName = $name.Capacity; [Text.StringBuilder] $domainName = [Text.StringBuilder]::New() [UInt32] $cchDomainName = $domainName.Capacity; [PureInvoke.AdvApi32+SidNameUse] $sidNameUse = [PureInvoke.AdvApi32+SidNameUse]::Unknown; [PureInvoke.ErrorCode] $errCode = [PureInvoke.ErrorCode]::Ok $result = [PureInvoke.AdvApi32]::LookupAccountSid($SystemName, $sid, $name, [ref] $cchName, $domainName, [ref] $cchDomainName, [ref] $sidNameUse) $errCode = [Marshal]::GetLastWin32Error() if (-not $result -and ($errCode -eq [PureInvoke.ErrorCode]::InsufficientBuffer)) { [void]$name.EnsureCapacity($cchName); [void]$domainName.EnsureCapacity($cchName); $result = [PureInvoke.AdvApi32]::LookupAccountSid($SystemName, $sid, $name, [ref] $cchName, $domainName, $cchDomainName, [ref] $sidNameUse) $errCode = [Marshal]::GetLastWin32Error() if (-not $result) { Write-Win32Error -ErrorCode $errCode return } } if ($errCode) { Write-Win32Error -ErrorCode $errCode return } $result.ReferencedDomainName = $domainName.ToString() $result.Name = $name.ToString() $result.Use = $sidNameUse return $result } function Invoke-KernelFindFileName { <# .SYNOPSIS Calls the Win32 `FindFirstFileNameW` and `FindNextFileNameW` functions to get the hardlinks to a file. .DESCRIPTION The `Invoke-KernelFindFileName` function finds all the hardlinks to a file. It calls the Win32 `FindFirstFileNameW` and `FindNextFileNameW` functions to get the paths. It returns the path to each hardlink, which includes the path to the file itself. The paths are returned *without* drive qualifiers at the beginning. Since hardlinks can't cross physical file systems, their drives will be the same as the source path. .LINK https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findfirstfilenamew .LINK https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findnextfilenamew .EXAMPLE Invoke-KernelFindFileName -Path 'C:\link.txt' Demonstrates how to get the hardlinks to a file by passing its path to the `Invoke-KernelFindFileName` function's `Path` parameter. #> [CmdletBinding()] param( [Parameter(Mandatory)] [String] $Path ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState [PureInvoke.ErrorCode] $errCode = [PureInvoke.ErrorCode]::Ok # Loop over and collect all hard links as their full paths. [IntPtr]$findHandle = [IntPtr]::Zero # Must be MAX_PATH otherwise calls to FindFirstFileName@ and FindNextFileNameW throw exceptions that crash # PowerShell. [Text.StringBuilder] $sbLinkName = [Text.StringBuilder]::New() [UInt32] $cchLinkName = $sbLinkName.Capacity $findHandle = [PureInvoke.Kernel32]::FindFirstFileNameW($Path, 0, [ref]$cchLinkName, $sbLinkName) $errCode = [Marshal]::GetLastWin32Error() Write-Debug "[Kernel32]::FindFirstFileNameW(""${Path}"", 0, ${cchLinkName}, ""${sbLinkName}"") return ${findHandle} GetLastError() ${errCode}" if ([PureInvoke.Kernel32]::INVALID_HANDLE_VALUE -eq $findHandle) { if ($errCode -eq [PureInvoke.ErrorCode]::MoreData) { [void]$sbLinkName.EnsureCapacity($cchLinkName) $findHandle = [PureInvoke.Kernel32]::FindFirstFileNameW($Path, 0, [ref]$cchLinkName, $sbLinkName) $errCode = [Marshal]::GetLastWin32Error() Write-Debug "[Kernel32]::FindFirstFileNameW(""${Path}"", 0, ${cchLinkName}, ""${sbLinkName}"")) return ${findHandle} GetLastError() ${errCode}" if ([PureInvoke.Kernel32]::INVALID_HANDLE_VALUE -eq $findHandle) { Write-Win32Error -ErrorCode $errCode return } } else { Write-Win32Error -ErrorCode $errCode return } } $linkName = $sbLinkName.ToString() if (-not $linkName) { Write-Win32Error -ErrorCode $errCode return } $linkName | Write-Output try { do { [void]$sbLinkName.Clear() $result = [PureInvoke.Kernel32]::FindNextFileNameW($findHandle, [ref]$cchLinkName, $sbLinkName) $errCode = [Marshal]::GetLastWin32Error() Write-Debug "[Kernel32]::FindNextFileNameW(${findHandle}, ${cchLinkName}, ""${sbLinkName}"")) return ${result} GetLastError() ${errCode}" if (-not $result -and $errCode -eq [PureInvoke.ErrorCode]::MoreData) { [void]$sbLinkName.EnsureCapacity($cchLinkName) $result = [PureInvoke.Kernel32]::FindNextFileNameW($findHandle, [ref]$cchLinkName, $sbLinkName) $errCode = [Marshal]::GetLastWin32Error() Write-Debug "[Kernel32]::FindNextFileNameW(${findHandle}, ${cchLinkName}, ""${sbLinkName}"")) return ${result} GetLastError() ${errCode}" } if ($result) { $linkName = $sbLinkName.ToString() if (-not $linkName) { Write-Win32Error -ErrorCode $errCode return } $linkName | Write-Output continue } if ($errCode -eq [PureInvoke.ErrorCode]::HandleEof) { return } if($errCode -eq [PureInvoke.ErrorCode]::InvalidHandle) { $msg = 'No matching files found.' Write-Error -Message $msg -ErrorAction $ErrorActionPreference return } Write-Win32Error -ErrorCode $errCode return } while ($true) } finally { [void][PureInvoke.Kernel32]::FindClose($findHandle); } } function Invoke-KernelGetVolumePathName { [CmdletBinding()] param( [Parameter(Mandatory)] [String] $Path ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState [Marshal]::SetLastPInvokeError([PureInvoke.ErrorCode]::Ok) [Marshal]::SetLastSystemError([PureInvoke.ErrorCode]::Ok) # Need to call GetFullPathName to get the size of the necessary buffer. $sbPath = [Text.StringBuilder]::New([PureInvoke.Kernel32]::MAX_PATH) $cchPath = [UInt32]$sbPath.Capacity; # in/out character-count variable for the WinAPI calls. # Get the volume (drive) part of the target file's full path (e.g., @"C:\") $result = [PureInvoke.Kernel32]::GetVolumePathName($Path, $sbPath, $cchPath) [PureInvoke.ErrorCode] $errCode = [Marshal]::GetLastWin32Error() $msg = "[Kernel32]::GetVolumePathName(""${Path}"", ""${sbPath}"", ${cchPath}) return ${result} " + "GetLastError() ${errCode}" Write-Debug $msg if (-not $result) { Write-Win32Error -ErrorCode $errCode return } return $sbPath.ToString() } function Use-CallerPreference { <# .SYNOPSIS Sets the PowerShell preference variables in a module's function based on the callers preferences. .DESCRIPTION Script module functions do not automatically inherit their caller's variables, including preferences set by common parameters. This means if you call a script with switches like `-Verbose` or `-WhatIf`, those that parameter don't get passed into any function that belongs to a module. When used in a module function, `Use-CallerPreference` will grab the value of these common parameters used by the function's caller: * ErrorAction * Debug * Confirm * InformationAction * Verbose * WarningAction * WhatIf This function should be used in a module's function to grab the caller's preference variables so the caller doesn't have to explicitly pass common parameters to the module function. This function is adapted from the [`Get-CallerPreference` function written by David Wyatt](https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d). There is currently a [bug in PowerShell](https://connect.microsoft.com/PowerShell/Feedback/Details/763621) that causes an error when `ErrorAction` is implicitly set to `Ignore`. If you use this function, you'll need to add explicit `-ErrorAction $ErrorActionPreference` to every `Write-Error` call. Please vote up this issue so it can get fixed. .LINK about_Preference_Variables .LINK about_CommonParameters .LINK https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d .LINK http://powershell.org/wp/2014/01/13/getting-your-script-module-functions-to-inherit-preference-variables-from-the-caller/ .EXAMPLE Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Demonstrates how to set the caller's common parameter preference variables in a module function. #> [CmdletBinding()] param ( [Parameter(Mandatory)] #[Management.Automation.PSScriptCmdlet] # The module function's `$PSCmdlet` object. Requires the function be decorated with the `[CmdletBinding()]` # attribute. $Cmdlet, [Parameter(Mandatory)] # The module function's `$ExecutionContext.SessionState` object. Requires the function be decorated with the # `[CmdletBinding()]` attribute. # # Used to set variables in its callers' scope, even if that caller is in a different script module. [Management.Automation.SessionState]$SessionState ) Set-StrictMode -Version 'Latest' # List of preference variables taken from the about_Preference_Variables and their common parameter name (taken # from about_CommonParameters). $commonPreferences = @{ 'ErrorActionPreference' = 'ErrorAction'; 'DebugPreference' = 'Debug'; 'ConfirmPreference' = 'Confirm'; 'InformationPreference' = 'InformationAction'; 'VerbosePreference' = 'Verbose'; 'WarningPreference' = 'WarningAction'; 'WhatIfPreference' = 'WhatIf'; } foreach( $prefName in $commonPreferences.Keys ) { $parameterName = $commonPreferences[$prefName] # Don't do anything if the parameter was passed in. if( $Cmdlet.MyInvocation.BoundParameters.ContainsKey($parameterName) ) { continue } $variable = $Cmdlet.SessionState.PSVariable.Get($prefName) # Don't do anything if caller didn't use a common parameter. if( -not $variable ) { continue } if( $SessionState -eq $ExecutionContext.SessionState ) { Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false } else { $SessionState.PSVariable.Set($variable.Name, $variable.Value) } } } function Write-Win32Error { [CmdletBinding()] param( [Parameter(Mandatory)] [PureInvoke.ErrorCode] $ErrorCode, [String] $Message ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState if ($Message) { $Message.TrimEnd('.') $Message = "${Message}: " } $win32Ex = [Win32Exception]::New($ErrorCode) $msg = "${Message}$($win32Ex.Message) (0x$($win32Ex.ErrorCode.ToString('x'))/$($win32Ex.NativeErrorCode))." Write-Error -Message $msg -ErrorAction $ErrorActionPreference } |