PoshToolbox.psm1
#region: Exceptions #region: .\src\Exceptions\New-ActiveDirectoryObjectNotFoundException.ps1 function New-ActiveDirectoryObjectNotFoundException { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification='Function does not change system state.')] [CmdletBinding()] [OutputType([System.Management.Automation.ErrorRecord])] ## PARAMETERS ############################################################# param( [Parameter( Position = 0, Mandatory )] [ValidateNotNullOrEmpty()] [string] $Message, [Parameter()] [switch] $Throw ) ## PROCESS ################################################################ process { $ErrorRecord = [System.Management.Automation.ErrorRecord]::new( [System.DirectoryServices.ActiveDirectory.ActiveDirectoryObjectNotFoundException]::new($Message), "System.DirectoryServices.ActiveDirectory.ActiveDirectoryObjectNotFoundException", [System.Management.Automation.ErrorCategory]::ObjectNotFound, $null ) if ($Throw) { throw $ErrorRecord } Write-Output $ErrorRecord } } #endregion #region: .\src\Exceptions\New-ActiveDirectoryOperationException.ps1 function New-ActiveDirectoryOperationException { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification='Function does not change system state.')] [CmdletBinding()] [OutputType([System.Management.Automation.ErrorRecord])] ## PARAMETERS ############################################################# param( [Parameter( Position = 0, Mandatory )] [ValidateNotNullOrEmpty()] [string] $Message, [Parameter()] [switch] $Throw ) ## PROCESS ################################################################ process { $ErrorRecord = [System.Management.Automation.ErrorRecord]::new( [System.DirectoryServices.ActiveDirectory.ActiveDirectoryOperationException]::new($Message), "System.DirectoryServices.ActiveDirectory.ActiveDirectoryOperationException", [System.Management.Automation.ErrorCategory]::InvalidOperation, $null ) if ($Throw) { throw $ErrorRecord } Write-Output $ErrorRecord } } #endregion #region: .\src\Exceptions\New-ActiveDirectoryServerDownException.ps1 function New-ActiveDirectoryServerDownException { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification='Function does not change system state.')] [CmdletBinding()] [OutputType([System.Management.Automation.ErrorRecord])] ## PARAMETERS ############################################################# param( [Parameter( Position = 0, Mandatory )] [ValidateNotNullOrEmpty()] [string] $Message, [Parameter()] [switch] $Throw ) ## PROCESS ################################################################ process { $ErrorRecord = [System.Management.Automation.ErrorRecord]::new( [System.DirectoryServices.ActiveDirectory.ActiveDirectoryServerDownException]::new($Message), "System.DirectoryServices.ActiveDirectory.ActiveDirectoryServerDownException", [System.Management.Automation.ErrorCategory]::ResourceUnavailable, $null ) if ($Throw) { throw $ErrorRecord } Write-Output $ErrorRecord } } #endregion #region: .\src\Exceptions\New-ArgumentException.ps1 function New-ArgumentException { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification='Function does not change system state.')] [CmdletBinding()] [OutputType([System.Management.Automation.ErrorRecord])] ## PARAMETERS ############################################################# param( [Parameter( Position = 0, Mandatory )] [ValidateNotNullOrEmpty()] [string] $Message, [Parameter()] [switch] $Throw ) ## PROCESS ################################################################ process { $ErrorRecord = [System.Management.Automation.ErrorRecord]::new( [System.ArgumentException]::new($Message), "System.ArgumentException", [System.Management.Automation.ErrorCategory]::InvalidArgument, $null ) if ($Throw) { throw $ErrorRecord } Write-Output $ErrorRecord } } #endregion #region: .\src\Exceptions\New-InvalidOperationException.ps1 function New-InvalidOperationException { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification='Function does not change system state.')] [CmdletBinding()] [OutputType([System.Management.Automation.ErrorRecord])] ## PARAMETERS ############################################################# param( [Parameter( Position = 0, Mandatory )] [ValidateNotNullOrEmpty()] [string] $Message, [Parameter()] [switch] $Throw ) ## PROCESS ################################################################ process { $ErrorRecord = [System.Management.Automation.ErrorRecord]::new( [System.InvalidOperationException]::new($Message), "System.InvalidOperationException", [System.Management.Automation.ErrorCategory]::ConnectionError, $null ) if ($Throw) { throw $ErrorRecord } Write-Output $ErrorRecord } } #endregion #region: .\src\Exceptions\New-LimitException.ps1 function New-LimitException { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification='Function does not change system state.')] [CmdletBinding()] [OutputType([System.Management.Automation.ErrorRecord])] ## PARAMETERS ############################################################# param( [Parameter( Position = 0, Mandatory )] [ValidateNotNullOrEmpty()] [string] $Message, [Parameter()] [switch] $Throw ) ## PROCESS ################################################################ process { $ErrorRecord = [System.Management.Automation.ErrorRecord]::new( [PoshToolbox.LimitException]::new($Message), "PoshToolbox.LimitException", [System.Management.Automation.ErrorCategory]::LimitsExceeded, $null ) if ($Throw) { throw $ErrorRecord } Write-Output $ErrorRecord } } #endregion #region: .\src\Exceptions\New-MethodInvocationException.ps1 function New-MethodInvocationException { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification='Function does not change system state.')] [CmdletBinding()] [OutputType([System.Management.Automation.ErrorRecord])] ## PARAMETERS ############################################################# param( [Parameter( Position = 0, Mandatory )] [ValidateNotNullOrEmpty()] [System.Exception] $Exception, [Parameter()] [switch] $Throw ) ## PROCESS ################################################################ process { $ErrorRecord = [System.Management.Automation.ErrorRecord]::new( $Exception, "System.Management.Automation.MethodInvocationException", [System.Management.Automation.ErrorCategory]::InvalidOperation, $null ) if ($Throw) { throw $ErrorRecord } Write-Output $ErrorRecord } } #endregion #region: .\src\Exceptions\New-PSInvalidOperationException.ps1 function New-PSInvalidOperationException { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification='Function does not change system state.')] [CmdletBinding()] [OutputType([System.Management.Automation.ErrorRecord])] ## PARAMETERS ############################################################# param( [Parameter( Position = 0, Mandatory )] [ValidateNotNullOrEmpty()] [string] $Message, [Parameter()] [switch] $Throw ) ## PROCESS ################################################################ process { $ErrorRecord = [System.Management.Automation.ErrorRecord]::new( [System.Management.Automation.PSInvalidOperationException]::new($Message), "System.Management.Automation.PSInvalidOperationException", [System.Management.Automation.ErrorCategory]::InvalidOperation, $null ) if ($Throw) { throw $ErrorRecord } Write-Output $ErrorRecord } } #endregion #region: .\src\Exceptions\New-UnauthorizedAccessException.ps1 function New-UnauthorizedAccessException { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification='Function does not change system state.')] [CmdletBinding()] [OutputType([System.Management.Automation.ErrorRecord])] ## PARAMETERS ############################################################# param( [Parameter( Position = 0, Mandatory )] [ValidateNotNullOrEmpty()] [string] $Message, [Parameter()] [switch] $Throw ) ## PROCESS ################################################################ process { $ErrorRecord = [System.Management.Automation.ErrorRecord]::new( [System.UnauthorizedAccessException]::new($Message), "System.UnauthorizedAccessException", [System.Management.Automation.ErrorCategory]::PermissionDenied, $null ) if ($Throw) { throw $ErrorRecord } Write-Output $ErrorRecord } } #endregion #endregion #region: Public #region: .\src\Public\ConvertFrom-Base64String.ps1 function ConvertFrom-Base64String { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [CmdletBinding()] [OutputType([object])] ## PARAMETERS ############################################################# param ( [Parameter( Mandatory, ValueFromPipeline )] [ValidateNotNullOrEmpty()] [string[]] $InputObject ) ## PROCESS ################################################################ process { foreach ($Object in $InputObject) { try { $Bytes = [System.Convert]::FromBase64String($Object) try { $String = -join [char[]] $Bytes Write-Output ([System.Management.Automation.PSSerializer]::Deserialize($String)) } catch { Write-Output $Bytes } ## EXCEPTIONS ################################################# } catch [System.Management.Automation.MethodInvocationException] { $PSCmdlet.WriteError(( New-MethodInvocationException -Exception $_.Exception.InnerException )) } catch { $PSCmdlet.WriteError($_) } } } } #endregion #region: .\src\Public\ConvertTo-Base64String.ps1 function ConvertTo-Base64String { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [CmdletBinding()] [OutputType([string])] ## PARAMETERS ############################################################# param ( [Parameter( Mandatory, ValueFromPipeline )] [ValidateNotNullOrEmpty()] [object] $InputObject, [Parameter()] [int32] $Depth = 2 ) ## PROCESS ################################################################ process { try { try { $Bytes = [byte[]] $InputObject } catch { $Serialize = [System.Management.Automation.PSSerializer]::Serialize($InputObject, $Depth) $Bytes = [byte[]] $Serialize.ToCharArray() } Write-Output ([System.Convert]::ToBase64String($Bytes)) ## EXCEPTIONS ################################################# } catch [System.Management.Automation.MethodInvocationException] { $PSCmdlet.WriteError(( New-MethodInvocationException -Exception $_.Exception.InnerException )) } catch { $PSCmdlet.WriteError($_) } } } #endregion #region: .\src\Public\Find-NlMtu.ps1 function Find-NlMtu { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSPossibleIncorrectUsageOfAssignmentOperator', '', Justification='Assignment Operator is intended.')] [CmdletBinding()] [OutputType([object])] ## PARAMETERS ############################################################# param ( [Alias("Hostname", "IPAddress", "Address")] [Parameter( Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName )] [ValidateNotNullOrEmpty()] [string[]] $ComputerName, [Parameter()] [ValidateRange(1, [int32]::MaxValue)] [int32] $Timeout = 10000, [Alias("Ttl", "TimeToLive", "Hops")] [Parameter()] [ValidateRange(1, [int32]::MaxValue)] [int32] $MaxHops = 128 ) ## BEGIN ################################################################## begin { $PingOptions = [System.Net.NetworkInformation.PingOptions]::new($MaxHops, $true) } ## PROCESS ################################################################ process { foreach ($Computer in $ComputerName) { try { Use-Object ($Ping = [System.Net.NetworkInformation.Ping]::new()) { $UpperBound = 65500 $LowerBound = 1 $Size = 9000 $Buffer = [byte[]]::new($Size) $Result = [System.Collections.Generic.List[System.Net.NetworkInformation.PingReply]]::new() while ($Size -ne $LowerBound) { try { Write-Verbose ("PING {0} with {1}-byte payload" -f $Computer, $Size) $Reply = $Ping.Send($Computer, $Timeout, $Buffer, $PingOptions) } catch { New-InvalidOperationException -Message ("Connection to '{0}' failed." -f $Computer) -Throw } switch ($Reply.Status) { "PacketTooBig" { $UpperBound = $Size } "Success" { $LowerBound = $Size } "TimedOut" { $UpperBound = $Size } default { New-InvalidOperationException -Message ("Connection to '{0}' failed with status '{1}.'" -f $Computer, $Reply.Status) -Throw } } $Result.Add($Reply) if (($Size = [System.Math]::Floor(($LowerBound + $UpperBound) / 2)) -eq 1) { New-InvalidOperationException -Message ("Connection to '{0}' failed with status 'NoReply.'" -f $Computer) -Throw } [array]::Resize([ref] $Buffer, $Size) } if (($Hops = $MaxHops - $Result.Where({ $_.Status -eq "Success" })[-1].Options.Ttl) -lt 0) { $Hops = 0 } Write-Output ([pscustomobject] @{ ComputerName = $Computer ReplyFrom = $Result.Where({ $_.Status -eq "Success" })[-1].Address "Time(ms)" = [int] ($Result.Where({ $_.Status -eq "Success" }).RoundtripTime | Measure-Object -Average).Average Hops = $Hops # IP Header (20 bytes) + ICMP Header (8 bytes) = 28 bytes MTU = $Size + 28 }) } ## EXCEPTIONS ################################################# } catch { $PSCmdlet.WriteError($_) } } } ## END #################################################################### end { } } #endregion #region: .\src\Public\Get-ADServiceAccountCredential.ps1 function Get-ADServiceAccountCredential { # Copyright (c) 2021 Ryan Ephgrave, https://github.com/Ryan2065/gMSACredentialModule # Modified "Get-GMSACredential.ps1" by Anthony J. Raymond [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification='Retrieved password is plaintext.')] [CmdletBinding()] [OutputType([pscredential])] ## PARAMETERS ############################################################# param ( [Alias("distinguishedName", "objectGuid", "objectSid", "sAMAccountName")] [Parameter( Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName )] [ValidateNotNullOrEmpty()] [string[]] $Identity, [Parameter()] [ValidateNotNullOrEmpty()] [string] $Server ) ## BEGIN ################################################################## begin { $Properties = @( @{ n = "sAMAccountName"; e = { $_.Properties."samaccountname" } } @{ n = "Length"; e = { $_.Properties."msds-managedpassword".Length } } @{ n = "ManagedPassword"; e = { $Length = $_.Properties."msds-managedpassword".Length Write-Output ($IntPtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($Length)) [System.Runtime.InteropServices.Marshal]::Copy([byte[]] $_.Properties."msds-managedpassword".ForEach({ $_ }), 0, $IntPtr, $Length) } } ) } ## PROCESS ################################################################ process { foreach ($Object in $Identity) { try { try { # https://ldapwiki.com/wiki/ObjectGuid $ObjectGuid = ([guid] $Object).ToByteArray().ForEach({ $_.ToString("X2") }) -join "\" $Filter = "(&(objectGuid=\${ObjectGuid})(ObjectCategory=msDS-GroupManagedServiceAccount))" } catch { $Filter = "(&(|(distinguishedName={0})(objectSid={0})(sAMAccountName={1}))(ObjectCategory=msDS-GroupManagedServiceAccount))" -f $Object, ($Object -ireplace "[^$]$", "$&$") } New-Variable -Name ADServiceAccount -Option AllScope Use-Object ($DirectorySearcher = [System.DirectoryServices.DirectorySearcher] $Filter) { if ($Server) { $DirectorySearcher.SearchRoot = [System.DirectoryServices.DirectoryEntry] "LDAP://${Server}" } $DirectorySearcher.SearchRoot.AuthenticationType = "Sealing" $DirectorySearcher.PropertiesToLoad.AddRange(@("sAMAccountName", "msDS-ManagedPassword")) $ADServiceAccount = $DirectorySearcher.FindOne() | Select-Object -Property $Properties if (-not $ADServiceAccount) { New-ActiveDirectoryObjectNotFoundException -Message ("Cannot find an object with identity: '{0}' under: '{1}'." -f $Object, $DirectorySearcher.SearchRoot.distinguishedName) -Throw } elseif ($ADServiceAccount.Length -eq 0) { New-ActiveDirectoryOperationException -Message "Cannot retrieve service account password. A process has requested access to an object, but has not been granted those access rights." -Throw } } # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/a9019740-3d73-46ef-a9ae-3ea8eb86ac2e $SecureString = ConvertTo-SecureString -String ([System.Runtime.InteropServices.Marshal]::PtrToStringUni([int64] $ADServiceAccount.ManagedPassword + 16)) -AsPlainText -Force Write-Output ([pscredential]::new($ADServiceAccount.sAMAccountName, $SecureString)) ## EXCEPTIONS ################################################# } catch [System.Management.Automation.SetValueInvocationException] { $PSCmdlet.WriteError(( New-ActiveDirectoryServerDownException -Message "Unable to contact the server. This may be because this server does not exist, it is currently down, or it does not have the Active Directory Services running." )) } catch { $PSCmdlet.WriteError($_) } finally { if ($ADServiceAccount) { [System.Runtime.InteropServices.Marshal]::Copy([byte[]]::new($ADServiceAccount.Length), 0, $ADServiceAccount.ManagedPassword, $ADServiceAccount.Length) [System.Runtime.InteropServices.Marshal]::FreeHGlobal($ADServiceAccount.ManagedPassword) $ADServiceAccount = $SecureString = $null $null = [System.GC]::GetTotalMemory($true) } } } } ## END #################################################################### end { } } #endregion #region: .\src\Public\Get-FolderProperties.ps1 function Get-FolderProperties { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification='Named to match Windows context menu.')] [CmdletBinding()] [OutputType([object])] ## PARAMETERS ############################################################# param ( [Parameter( Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = "Path" )] [ValidateScript({ if (Test-Path -Path $_ -PathType Container) { return $true } throw "The argument specified must resolve to a valid folder path." })] [string[]] $Path, [Alias("PSPath")] [Parameter( Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = "LiteralPath" )] [ValidateScript({ if (Test-Path -LiteralPath $_ -PathType Container) { return $true } throw "The argument specified must resolve to a valid folder path." })] [string[]] $LiteralPath, [Parameter()] [ValidateSet( "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB", # Decimal Metric (Base 10) "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB" # Binary IEC (Base 2) )] [string] $Unit = "MiB" ) ## BEGIN ################################################################## begin { $Prefix = @{ [char] "K" = 1 # kilo [char] "M" = 2 # mega [char] "G" = 3 # giga [char] "T" = 4 # tera [char] "P" = 5 # peta [char] "E" = 6 # exa [char] "Z" = 7 # zetta [char] "Y" = 8 # yotta } $Base = $Unit.Contains("i") | Use-Ternary { 1024 } { 1000 } $Divisor = [System.Math]::Pow($Base, $Prefix[$Unit[0]]) } ## PROCESS ################################################################ process { $Process = ($PSCmdlet.ParameterSetName -cmatch "^LiteralPath") | Use-Ternary { Resolve-PoshPath -LiteralPath $LiteralPath } { Resolve-PoshPath -Path $Path } foreach ($Object in $Process) { try { if ($Object.Provider.Name -ne "FileSystem") { New-ArgumentException "The argument specified must resolve to a valid path on the FileSystem provider." -Throw } $Folder = [System.IO.DirectoryInfo] $Object.ProviderPath Write-Verbose ("GET {0}" -f $Folder) $Dirs = $Files = $Bytes = 0 # https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/robocopy $Result = robocopy $Folder.FullName.TrimEnd("\") \\null /l /e /np /xj /r:0 /w:0 /bytes /nfl /ndl if (($LASTEXITCODE -eq 16) -and ($Result[-2] -eq "Access is denied.")) { New-UnauthorizedAccessException -Message ("Access to the path '{0}' is denied." -f $Folder) -Throw } elseif ($LASTEXITCODE -eq 16) { New-ArgumentException -Message ("The specified path '{0}' is invalid." -f $Folder) -Throw } switch -Regex ($Result) { "Dirs :\s+(\d+)" { $Dirs = [double] $Matches[1] - 1 } "Files :\s+(\d+)" { $Files = [double] $Matches[1] } "Bytes :\s+(\d+)" { $Bytes = [double] $Matches[1] } } Write-Output ([pscustomobject] @{ FullName = $Folder.FullName Length = $Bytes Size = "{0:n2} {1}" -f ($Bytes / $Divisor), $Unit Contains = "{0} Files, {1} Folders" -f $Files, $Dirs Created = "{0:F}" -f $Folder.CreationTime }) ## EXCEPTIONS ################################################# } catch { $PSCmdlet.WriteError($_) } } } ## END #################################################################### end { } } #endregion #region: .\src\Public\Invoke-ExponentialBackoff.ps1 function Invoke-ExponentialBackoff { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [CmdletBinding()] [OutputType([object])] ## PARAMETERS ############################################################# param ( [Parameter( Position = 0, Mandatory )] [scriptblock] $ScriptBlock, [Parameter()] [int] $RetryCount = 3, [Parameter()] [int] $Base = 2, [Parameter()] [int] $Scalar = 1 ) ## PROCESS ################################################################ process { for ([int] $i = 0; $i -lt $RetryCount; $i++) { try { . $ScriptBlock break } catch { $PSCmdlet.WriteError($_) if (($i + 1) -ge $RetryCount) { $PSCmdlet.ThrowTerminatingError(( New-LimitException -Message ("The operation has reached the limit of {0} retries." -f $RetryCount) )) } Start-Sleep -Milliseconds ((Get-Random -Maximum 1000) * $Scalar * [System.Math]::Pow($Base, $i)) } } } } #endregion #region: .\src\Public\Join-File.ps1 function Join-File { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [CmdletBinding(SupportsShouldProcess)] [OutputType([object])] ## PARAMETERS ############################################################# param ( [Parameter( Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = "Path" )] [ValidateScript({ if (Test-Path -Path $_ -PathType Leaf -Filter *.*split) { return $true } throw "The argument specified must resolve to a valid split type file." })] [string[]] $Path, [Alias("PSPath")] [Parameter( Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = "LiteralPath" )] [ValidateScript({ if (Test-Path -LiteralPath $_ -PathType Leaf -Filter *.*split) { return $true } throw "The argument specified must resolve to a valid split type file." })] [string[]] $LiteralPath, [Parameter()] [ValidateScript({ if (Test-Path -LiteralPath $_ -IsValid) { return $true } throw "The argument specified must resolve to a valid file or folder path." })] [string] $Destination = (Get-Location -PSProvider FileSystem).ProviderPath ) ## BEGIN ################################################################## begin { $DestinationInfo = [System.IO.FileInfo] (Resolve-PoshPath -LiteralPath $Destination).ProviderPath } ## PROCESS ################################################################ process { $Process = ($PSCmdlet.ParameterSetName -cmatch "^LiteralPath") | Use-Ternary { Resolve-PoshPath -LiteralPath $LiteralPath } { Resolve-PoshPath -Path $Path } foreach ($Object in $Process) { try { if ($Object.Provider.Name -ne "FileSystem") { New-ArgumentException "The argument specified must resolve to a valid path on the FileSystem provider." -Throw } $File = [System.IO.FileInfo] $Object.ProviderPath $CalculatedDestination = $DestinationInfo.Extension | Use-Ternary { $DestinationInfo } { "{0}\{1}" -f $DestinationInfo.FullName.TrimEnd("\"), $File.BaseName } if ($PSCmdlet.ShouldProcess($CalculatedDestination, "Write Content")) { if (-not ($Directory = $DestinationInfo.Extension | Use-Ternary { $DestinationInfo.Directory } { $DestinationInfo }).Exists) { $null = [System.IO.Directory]::CreateDirectory($Directory) } Write-Verbose ("WRITE {0}" -f $CalculatedDestination) Use-Object ($Writer = [System.IO.File]::OpenWrite($CalculatedDestination)) { # sort to fix ChildItem number sorting foreach ($SplitFile in (Get-ChildItem -Path ("{0}\{1}.*split" -f $File.Directory, $File.BaseName)).FullName | Sort-Object -Property @{e = { [int32] [regex]::Match($_, "\.(\d+)split$").Groups[1].Value } }) { Write-Verbose ("READ {0}" -f $SplitFile) $Bytes = [System.IO.File]::ReadAllBytes($SplitFile) $Writer.Write($Bytes, 0, $Bytes.Length) } } } Write-Output (Get-ChildItem -Path $CalculatedDestination) ## EXCEPTIONS ################################################# } catch [System.Management.Automation.MethodInvocationException] { $PSCmdlet.WriteError(( New-MethodInvocationException -Exception $_.Exception.InnerException )) } catch { $PSCmdlet.WriteError($_) } } } ## END #################################################################### end { } } #endregion #region: .\src\Public\New-Exception.ps1 function New-Exception { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification='Function does not change system state.')] [CmdletBinding()] [OutputType([System.Management.Automation.ErrorRecord])] ## PARAMETERS ############################################################# param( [Parameter( Position = 0, Mandatory )] [ValidateNotNullOrEmpty()] [string] $Message, [Parameter()] [switch] $Throw ) ## PROCESS ################################################################ process { $ErrorRecord = [System.Management.Automation.ErrorRecord]::new( [System.Exception]::new($Message), "System.Exception", [System.Management.Automation.ErrorCategory]::NotSpecified, $null ) if ($Throw) { throw $ErrorRecord } Write-Output $ErrorRecord } } #endregion #region: .\src\Public\New-IPAddress.ps1 function New-IPAddress { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification='Function does not change system state.')] [CmdletBinding()] [OutputType([object])] ## PARAMETERS ############################################################# param ( [Alias("Address")] [Parameter( Position = 0, Mandatory, ValueFromPipeline )] [string[]] $IPAddress ) ## PROCESS ################################################################ process { foreach ($Address in $IPAddress) { try { Write-Output ([System.Net.IPAddress]::Parse($Address)) ## EXCEPTIONS ################################################# } catch [System.Management.Automation.MethodInvocationException] { $PSCmdlet.WriteError(( New-MethodInvocationException -Exception $_.Exception.InnerException )) } catch { $PSCmdlet.WriteError($_) } } } } #endregion #region: .\src\Public\New-IPSubnet.ps1 function New-IPSubnet { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification='Function does not change system state.')] [CmdletBinding()] [OutputType([object])] ## PARAMETERS ############################################################# param ( [Parameter( Position = 0, Mandatory, ValueFromPipeline, ParameterSetName = "InputObject" )] [string[]] $InputObject, [Alias("Address")] [Parameter( Mandatory, ParameterSetName = "IPAddress" )] [string] $IPAddress, [Alias("Prefix")] [Parameter( Mandatory, ParameterSetName = "IPAddress" )] [int] $IPPrefix ) ## PROCESS ################################################################ process { foreach ($Object in $PSBoundParameters[$PSCmdlet.ParameterSetName]) { try { if ($PSCmdlet.ParameterSetName -eq "InputObject") { $IPAddress = ($Object -isplit "\\|\/")[0] $IPPrefix = ($Object -isplit "\\|\/")[-1] } Write-Output ([System.Net.IPSubnet]::Parse($IPAddress, $IPPrefix)) ## EXCEPTIONS ################################################# } catch [System.Management.Automation.MethodException] { $PSCmdlet.WriteError(( New-MethodInvocationException -Exception $_.Exception.InnerException )) } catch { $PSCmdlet.WriteError($_) } } } } #endregion #region: .\src\Public\Resolve-PoshPath.ps1 function Resolve-PoshPath { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [CmdletBinding()] [OutputType([object])] ## PARAMETERS ############################################################# param ( [Parameter( Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = "Path" )] [ValidateNotNullOrEmpty()] [string[]] $Path, [Alias("PSPath")] [Parameter( Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = "LiteralPath" )] [ValidateNotNullOrEmpty()] [string[]] $LiteralPath ) ## PROCESS ################################################################ process { foreach ($PSPath in $PSBoundParameters[$PSCmdlet.ParameterSetName]) { try { if ($PSCmdlet.ParameterSetName -eq "Path") { try { $PSPath = $PSCmdlet.SessionState.Path.GetResolvedPSPathFromPSPath($PSPath) } catch [System.Management.Automation.MethodInvocationException] { $PSPath = $_.Exception.InnerException.ItemName } } foreach ($String in $PSPath) { $Provider = $Drive = $null $ProviderPath = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($String, [ref] $Provider, [ref] $Drive) Write-Output ([pscustomobject] @{ ProviderPath = $ProviderPath Provider = $Provider Drive = $Drive }) } ## EXCEPTIONS ################################################# } catch [System.Management.Automation.MethodInvocationException] { $PSCmdlet.WriteError(( New-MethodInvocationException -Exception $_.Exception.InnerException )) } catch { $PSCmdlet.WriteError($_) } } } } #endregion #region: .\src\Public\Split-File.ps1 function Split-File { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [CmdletBinding(SupportsShouldProcess)] [OutputType([object])] ## PARAMETERS ############################################################# param ( [Parameter( Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = "Path" )] [ValidateScript({ if (Test-Path -Path $_ -PathType Leaf) { return $true } throw "The argument specified must resolve to a valid file path." })] [string[]] $Path, [Alias("PSPath")] [Parameter( Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = "LiteralPath" )] [ValidateScript({ if (Test-Path -LiteralPath $_ -PathType Leaf) { return $true } throw "The argument specified must resolve to a valid file path." })] [string[]] $LiteralPath, [Parameter()] [ValidateScript({ if (Test-Path -LiteralPath $_ -IsValid) { return $true } throw "The argument specified must resolve to a valid file or folder path." })] [string] $Destination = (Get-Location -PSProvider FileSystem).ProviderPath, [Parameter ( Mandatory )] [ValidateRange(0, [int32]::MaxValue)] [int32] $Size ) ## BEGIN ################################################################## begin { $DestinationInfo = [System.IO.FileInfo] (Resolve-PoshPath -LiteralPath $Destination).ProviderPath } ## PROCESS ################################################################ process { $Process = ($PSCmdlet.ParameterSetName -cmatch "^LiteralPath") | Use-Ternary { Resolve-PoshPath -LiteralPath $LiteralPath } { Resolve-PoshPath -Path $Path } foreach ($Object in $Process) { try { if ($Object.Provider.Name -ne "FileSystem") { New-ArgumentException "The argument specified must resolve to a valid path on the FileSystem provider." -Throw } $File = [System.IO.FileInfo] $Object.ProviderPath Write-Verbose ("READ {0}" -f $File) Use-Object ($Reader = [System.IO.File]::OpenRead($File)) { $Buffer = [byte[]]::new($Size) $Count = 1 $CalculatedDestination = $DestinationInfo.Extension | Use-Ternary { "{0}\{1}" -f $DestinationInfo.Directory.FullName, $File.Name } { "{0}\{1}" -f $DestinationInfo.FullName.TrimEnd("\"), $File.Name } while ($Read = $Reader.Read($Buffer, 0, $Buffer.Length)) { if ($Read -ne $Buffer.Length) { [array]::Resize([ref] $Buffer, $Read) } $SplitFile = "{0}.{1}split" -f $CalculatedDestination, $Count if ($PSCmdlet.ShouldProcess($SplitFile, "Write Content")) { if (-not ($Directory = $DestinationInfo.Extension | Use-Ternary { $DestinationInfo.Directory } { $DestinationInfo }).Exists) { $null = [System.IO.Directory]::CreateDirectory($Directory) } Write-Verbose ("WRITE {0}" -f $SplitFile) [System.IO.File]::WriteAllBytes($SplitFile, $Buffer) } $Count++ } # sort to fix ChildItem number sorting Write-Output (Get-ChildItem -Path ("{0}.*split" -f $CalculatedDestination) | Sort-Object -Property @{e = { [int32] [regex]::Match($_.FullName, "\.(\d+)split$").Groups[1].Value } }) } ## EXCEPTIONS ################################################# } catch [System.Management.Automation.MethodInvocationException] { $PSCmdlet.WriteError(( New-MethodInvocationException -Exception $_.Exception.InnerException )) } catch { $PSCmdlet.WriteError($_) } } } ## END #################################################################### end { } } #endregion #region: .\src\Public\Start-PoshLog.ps1 function Start-PoshLog { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '', Justification='As designed to receive log events.')] [CmdletBinding(DefaultParameterSetName = "Path")] [OutputType([void])] ## PARAMETERS ############################################################# param ( [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = "Path" )] [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = "PathAppend" )] [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = "PathNoClobber" )] [ValidateScript({ if (Test-Path -Path $_ -IsValid) { return $true } throw "The argument specified must resolve to a valid file or folder path." })] [string[]] $Path = [Environment]::GetFolderPath("MyDocuments"), [Alias("PSPath")] [Parameter( Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = "LiteralPath" )] [Parameter( Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = "LiteralPathAppend" )] [Parameter( Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = "LiteralPathNoClobber" )] [ValidateScript({ if (Test-Path -LiteralPath $_ -IsValid) { return $true } throw "The argument specified must resolve to a valid file or folder path." })] [string[]] $LiteralPath, [Parameter(ParameterSetName = "PathAppend")] [Parameter(ParameterSetName = "LiteralPathAppend")] [switch] $Append, [Parameter(ParameterSetName = "PathNoClobber")] [Parameter(ParameterSetName = "LiteralPathNoClobber")] [switch] $NoClobber, [Parameter()] [switch] $AsUtc, [Parameter()] [switch] $PassThru ) ## BEGIN ################################################################## begin { $DateTime = [datetime]::Now $Format = $AsUtc | Use-Ternary { "yyyy\-MM\-dd HH:mm:ss\Z", "ToUniversalTime", "yyyyMMdd\-HHmmss\Z" } { "yyyy\-MM\-dd HH:mm:ss", "ToLocalTime", "yyyyMMdd\-HHmmss" } $FileMode = $Append | Use-Ternary { [System.IO.FileMode]::Append } { $NoClobber | Use-Ternary { [System.IO.FileMode]::CreateNew } { [System.IO.FileMode]::Create } } $Template = { "**********************" "Windows PowerShell log start" "Version: {0} ({1})" -f $PSVersionTable.PSVersion, $PSVersionTable.PSEdition "Start time: {0:$( $Format[0] )}" -f $DateTime.($Format[1]).Invoke() "**********************" } } ## PROCESS ################################################################ process { $Process = ($PSCmdlet.ParameterSetName -cmatch "^LiteralPath") | Use-Ternary { Resolve-PoshPath -LiteralPath $LiteralPath } { Resolve-PoshPath -Path $Path } foreach ($Object in $Process) { try { if ($Object.Provider.Name -ne "FileSystem") { New-ArgumentException "The argument specified must resolve to a valid path on the FileSystem provider." -Throw } $FileInfo = [System.IO.FileInfo] $Object.ProviderPath if (-not ($Directory = $FileInfo.Extension | Use-Ternary { $FileInfo.Directory } { $FileInfo }).Exists) { $null = [System.IO.Directory]::CreateDirectory($Directory) } if (-not $FileInfo.Extension) { $FileInfo = [System.IO.FileInfo] ("PowerShell_log.{0}.{1:$( $Format[2] )}.txt" -f ([guid]::NewGuid() -isplit "-")[0], $DateTime.($Format[1]).Invoke()) } Use-Object ($File = [System.IO.File]::Open($Directory.FullName + "\" + $FileInfo.Name, $FileMode)) { if ($Append) { $NewLine = [System.Text.Encoding]::UTF8.GetBytes([System.Environment]::NewLine) $File.Write($NewLine, 0, $NewLine.Length) } foreach ($Line in $Template.Invoke()) { $Bytes = [System.Text.Encoding]::UTF8.GetBytes($Line + [System.Environment]::NewLine) $File.Write($Bytes, 0, $Bytes.Length) } } $Global:PSLogDetails += @(@{ Path = $File.Name; Utc = $AsUtc }) Write-Information -InformationAction Continue -MessageData ("Log started, output file is '{0}'" -f $File.Name) -InformationVariable null ## EXCEPTIONS ################################################# } catch [System.Management.Automation.MethodInvocationException] { $PSCmdlet.WriteError(( New-MethodInvocationException -Exception $_.Exception.InnerException )) } catch { $PSCmdlet.WriteError($_) } } } ## END #################################################################### end { if (-not (Get-EventSubscriber | Where-Object SourceIdentifier -CMatch "^PSLog") -and $PSLogDetails) { $Global:PSLogInformation = [System.Collections.ObjectModel.ObservableCollection[System.Management.Automation.InformationRecord]]::new() $Global:PSLogWarning = [System.Collections.ObjectModel.ObservableCollection[System.Management.Automation.WarningRecord]]::new() $Global:PSLogError = [System.Collections.ObjectModel.ObservableCollection[System.Management.Automation.ErrorRecord]]::new() $Action = { Write-PoshLog -PSEventArgs $Event } $null = Register-ObjectEvent -EventName CollectionChanged -InputObject $PSLogInformation -SourceIdentifier PSLogInformation -Action $Action $null = Register-ObjectEvent -EventName CollectionChanged -InputObject $PSLogWarning -SourceIdentifier PSLogWarning -Action $Action $null = Register-ObjectEvent -EventName CollectionChanged -InputObject $PSLogError -SourceIdentifier PSLogError -Action $Action $Global:PSDefaultParameterValues["Write-Information:InformationVariable"] = "+PSLogInformation" $Global:PSDefaultParameterValues["Write-Warning:WarningVariable"] = "+PSLogWarning" $Global:PSDefaultParameterValues["Write-Error:ErrorVariable"] = "+PSLogError" } if ($PassThru) { Write-Output $PSLogDetails.Path -NoEnumerate } } } #endregion #region: .\src\Public\Stop-PoshLog.ps1 function Stop-PoshLog { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [CmdletBinding()] [OutputType([void])] ## PARAMETERS ############################################################# param () ## BEGIN ################################################################## begin { if (-not $PSLogDetails) { $PSCmdlet.ThrowTerminatingError(( New-PSInvalidOperationException -Message "An error occurred stopping the log: The host is not currently logging." )) } if ($Events = Get-EventSubscriber | Where-Object SourceIdentifier -CMatch "^PSLog") { $Events | Unregister-Event $Global:PSDefaultParameterValues.Remove("Write-Information:InformationVariable") $Global:PSDefaultParameterValues.Remove("Write-Warning:WarningVariable") $Global:PSDefaultParameterValues.Remove("Write-Error:ErrorVariable") } $DateTime = [datetime]::Now $Template = { "**********************" "Windows PowerShell log end" "End time: {0:$( $Format[0] )}" -f $DateTime.($Format[1]).Invoke() "**********************" } } ## PROCESS ################################################################ process { foreach ($PSLog in $PSLogDetails) { try { $Format = $PSLog.Utc | Use-Ternary { "yyyy\-MM\-dd HH:mm:ss\Z", "ToUniversalTime" } { "yyyy\-MM\-dd HH:mm:ss", "ToLocalTime" } Use-Object ($File = [System.IO.File]::AppendText($PSLog.Path)) { foreach ($Line in $Template.Invoke()) { $File.WriteLine($Line) } } Write-Information -InformationAction Continue -MessageData ("Log stopped, output file is '{0}'" -f $PSLog.Path) -InformationVariable null ## EXCEPTIONS ################################################# } catch [System.Management.Automation.MethodInvocationException] { $PSCmdlet.WriteError(( New-MethodInvocationException -Exception $_.Exception.InnerException )) } catch { $PSCmdlet.WriteError($_) } } } ## END #################################################################### end { Remove-Variable -Name PSLog* -Scope Global } } #endregion #region: .\src\Public\Use-ErrorCoalescing.ps1 function Use-ErrorCoalescing { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [CmdletBinding()] [Alias("?!")] [OutputType([object])] ## PARAMETERS ############################################################# param ( [Parameter( Mandatory, ValueFromPipeline )] [scriptblock] $InputObject, [Parameter( Position = 0, Mandatory )] [object] $IfError ) ## PROCESS ################################################################ process { # wrapping in an array to handle $null as input foreach ($Object in @($InputObject)) { try { . $Object } catch { if ($IfError -is [scriptblock]) { . $IfError } else { Write-Output $IfError -NoEnumerate } } } } } #endregion #region: .\src\Public\Use-NullCoalescing.ps1 function Use-NullCoalescing { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [CmdletBinding()] [Alias("??")] [OutputType([object])] ## PARAMETERS ############################################################# param ( [Parameter( Mandatory, ValueFromPipeline )] [AllowNull()] [AllowEmptyString()] [AllowEmptyCollection()] [object] $InputObject, [Parameter( Position = 0, Mandatory )] [object] $IfNull ) ## PROCESS ################################################################ process { # wrapping in an array to handle $null as input foreach ($Object in @($InputObject)) { try { if (($null -eq $Object) -and ($IfNull -is [scriptblock])) { . $IfNull } elseif ($null -eq $Object) { Write-Output $IfNull -NoEnumerate } else { Write-Output $Object -NoEnumerate } } catch { $PSCmdlet.WriteError($_) } } } } #endregion #region: .\src\Public\Use-Object.ps1 function Use-Object { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [CmdletBinding()] [OutputType([object])] ## PARAMETERS ############################################################# param ( [Parameter( Position = 0, Mandatory )] [AllowEmptyString()] [AllowEmptyCollection()] [AllowNull()] [object] $InputObject, [Parameter( Position = 1, Mandatory )] [scriptblock] $ScriptBlock ) ## PROCESS ################################################################ process { try { . $ScriptBlock } catch { throw $_ } finally { foreach ($Object in $InputObject) { if ($Object -is [System.IDisposable]) { $Object.Dispose() } elseif ([System.Runtime.InteropServices.Marshal]::IsComObject($Object)) { $null = [System.Runtime.InteropServices.Marshal]::ReleaseComObject($Object) } } } } } #endregion #region: .\src\Public\Use-Ternary.ps1 function Use-Ternary { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [CmdletBinding()] [Alias("?:")] [OutputType([object])] ## PARAMETERS ############################################################# param ( [Parameter( Mandatory, ValueFromPipeline )] [AllowNull()] [AllowEmptyString()] [AllowEmptyCollection()] [object] $InputObject, [Parameter( Position = 0, Mandatory )] [object] $IfTrue, [Parameter( Position = 1, Mandatory )] [object] $IfFalse ) ## PROCESS ################################################################ process { # wrapping in an array to handle $null as input foreach ($Object in @($InputObject)) { try { if ($Object -and ($IfTrue -is [scriptblock])) { . $IfTrue } elseif ($Object) { Write-Output $IfTrue -NoEnumerate } elseif ($IfFalse -is [scriptblock]) { . $IfFalse } else { Write-Output $IfFalse -NoEnumerate } } catch { $PSCmdlet.WriteError($_) } } } } #endregion #region: .\src\Public\Write-PoshLog.ps1 function Write-PoshLog { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [CmdletBinding( DefaultParameterSetName = "Type" )] [OutputType([void])] ## PARAMETERS ############################################################# param ( [Parameter( Mandatory, DontShow, ParameterSetName = "PSEventArgs" )] [ValidateNotNullOrEmpty()] [System.Management.Automation.PSEventArgs] $PSEventArgs, [Parameter( ParameterSetName = "Type" )] [ValidateSet("Log", "Information", "Warning", "Error")] [string] $Type = "Log", [Parameter( Position = 0, Mandatory, ParameterSetName = "Type" )] [string] $Message ) ## BEGIN ################################################################## begin { if (-not $PSLogDetails) { $PSCmdlet.ThrowTerminatingError(( New-PSInvalidOperationException -Message "An error occurred writing the log: The host is not currently logging." )) } $TypeMap = @{ 'Log' = "LOG" 'Information' = "INFO" 'Warning' = "WARN" 'Error' = "ERROR" } $Template = { "{0:$( $Format[0] )}`t[{1}] `t{2}" -f $DateTime.($Format[1]).Invoke(), $TypeMap.$Type, $Message } if ($PSEventArgs) { $DateTime = $PSEventArgs.TimeGenerated switch ($PSEventArgs.SourceIdentifier) { "PSLogInformation" { $Type = "Information" $Message = $PSEventArgs.SourceEventArgs.NewItems.MessageData } "PSLogWarning" { $Type = "Warning" $Message = $PSEventArgs.SourceEventArgs.NewItems.Message } "PSLogError" { $Type = "Error" $Message = $PSEventArgs.SourceEventArgs.NewItems.Exception.Message } } } else { $DateTime = [datetime]::Now } } ## PROCESS ################################################################ process { foreach ($PSLog in $PSLogDetails) { try { $Format = $PSLog.Utc | Use-Ternary { "yyyy\-MM\-dd HH:mm:ss\Z", "ToUniversalTime" } { "yyyy\-MM\-dd HH:mm:ss", "ToLocalTime" } Use-Object ($File = [System.IO.File]::AppendText($PSLog.Path)) { $File.WriteLine($Template.Invoke()[0]) } ## EXCEPTIONS ################################################# } catch [System.Management.Automation.MethodInvocationException] { $PSCmdlet.WriteError(( New-MethodInvocationException -Exception $_.Exception.InnerException )) } catch { $PSCmdlet.WriteError($_) } } } ## END #################################################################### end { } } #endregion #endregion |