classes/BindRecordClass.ps1
class BindRecord:System.IComparable { [string]$HostName [ValidateRange( 60, [int]::MaxValue )] [System.Nullable[int]]$TimeToLive [string]$RecordClass [System.Nullable[BindRecordType]]$RecordType [string]$RecordData [string]$Comment static [string[]] $ValidRecordProperties = 'HostName', 'TimeToLive', 'RecordClass', 'RecordType', 'RecordData', 'Comment' static [bool] IsValidRecordProperty ( [string] $PropertyName ) { return $PropertyName -in [BindRecord]::ValidRecordProperties } BindRecord () {} BindRecord ( [string]$Record ) { $this.__ParseString( $Record, '' ) } BindRecord ( [pscustomobject]$Record ) { $this.__InitRecord( $Record ) } hidden [void] __ParseString ( [string]$Record, [string]$Origin ) { if ( $Record -match '^(?<HostName>\S+)\s+(?<TimeToLive>\d+)\s+(?<RecordClass>\S+)\s+(?<RecordType>\S+)\s+(?<RecordData>"[^"]*"|[^;]+)\s*;?\s*(?<Comment>.*)$' ) { $this.__InitRecord( [pscustomobject]$Matches ) } elseif ( $Record -match '^;\s*(.*)$' ) { $this.Comment = $Matches[1] } } hidden [void] __InitRecord ( [pscustomobject] $Record ) { $Record | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name | Where-Object { [BindRecord]::IsValidRecordProperty( $_ ) -and -not [string]::IsNullOrEmpty( $Record.$_ ) } | ForEach-Object { if ( $Record.$_ -is [string] ) { $this.$_ = ($Record.$_).Trim() } else { $this.$_ = $Record.$_ } } if ( -not $this.RecordClass ) { $this.RecordClass = 'IN' } } [string] ToString () { return $this.ToString( '{0} {1} {2} {3} {4}' ) } [string] ToString ( [string]$Format ) { return $this.ToString( 0, $Format ) } [string] ToString ( [int]$Index, [string]$Format ) { if ( [BindRecord]::HasErrors( $Index, $this ) ) { Write-Error 'Record Errors Present' -ErrorAction Stop } $FormattedRecord = $Format -f $this.HostName, $this.TimeToLive, $this.RecordClass, $this.RecordType, $this.RecordData if ( [string]::IsNullOrWhiteSpace( $FormattedRecord ) -and -not [string]::IsNullOrEmpty( $this.Comment ) ) { return '; ' + $this.Comment.Trim() } elseif ( -not [string]::IsNullOrEmpty( $this.Comment ) ) { return $FormattedRecord.Trim() + ' ; ' + $this.Comment.Trim() } else { return $FormattedRecord.Trim() } } [bool] Equals ( $that ) { $IsEqual = $true [BindRecord]$that = $that $this | Get-Member -MemberType Property | Select-Object -ExpandProperty Name | Where-Object { $this.$_ -ne $that.$_ } | Select-Object -First 1 | ForEach-Object { $IsEqual = $false } return $IsEqual } [int] CompareTo ( $that ) { [BindRecord]$that = $that # SOA records are always before if ( $this.RecordType -eq 'SOA' -and $that.RecordType -ne 'SOA' ) { return -1 } if ( $this.RecordType -ne 'SOA' -and $that.RecordType -eq 'SOA' ) { return 1 } # now we sort by host name if ( $this.HostName -lt $that.HostName ) { return -1 } if ( $this.HostName -gt $that.HostName ) { return 1 } # now we sort record types based on the ENUM values if ( $this.RecordType -lt $that.RecordType ) { return -1 } if ( $this.RecordType -gt $that.RecordType ) { return 1 } # now we sort on record data if ( $this.RecordData -lt $that.RecordData ) { return -1 } if ( $this.RecordData -gt $that.RecordData ) { return 1 } # finally we sort on TTL if ( $this.TimeToLive -lt $that.TimeToLive ) { return -1 } if ( $this.TimeToLive -gt $that.TimeToLive ) { return 1 } # if everything is equal then the records are equal # we are ignoring comments for the sort return 0 } static [bool] HasErrors ( [int]$Index, [BindRecord]$Record ) { # if all attributes are blank except a comment, then it's a valid comment if ( [string]::IsNullOrWhiteSpace( $Record.HostName ) -and [string]::IsNullOrWhiteSpace( $Record.TimeToLive ) -and [string]::IsNullOrWhiteSpace( $Record.RecordClass ) -and [string]::IsNullOrWhiteSpace( $Record.RecordType ) -and [string]::IsNullOrWhiteSpace( $Record.RecordData ) -and -not [string]::IsNullOrWhiteSpace( $Record.Comment ) ) { return $false } $ReturnValue = $false # if the hostname is not defined then it's invalid if ( [string]::IsNullOrWhiteSpace( $Record.HostName ) ) { Write-Warning "Record ${Index} - HostName is missing" $ReturnValue = $true } # if the TTL is not defined or less than 60 then it's invalid if ( [string]::IsNullOrWhiteSpace( $Record.TimeToLive ) -or [int]$Record.TimeToLive -lt 60 ) { Write-Warning "Record ${Index} - TimeToLive is missing or invalid, value must not be less than 60" $ReturnValue = $true } # if the RecordClass is empty it's invalid if ( [string]::IsNullOrWhiteSpace( $Record.RecordClass ) ) { Write-Warning "Record ${Index} - RecordClass is missing" $ReturnValue = $true # if the RecordClass is NOT 'IN' we throw a warning } elseif ( $Record.RecordClass -ne 'IN' ) { Write-Warning "Record ${Index} - RecordClass is not 'IN', please verify that is intentional" } # if the RecordType is empty it's invalid if ( [string]::IsNullOrWhiteSpace( $Record.RecordType ) ) { Write-Warning "Record ${Index} - RecordType is missing" $ReturnValue = $true # if the RecordType is UNKNOWN it's invalid } elseif ( $Record.RecordType -eq 'UNKNOWN' -or $Record.RecordType -notin [BindRecordType].GetEnumValues() ) { Write-Warning "Record ${Index} - RecordType is invalid" $ReturnValue = $true } # if the RecordData is empty it's invalid if ( [string]::IsNullOrWhiteSpace( $Record.RecordData ) ) { Write-Warning "Record ${Index} - RecordData is missing" $ReturnValue = $true } # if the RecordType is TXT and the RecordData is not enclosed in double quotes # then the value is invalid if ( $Record.RecordType -eq 'TXT' -and $Record.RecordData -notmatch '^".*"$' ) { Write-Warning "Record ${Index} - RecordType is TXT, RecordData needs to be enclosed in double quotes" $ReturnValue = $true } # if RecordData is enclosed in quotes, and contains unescaped quotes the data is invalid if ( $Record.RecordData -match '^".*"$' -and $Record.RecordData -replace '^"(.*)"$', '$1' -match '(?<!\\)"' ) { Write-Warning "Record ${Index} - RecordData is enclosed in quotes and contains unescaped quotes" $ReturnValue = $true } return $ReturnValue } } |