Class/DhcpOptionObject.psm1

using module '.\Enums.psm1'

# DHCP Option object
Class DhcpOptionObject {
    [byte]$OptionCode
    [string]$Name

    [ValidateCount(0, 1024)]
    Hidden [byte[]]$_Value

    [ValidateRange(1, 255)]
    Hidden [byte]$SplitSize = 255

    DhcpOptionObject([byte]$OptionCode, [byte[]]$Value) {
        $this.OptionCode = $OptionCode
        $this._Value = $Value
        $this.Name = ($OptionCode -as [DhcpOption])

        $this | Add-Member ScriptProperty 'Value' {
            $this.ParseValue($this._Value)
        }

        $this | Add-Member ScriptProperty 'Length' {
            [byte]$this._Value.Count
        }
    }

    DhcpOptionObject([byte]$OptionCode) {
        $this.OptionCode = $OptionCode
        $this._Value = $null
        $this.Name = ($OptionCode -as [DhcpOption])

        $this | Add-Member ScriptProperty 'Value' {
            $this.ParseValue($this._Value)
        }

        $this | Add-Member ScriptProperty 'Length' {
            [byte]$this._Value.Count
        }
    }

    [byte[]]GetBytes() {
        $ByteArray = New-Object 'System.Collections.Generic.List[byte]'
        if ($null -eq $this._Value) {
            $ByteArray.Add($this.OptionCode)
            $ByteArray.Add(0)
            return $ByteArray.ToArray()
        }
        else {
            $Reader = [System.IO.BinaryReader]::new((New-Object IO.MemoryStream(@(, $this._Value))))
            try {
                (1..([math]::Ceiling($this._Value.Count / $this.SplitSize))) | ForEach-Object {
                    $ByteArray.Add($this.OptionCode)
                    $Length = [Math]::Min(($Reader.BaseStream.Length - $Reader.BaseStream.Position), $this.SplitSize)
                    $ByteArray.Add($Length)
                    $ByteArray.AddRange($Reader.ReadBytes($Length))
                }
            }
            finally {
                $Reader.Close()
            }
            return $ByteArray.ToArray()
        }
    }

    static [DhcpOptionObject]Parse([byte[]]$Bytes) {
        if ($Bytes.Count -le 2) { throw [System.ArgumentException]::new() }
        else {
            $length = $Bytes[1]
            return [DhcpOptionObject]::new($Bytes[0], $Bytes[ - $length..-1])
        }
    }

    Hidden [Object] ParseValue([byte[]]$Value) {
        try {
            switch ($this.OptionCode -as [DhcpOption]) {
                { $_ -in ('SubnetMask', 'ServerId', 'RequestedIPAddress') } {
                    # Single IP address
                    return [ipaddress]::new($Value[0..3])
                }
                { $_ -in ('Router', 'TimeServer', 'NameServer', 'DomainNameServer', 'NTPServers', 'NETBIOSNameSrv') } {
                    # Multiple IP addresses
                    $OptionValue = [ipaddress[]]@()
                    for ($i = 0; ($i + 4) -le $Value.Count; $i += 4) {
                        $OptionValue += [ipaddress]::new($Value[$i..($i + 3)])
                    }
                    return $OptionValue
                }
                { $_ -in ('DomainName', 'Hostname', 'ClassId', 'WebProxyAutoDiscovery', 'PCode', 'TCode') } {
                    # String
                    return [string]::new($Value)
                }
                { $_ -in ('IPAddressLeaseTime', 'RenewalTime', 'RebindingTime', 'ARPTimeout') } {
                    # TimeSpan (UInt32)
                    # Convert big endian order bytes to UInt32 seconds
                    $Ticks = [int64]([ipaddress]::NetworkToHostOrder([System.BitConverter]::ToInt64(([byte[]]::new(4) + $Value), 0)) * 1e7)
                    return [timespan]::new($Ticks)
                }
                TimeOffset {
                    # TimeSpan (Int32)
                    $Ticks = [Int64](([ipaddress]::NetworkToHostOrder([System.BitConverter]::ToInt32($Value, 0))) * 1e7)
                    return [timespan]::new($Ticks)
                }
                DomainSearch {
                    # multiple strings
                    # RFC 3397
                    return [DhcpOptionObject]::ParseDomainSearchList($Value)
                }
                SIPServersDHCPOption {
                    # multiple strings or IP addresses
                    # RFC 3361
                    $Enc = $Value[0]
                    $Data = $Value[1..($Value.Length - 1)]
                    if ($Enc -eq 0) {
                        return [DhcpOptionObject]::ParseDomainSearchList($Data)
                    }
                    else {
                        $OptionValue = [ipaddress[]]@()
                        for ($i = 0; ($i + 4) -le $Data.Count; $i += 4) {
                            $OptionValue += [ipaddress]::new($Data[$i..($i + 3)])
                        }
                        return $OptionValue
                    }
                }
                DHCPMessageType {
                    return ($Value[0] -as [DhcpMessageType])
                }
                Default {
                    return $Value
                }
            }
        }
        catch {}
        return $Value
    }

    Hidden static [string[]] ParseDomainSearchList([byte[]]$Value) {
        # Ref: RFC 1035, 3396, 3397
        $DomainSearchList = [System.Collections.Generic.List[string]]::new()
        $Domain = @()
        $NextPosition = 0
        for ($idx = 0; $idx -lt $Value.Length; ) {
            $Length = $Value[$idx++]
            if (0 -eq $Length) {
                # detects end
                if ($NextPosition -gt $idx) {
                    # back to pointer
                    $idx = $NextPosition
                }

                if ($Domain.Count -gt 0) {
                    $DomainSearchList.Add($Domain -join '.')
                }
                $Domain = @()
                continue
            }
            elseif ($Length -ge 0xc0) {
                # detects compression pointer
                $HigherOctet = (($Length -band 0x3f) -shl 8)
                if ($idx -lt $Value.Length) {
                    $LowerOctet = $Value[$idx]
                }
                else { break }
                $CPointer = [int]($HigherOctet + $LowerOctet)
                $NextPosition = ++$idx
                $idx = $CPointer
                continue
            }
            else {
                # continue reading
                $lastIdx = ($idx + $Length - 1)
                if ($lastIdx -lt $Value.Length) {
                    $Domain += [string]::new($Value[$idx..$lastIdx])
                }
                else { break }
                $idx += $Length
                continue
            }
        }

        return $DomainSearchList.ToArray()
    }

    Hidden static [byte[]] ConvertDomainSearchListToBytes([string[]]$Domains) {
        # Ref: RFC 1035, 3397
        [byte[]]$Result = $null
        $IdnMapping = [System.Globalization.IdnMapping]::new()
        $PointerList = @{}
        $Writer = [System.IO.BinaryWriter]::new([System.IO.MemoryStream]::new())
        try {
            foreach ($Domain in $Domains) {
                while (-not [string]::IsNullOrWhiteSpace($Domain)) {
                    # Convert internationalized domain names to Punycode
                    $Domain = $IdnMapping.GetAscii($Domain.Trim("`0", '.').Trim())

                    if ($PointerList.ContainsKey($Domain)) {
                        if ($PointerList[$Domain] -ge 0 -and $PointerList[$Domain] -le 0x3fff) {
                            # Compression pointer (2 bytes big endian)
                            $Pointer = [System.BitConverter]::GetBytes([ipaddress]::NetworkToHostOrder([Int32]((0xc0 -shl 8) + $PointerList[$Domain])))[2..3]
                            $Writer.Write([byte[]]$Pointer)
                            break
                        }
                    }

                    if ($Domain.IndexOf('.') -le 0) {
                        $PointerList[$Domain] = $Writer.BaseStream.Position
                        $TLDBytes = [System.Text.Encoding]::UTF8.GetBytes($Domain)
                        $Writer.Write([byte]$TLDBytes.Length)
                        $Writer.Write([byte[]]$TLDBytes)
                        # End flag
                        $Writer.Write([byte]0x00)
                        break
                    }
                    else {
                        $PointerList[$Domain] = $Writer.BaseStream.Position
                        $SplitDomain = $Domain.Split('.', 2, 1)
                        $Domain = $SplitDomain[1]
                        $LLD = $SplitDomain[0].Trim("`0").Trim()
                        $LLDBytes = [System.Text.Encoding]::UTF8.GetBytes($LLD)
                        $Writer.Write([byte]$LLDBytes.Length)
                        $Writer.Write([byte[]]$LLDBytes)
                    }
                }
            }
        }
        finally {
            $Result = $Writer.BaseStream.ToArray()
            $Writer.Close()
        }

        return $Result
    }

    [string] ToString() {
        return ('{0} ({1})' -f $this.Name, $this.Value)
    }
}