Classes/IPSubnet.ps1

# Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details)

class IPSubnet : System.Object {
    ## PROPERTIES #############################################################
    [System.Net.IPAddress] $Network
    [System.Net.Sockets.AddressFamily] $AddressFamily
    [int] $Prefix
    [System.Net.IPAddress] $SubnetMask
    [System.Net.IPAddress] $LastAddress

    [bigint] hidden $PrefixInt
    [bigint] hidden $StartInt
    [bigint] hidden $EndInt

    ## METHODS ################################################################
    static [IPSubnet] Parse([System.Net.IPAddress] $IPAddress, [int] $Prefix) {
        return ([IPSubnet]::new($IPAddress, $Prefix))
    }

    static [bool] TryParse([object] $IPAddress, [object] $Prefix, [ref] $Variable) {
        try {
            $Variable.Value = [IPSubnet]::new($IPAddress, $Prefix)

            return $true
        } catch {
            return $false
        }
    }

    [string] ToString() {
        return ("{0}/{1}" -f $this.Network, $this.Prefix)
    }

    [bool] Contains([System.Net.IPAddress] $IPAddress) {
        $ContainsAddress = $this.ToAddress($IPAddress, $false) -band $this.PrefixInt
        $NetworkAddress = $this.ToAddress($this.Network, $false)

        return ($NetworkAddress -eq $ContainsAddress)
    }

    [IPSubnet[]] Subnet([int] $Prefix) {
        $Power = switch ($this.AddressFamily) {
            "InterNetwork" { [bigint]::Pow(2, (32 - $Prefix)) }
            "InterNetworkV6" { [bigint]::Pow(2, (128 - $Prefix)) }
        }

        $Subnets = for ($AddressInt = $this.StartInt; $AddressInt -le $this.EndInt; $AddressInt += $Power) {
            [IPSubnet]::new(($this.FromAddress($AddressInt, $true)), $Prefix)
        }

        return $Subnets
    }

    [bigint] hidden ToAddress([System.Net.IPAddress] $IPAddress, [bool] $Reverse) {
        [byte[]] $Bytes = $IPAddress.GetAddressBytes()

        if ($Reverse) {
            [array]::Reverse($Bytes)
        }

        return ([bigint]::new($Bytes + 0))
    }

    [System.Net.IPAddress] hidden FromAddress([bigint] $BigInt, [bool] $Reverse) {
        [byte[]] $Bytes = $BigInt.ToByteArray()

        switch ($this.AddressFamily) {
            "InterNetwork" { [array]::Resize([ref] $Bytes, 4) }
            "InterNetworkV6" { [array]::Resize([ref] $Bytes, 16) }
        }

        if ($Reverse) {
            [array]::Reverse($Bytes)
        }

        return ([System.Net.IPAddress] $Bytes)
    }

    [bigint] hidden GetPrefixInt([int] $Int) {
        $Binary = switch ($this.AddressFamily) {
            "InterNetwork" { ('1' * $Int).PadRight(32, '0') }
            "InterNetworkV6" { ('1' * $Int).PadRight(128, '0') }
        }

        $Bytes = $Binary -isplit "(?<byte>[01]{8})", 0, "ExplicitCapture" |
            Where-Object { $_ } | ForEach-Object { [System.Convert]::ToUInt32($_, 2) }

        # append zero byte for unsigned
        return ([bigint]::new($Bytes + 0))
    }

    ## CONSTRUCTORS ###########################################################
    IPSubnet([System.Net.IPAddress] $IPAddress, [int] $Prefix) {
        $this.Prefix = $Prefix
        $this.AddressFamily = $IPAddress.AddressFamily

        switch ($true) {
            { $this.AddressFamily -eq "InterNetwork" -and $this.Prefix -iin 0..32 } { continue }
            { $this.AddressFamily -eq "InterNetworkV6" -and $this.Prefix -iin 0..128 } { continue }
            default { throw ([System.InvalidCastException] "An invalid prefix for the given address family was specified.") }
        }

        $this.PrefixInt = $this.GetPrefixInt($this.Prefix)
        $this.SubnetMask = $this.FromAddress($this.PrefixInt, $false)

        $StartAddress = $this.ToAddress($IPAddress, $false) -band $this.PrefixInt
        $this.Network = $this.FromAddress($StartAddress, $false)

        $EndAddress = switch ($this.AddressFamily) {
            "InterNetwork" { $StartAddress -bor (-bnot $this.PrefixInt -band ([bigint]::Pow(2, 32) - 1)) }
            "InterNetworkV6" { $StartAddress -bor (-bnot $this.PrefixInt -band ([bigint]::Pow(2, 128) - 1)) }
        }
        $this.LastAddress = $this.FromAddress($EndAddress, $false)

        $this.StartInt = $this.ToAddress($this.Network, $true)
        $this.EndInt = $this.ToAddress($this.LastAddress, $true)
    }
}