LCOV - code coverage report
Current view: top level - src/formats - idn_hostname.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 94.1 % 17 16
Test Date: 2026-06-16 03:31:00 Functions: - 0 0
Legend: Lines: hit not hit

            Line data    Source code
       1              : // Copyright 2026 The Authors
       2              : //
       3              : // Licensed under the Apache License, Version 2.0 (the "License");
       4              : // you may not use this file except in compliance with the License.
       5              : // You may obtain a copy of the License at
       6              : //
       7              : //      https://www.apache.org/licenses/LICENSE-2.0
       8              : //
       9              : // Unless required by applicable law or agreed to in writing, software
      10              : // distributed under the License is distributed on an "AS IS" BASIS,
      11              : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12              : // See the License for the specific language governing permissions and
      13              : // limitations under the License.
      14              : 
      15              : /// Best-effort IDN hostname validator (RFC 5890 §2.3.2.3).
      16              : ///
      17              : /// **Important:** This is a pragmatic approximation, not a full IDNA 2008 /
      18              : /// Punycode conformance check. Full IDNA 2008 conformance requires Punycode
      19              : /// encoding and Unicode normalization that are not available in a pure-Dart
      20              : /// context without external dependencies. This validator accepts:
      21              : ///
      22              : /// - Any ASCII hostname valid under RFC 1123 (see `hostname` format).
      23              : /// - Labels that contain Unicode letters or digits (Unicode categories L and N)
      24              : ///   in addition to ASCII alphanumerics and hyphens, with the same structural
      25              : ///   rules (no leading/trailing hyphen, label ≤ 63 chars, total ≤ 253 chars).
      26              : ///
      27              : /// This catches clearly invalid inputs (empty labels, leading/trailing hyphens,
      28              : /// oversized labels, oversized total) without requiring Punycode processing.
      29              : library;
      30              : 
      31              : /// A single ASCII hostname label per RFC 1123.
      32              : ///
      33              : /// A label must:
      34              : /// - Start and end with a letter or digit (ASCII).
      35              : /// - Contain only letters, digits, or hyphens.
      36              : /// - Be between 1 and 63 characters.
      37              : ///
      38              : /// Single-character labels (a letter or digit alone) are also valid.
      39            3 : final RegExp _asciiLabel = RegExp(
      40              :   r'^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?$',
      41              : );
      42              : 
      43              : /// Validates IDN hostnames with a best-effort Unicode label check.
      44              : ///
      45              : /// This is a pragmatic best-effort validator (not full IDNA 2008 / Punycode
      46              : /// conformance). It accepts ASCII hostnames and hostnames whose labels contain
      47              : /// Unicode letters or digits while still enforcing the RFC 1123 structural
      48              : /// rules (label length, total length, no leading/trailing hyphen).
      49              : ///
      50              : /// Example:
      51              : /// ```dart
      52              : /// IdnHostname.isValid('example.com');   // true
      53              : /// IdnHostname.isValid('münchen.de');    // true  (best-effort: unicode label)
      54              : /// IdnHostname.isValid('-bad.com');      // false (leading hyphen)
      55              : /// IdnHostname.isValid('a' * 64 + '.com'); // false (label too long)
      56              : /// ```
      57              : class IdnHostname {
      58            0 :   IdnHostname._();
      59              : 
      60              :   /// Returns `true` if [value] is a valid IDN hostname (best-effort check).
      61              :   ///
      62              :   /// Both ASCII labels and Unicode labels are accepted. The total hostname
      63              :   /// length must be ≤ 253 characters. Each label must be ≤ 63 characters.
      64              :   /// Labels must not start or end with a hyphen.
      65            1 :   static bool isValid(String value) {
      66            1 :     if (value.isEmpty) return false;
      67              : 
      68              :     // Trailing dots are rejected (RFC 1123 host names, not DNS zone-file FQDNs).
      69            1 :     if (value.endsWith('.')) return false;
      70              : 
      71              :     // Total length must not exceed 253 characters.
      72            2 :     if (value.length > 253) return false;
      73              : 
      74            1 :     final labels = value.split('.');
      75              : 
      76            2 :     for (final label in labels) {
      77            1 :       if (label.isEmpty) return false;
      78            2 :       if (label.length > 63) return false;
      79              : 
      80              :       // Try the ASCII label pattern first.
      81            2 :       if (_asciiLabel.hasMatch(label)) continue;
      82              : 
      83              :       // For labels containing non-ASCII characters, apply structural rules:
      84              :       // must not start or end with a hyphen, and every character must be a
      85              :       // Unicode letter (L), Unicode digit (N), ASCII alphanumeric, or hyphen.
      86              :       // NOTE: Dart's RegExp does not support \p{L}/\p{N} Unicode properties
      87              :       // natively. We check by rejecting controls and known-invalid characters
      88              :       // rather than by allow-listing, then enforce the structural hyphen rule.
      89            1 :       if (!_isValidUnicodeLabel(label)) return false;
      90              :     }
      91              : 
      92              :     return true;
      93              :   }
      94              : 
      95              :   /// Validates a single label that may contain Unicode characters.
      96              :   ///
      97              :   /// Rules enforced:
      98              :   /// - Must not start or end with `-`.
      99              :   /// - Must not be empty (enforced by the caller).
     100              :   /// - Must not contain characters that are ASCII control characters, spaces,
     101              :   ///   or the explicitly disallowed set (`@`, `[`, `]`, etc.).
     102              :   /// - Must contain at least one non-hyphen character.
     103              :   ///
     104              :   /// This is a best-effort check. Full IDNA 2008 validation would require
     105              :   /// Punycode encoding and Unicode normalization (NFKC).
     106            1 :   static bool _isValidUnicodeLabel(String label) {
     107              :     // Must not start or end with a hyphen.
     108            2 :     if (label.startsWith('-') || label.endsWith('-')) return false;
     109              : 
     110              :     // Pattern of characters that are explicitly disallowed in any label
     111              :     // character position, regardless of Unicode category.
     112              :     // This covers ASCII control characters, space, and label-separating /
     113              :     // URI-reserved punctuation.
     114            1 :     final disallowed = RegExp(
     115              :       r'[\x00-\x1F\x7F !@#$%^&*()=+\[\]{};:",<>/?\\|`~]',
     116              :     );
     117            1 :     if (disallowed.hasMatch(label)) return false;
     118              : 
     119              :     // The label must contain at least one non-hyphen character.
     120            2 :     if (label.replaceAll('-', '').isEmpty) return false;
     121              : 
     122              :     return true;
     123              :   }
     124              : }
        

Generated by: LCOV version 2.0-1