@@ -22,6 +22,7 @@ You should have received a copy of the GNU General Public License
2222using System . IO ;
2323using System . Text ;
2424using System . Text . Json ;
25+ using System . Text . RegularExpressions ;
2526using System . Threading . Tasks ;
2627using TechnitiumLibrary . IO ;
2728
@@ -46,6 +47,75 @@ public class DnsNAPTRRecordData : DnsResourceRecordData
4647
4748 public DnsNAPTRRecordData ( ushort order , ushort preference , string flags , string services , string regexp , string replacement )
4849 {
50+ ArgumentNullException . ThrowIfNull ( flags ) ;
51+
52+ ArgumentNullException . ThrowIfNull ( services ) ;
53+
54+ ArgumentNullException . ThrowIfNull ( regexp ) ;
55+
56+ ArgumentNullException . ThrowIfNull ( replacement ) ;
57+
58+ // RFC 3403: REGEXP and REPLACEMENT are mutually exclusive
59+ // If both are present (non-empty), the record is in error and MUST be rejected or ignored.
60+ if ( regexp . Length > 0 && replacement . Length > 0 )
61+ throw new ArgumentException (
62+ "REGEXP and REPLACEMENT are mutually exclusive per RFC 3403." ) ;
63+
64+ // RFC 3403: FLAGS validation
65+ // Flags are single characters from A–Z and 0–9, case-insensitive.
66+ for ( int i = 0 ; i < flags . Length ; i ++ )
67+ {
68+ char c = flags [ i ] ;
69+ if ( ! ( char . IsAsciiLetter ( c ) || char . IsDigit ( c ) ) )
70+ throw new ArgumentException (
71+ $ "Invalid NAPTR flag '{ c } '. Allowed set is A–Z and 0–9.",
72+ nameof ( flags ) ) ;
73+ }
74+
75+ // RFC 3403: SERVICES is a DNS <character-string>
76+ // RFC intentionally does NOT define semantics here; only basic sanity checks.
77+ // Enforce non-control UTF-16 chars; deeper validation is application-specific.
78+ for ( int i = 0 ; i < services . Length ; i ++ )
79+ {
80+ if ( char . IsControl ( services [ i ] ) )
81+ throw new ArgumentException (
82+ "SERVICES contains control characters, which are not permitted." ,
83+ nameof ( services ) ) ;
84+ }
85+
86+ // RFC 3403: REPLACEMENT must be a fully qualified domain name
87+ if ( replacement . Length > 0 )
88+ {
89+ // Must end with a root label (trailing dot)
90+ if ( ! replacement . EndsWith ( "." , StringComparison . Ordinal ) )
91+ throw new ArgumentException (
92+ "REPLACEMENT must be a fully qualified domain name ending with a dot." ,
93+ nameof ( replacement ) ) ;
94+
95+ // No name compression, no empty labels except the root
96+ if ( replacement . Contains ( ".." , StringComparison . Ordinal ) )
97+ throw new ArgumentException (
98+ "REPLACEMENT contains empty DNS labels." ,
99+ nameof ( replacement ) ) ;
100+ }
101+
102+ // RFC 3403: REGEXP sanity
103+ // The RFC requires POSIX ERE semantics but does not mandate compile-time validation.
104+ // Enforce only that it is non-empty when used.
105+ if ( regexp . Length == 0 && replacement . Length == 0 )
106+ throw new ArgumentException (
107+ "Either REGEXP or REPLACEMENT must be specified per RFC 3403." ) ;
108+
109+ // OPTIONAL: Validate regex, not defined in RFC.
110+ // It is a NICE-TO-HAVE, not a MUST.
111+ if ( ! IsValidRegex ( regexp ) )
112+ {
113+ throw new ArgumentException (
114+ "REGEXP is not a valid regular expression." ,
115+ nameof ( regexp ) ) ;
116+ }
117+
118+ // DNS <character-string> constraints
49119 if ( DnsClient . IsDomainNameUnicode ( replacement ) )
50120 replacement = DnsClient . ConvertDomainNameToAscii ( replacement ) ;
51121
@@ -84,6 +154,23 @@ private void Serialize()
84154 }
85155 }
86156
157+ private bool IsValidRegex ( string pattern )
158+ {
159+ if ( pattern is null ) return false ;
160+ try
161+ {
162+ _ = new Regex (
163+ pattern ,
164+ RegexOptions . None ,
165+ TimeSpan . FromMilliseconds ( 100 ) ) ;
166+ return true ;
167+ }
168+ catch ( ArgumentException )
169+ {
170+ return false ;
171+ }
172+ }
173+
87174 #endregion
88175
89176 #region protected
0 commit comments