LCOV - code coverage report
Current view: top level - src/formats - ipv6.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 97.8 % 45 44
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              : /// IPv6 address format validator (RFC 4291 §2.2).
      16              : ///
      17              : /// Validates IPv6 address strings without using `dart:io`, which is unavailable
      18              : /// in some Dart environments (e.g. the browser). The implementation uses a
      19              : /// group-counting approach rather than a single monolithic regex, making the
      20              : /// logic more readable and the edge-case handling explicit.
      21              : ///
      22              : /// Supported forms:
      23              : /// - Full eight-group form: `2001:db8:85a3:0:0:8a2e:370:7334`
      24              : /// - All compressed (`::`) positions: `::1`, `1::`, `1::2`, `::`
      25              : /// - IPv4-mapped tail: `::ffff:192.168.1.1`
      26              : ///
      27              : /// This is a best-effort structural check, not a full RFC 4291 parser.
      28              : library;
      29              : 
      30              : /// Pattern matching a single hex group (1–4 hex digits), case-insensitive.
      31            3 : final RegExp _hexGroup = RegExp(r'^[0-9a-fA-F]{1,4}$');
      32              : 
      33              : /// Pattern matching a valid IPv4 dotted-quad with no leading zeros in any
      34              : /// octet, per the per-octet alternation that rejects values > 255.
      35            3 : final RegExp _ipv4Dotted = RegExp(
      36              :   r'^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)'
      37              :   r'\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)'
      38              :   r'\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)'
      39              :   r'\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)$',
      40              : );
      41              : 
      42              : /// Validates IPv6 address strings (RFC 4291 §2.2).
      43              : ///
      44              : /// Both compressed and uncompressed forms are accepted. Group matching is
      45              : /// case-insensitive (e.g. `2001:DB8::1` is valid). The empty string and
      46              : /// strings with trailing/leading whitespace are rejected.
      47              : ///
      48              : /// Example:
      49              : /// ```dart
      50              : /// Ipv6.isValid('::1');             // true  (loopback)
      51              : /// Ipv6.isValid('::');              // true  (all-zeros)
      52              : /// Ipv6.isValid('::ffff:1.2.3.4'); // true  (IPv4-mapped)
      53              : /// Ipv6.isValid('gggg::1');         // false (invalid hex digit)
      54              : /// Ipv6.isValid('1::2::3');         // false (two :: groups)
      55              : /// ```
      56              : class Ipv6 {
      57            0 :   Ipv6._();
      58              : 
      59              :   /// Returns `true` if [value] is a syntactically valid IPv6 address string.
      60            1 :   static bool isValid(String value) {
      61            1 :     if (value.isEmpty) return false;
      62              : 
      63              :     // At most one '::' is allowed in any IPv6 address.
      64            2 :     final dcCount = '::'.allMatches(value).length;
      65            1 :     if (dcCount > 1) return false;
      66              : 
      67            1 :     final hasDoubleColon = dcCount == 1;
      68              : 
      69              :     // Detect an IPv4-mapped tail (e.g. ::ffff:1.2.3.4).
      70              :     // The IPv4 portion appears after the last ':' when the string contains a
      71              :     // dotted-quad pattern.
      72            1 :     final lastColon = value.lastIndexOf(':');
      73            1 :     if (lastColon >= 0) {
      74            2 :       final tail = value.substring(lastColon + 1);
      75            1 :       if (tail.contains('.')) {
      76              :         // The tail looks like an IPv4 address — validate as IPv4-mapped.
      77            1 :         return _validateIpv4Mapped(value, tail, hasDoubleColon);
      78              :       }
      79              :     }
      80              : 
      81              :     // Pure hex form (no IPv4 tail).
      82              :     if (hasDoubleColon) {
      83            1 :       return _validateCompressed(value);
      84              :     } else {
      85            1 :       return _validateFull(value);
      86              :     }
      87              :   }
      88              : 
      89              :   /// Validates a full (uncompressed) eight-group IPv6 address.
      90              :   ///
      91              :   /// Expects exactly eight colon-separated hex groups.
      92            1 :   static bool _validateFull(String value) {
      93            1 :     final groups = value.split(':');
      94            2 :     if (groups.length != 8) return false;
      95            3 :     return groups.every(_hexGroup.hasMatch);
      96              :   }
      97              : 
      98              :   /// Validates a compressed IPv6 address that contains exactly one `::`.
      99              :   ///
     100              :   /// The `::` expands to fill the remaining groups so the total reaches eight.
     101              :   /// Each side of `::` may have zero to six explicit hex groups.
     102            1 :   static bool _validateCompressed(String value) {
     103            1 :     final sides = value.split('::');
     104              :     // split('::') on a valid compressed address always produces exactly 2
     105              :     // parts (even if one or both are empty strings).
     106            2 :     if (sides.length != 2) return false;
     107              : 
     108            5 :     final left = sides[0].isEmpty ? <String>[] : sides[0].split(':');
     109            5 :     final right = sides[1].isEmpty ? <String>[] : sides[1].split(':');
     110              : 
     111              :     // The total explicit groups must leave at least one slot for :: to expand,
     112              :     // so at most 6 groups across both sides (8 total − 2 from ::).
     113              :     // Exception: '::' alone has 0 explicit groups, which is fine (all zeros).
     114            4 :     if (left.length + right.length > 6) return false;
     115              : 
     116            5 :     return [...left, ...right].every(_hexGroup.hasMatch);
     117              :   }
     118              : 
     119              :   /// Validates an IPv6 address that has an IPv4 dotted-quad tail.
     120              :   ///
     121              :   /// Legal forms under RFC 4291 §2.2 rule 3:
     122              :   /// ```
     123              :   /// x:x:x:x:x:x:d.d.d.d
     124              :   /// ```
     125              :   /// where there are exactly six hex groups before the IPv4 part, or a
     126              :   /// `::` compressed form where the hex groups plus the IPv4 tail account for
     127              :   /// the full 128 bits (IPv4 counts as two 16-bit groups).
     128              :   ///
     129              :   /// [tail] is the portion after the last `:` (the IPv4 address string).
     130            1 :   static bool _validateIpv4Mapped(
     131              :     String value,
     132              :     String tail,
     133              :     bool hasDoubleColon,
     134              :   ) {
     135              :     // The IPv4 dotted-quad must itself be valid.
     136            2 :     if (!_ipv4Dotted.hasMatch(tail)) return false;
     137              : 
     138              :     // Strip the IPv4 tail (plus the preceding colon) to get the hex prefix.
     139            3 :     final prefixWithColon = value.substring(0, value.lastIndexOf(':') + 1);
     140              :     // prefixWithColon now ends in ':', e.g. "::ffff:" or "::".
     141              : 
     142              :     if (hasDoubleColon) {
     143              :       // Split on '::'.
     144            1 :       final dcIndex = prefixWithColon.indexOf('::');
     145            1 :       final leftStr = prefixWithColon.substring(0, dcIndex);
     146              :       // The portion after '::' ends in ':', strip the trailing colon.
     147            2 :       final rightStr = prefixWithColon.substring(dcIndex + 2);
     148            1 :       final rightClean = rightStr.endsWith(':')
     149            3 :           ? rightStr.substring(0, rightStr.length - 1)
     150              :           : rightStr;
     151              : 
     152            3 :       final left = leftStr.isEmpty ? <String>[] : leftStr.split(':');
     153            3 :       final right = rightClean.isEmpty ? <String>[] : rightClean.split(':');
     154              : 
     155              :       // The IPv4 tail counts as 2 hex groups, so at most 4 explicit hex
     156              :       // groups are allowed across both sides of '::'.
     157            4 :       if (left.length + right.length > 4) return false;
     158              : 
     159            1 :       return [
     160              :         ...left,
     161            1 :         ...right,
     162            5 :       ].every((g) => g.isEmpty || _hexGroup.hasMatch(g));
     163              :     } else {
     164              :       // No '::'. The prefix must be exactly 6 hex groups separated by ':'.
     165              :       // prefixWithColon ends with ':', so split gives a trailing empty string.
     166              :       final groups = prefixWithColon
     167            1 :           .split(':')
     168            3 :           .where((s) => s.isNotEmpty)
     169            1 :           .toList();
     170            2 :       if (groups.length != 6) return false;
     171            3 :       return groups.every(_hexGroup.hasMatch);
     172              :     }
     173              :   }
     174              : }
        

Generated by: LCOV version 2.0-1