LCOV - code coverage report
Current view: top level - src/formats - formats_base.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 91.2 % 34 31
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              : import 'dart:collection' show UnmodifiableMapView;
      15              : 
      16              : import 'package:intl/intl.dart';
      17              : import 'package:uuid/validation.dart';
      18              : 
      19              : import 'digit_string.dart';
      20              : import 'doi.dart';
      21              : import 'duration.dart';
      22              : import 'email.dart';
      23              : import 'hex.dart';
      24              : import 'idn_hostname.dart';
      25              : import 'isbn.dart';
      26              : import 'ipv6.dart';
      27              : import 'lang.dart';
      28              : import 'roman.dart';
      29              : import 'urn.dart';
      30              : 
      31              : export 'digit_string.dart';
      32              : export 'doi.dart';
      33              : export 'duration.dart';
      34              : export 'email.dart';
      35              : export 'hex.dart';
      36              : export 'idn_hostname.dart';
      37              : export 'isbn.dart';
      38              : export 'ipv6.dart';
      39              : export 'lang.dart';
      40              : export 'roman.dart';
      41              : export 'urn.dart';
      42              : 
      43              : /// An immutable descriptor for a single named string-format validator.
      44              : ///
      45              : /// [name] identifies the format (e.g. `'email'`, `'uri'`, `'uuid'`).
      46              : /// [description] is a human-readable explanation of the format.
      47              : /// [function] returns `true` if a string value satisfies the format.
      48              : class StringValidator {
      49              :   final String name;
      50              :   final String description;
      51              :   final bool Function(String) function;
      52              : 
      53           10 :   StringValidator(this.name, this.description, this.function);
      54              : }
      55              : 
      56              : /// Provides a registry of named string-format validators.
      57              : ///
      58              : /// Implementations must ensure [supportedValidators] returns an unmodifiable
      59              : /// view — callers may enumerate it but must not mutate it.
      60              : abstract class StringValidatorService {
      61              :   /// All validators indexed by format name.
      62              :   ///
      63              :   /// The returned map is unmodifiable; calling any mutating method throws
      64              :   /// [UnsupportedError].
      65              :   UnmodifiableMapView<String, StringValidator> get supportedValidators;
      66              : 
      67              :   /// Returns the [StringValidator] for [name], or `null` if unrecognised.
      68              :   StringValidator? getValidator(String name);
      69              : }
      70              : 
      71              : /// The built-in registry of JSON Schema and project-specific format validators.
      72              : class StringFormatValidator implements StringValidatorService {
      73            2 :   @override
      74              :   UnmodifiableMapView<String, StringValidator> get supportedValidators =>
      75            4 :       UnmodifiableMapView(_supportedValidators);
      76              : 
      77           10 :   @override
      78           20 :   StringValidator? getValidator(String name) => _supportedValidators[name];
      79              : 
      80              :   final _supportedValidators = {
      81              :     'uri': StringValidator(
      82              :       'uri',
      83              :       'A string instance is valid against this attribute if it is a valid URI,'
      84              :           ' according to RFC 3986. Note: a valid URN (urn: scheme) is also a'
      85              :           ' valid URI, so this validator accepts both http URLs and URNs.',
      86              :       // Uri.tryParse is intentionally lenient: it accepts all registered URI
      87              :       // schemes, including urn:. A URN is a valid URI per RFC 3986.
      88            4 :       (value) => Uri.tryParse(value) != null ? true : false,
      89              :     ),
      90              :     'urn': StringValidator(
      91              :       'urn',
      92              :       'A string instance is valid against this attribute if it is a valid URN,'
      93              :           ' according to RFC 8141',
      94              :       // Urn.tryParse strictly validates URN syntax (urn:<nid>:<nss>).
      95              :       // Plain http/https URLs are not URNs and are rejected.
      96            4 :       (value) => Urn.tryParse(value) != null ? true : false,
      97              :     ),
      98              :     'duration': StringValidator(
      99              :       'duration',
     100              :       'A string instance is valid against this attribute if it is a valid '
     101              :           'representation according to RFC3339',
     102              :       Iso8601Duration.isValid,
     103              :     ),
     104              :     'email': StringValidator(
     105              :       'email',
     106              :       'A string instance is valid against this attribute if it is a valid '
     107              :           'representation of a pragmatic subset of RFC 5322',
     108              :       Email.isValid,
     109              :     ),
     110              :     'lang': StringValidator(
     111              :       'lang',
     112              :       'A string instance is valid against this attribute if it is a valid '
     113              :           'language tag as defined in RFC 5646',
     114              :       LanguageTag.isValid,
     115              :     ),
     116              :     'date-time': StringValidator(
     117              :       'date-time',
     118              :       'A string instance is valid against this attribute if it is a valid '
     119              :           'representation according to the "date-time" ABNF rule in ISO8601',
     120            2 :       (value) => DateTime.tryParse(value) != null ? true : false,
     121              :     ),
     122              :     'date': StringValidator(
     123              :       'date',
     124              :       'A string instance is valid against this attribute if it is a valid '
     125              :           'representation according to the "date" ABNF rule in RFC3339',
     126              :       isValidDate,
     127              :     ),
     128              :     'time': StringValidator(
     129              :       'time',
     130              :       'A string instance is valid against this attribute if it is a valid '
     131              :           'representation according to the "time" ABNF rule in RFC3339',
     132            3 :       (value) => DateFormat('HH:mm:ss.SSS').tryParseStrict(value) != null,
     133              :     ),
     134              :     'uuid': StringValidator(
     135              :       'uuid',
     136              :       'A string instance is valid against this attribute if it is a valid '
     137              :           'string representation of a UUID, according to RFC9562',
     138            2 :       (value) => UuidValidation.isValidUUID(fromString: value, noDashes: false),
     139              :     ),
     140              :     'regex': StringValidator(
     141              :       'regex',
     142              :       'A regular expression, which SHOULD be valid '
     143              :           'according to the ECMA-262 [ecma262] regular expression dialect.',
     144              :       isValidRegex,
     145              :     ),
     146              :     'hex-string': StringValidator(
     147              :       'hex-string',
     148              :       'The string represents a valid '
     149              :           'series of characters for a hexadecimal number',
     150              :       HexString.isValid,
     151              :     ),
     152              :     'digit-string': StringValidator(
     153              :       'digit-string',
     154              :       'The string represents a valid '
     155              :           'series of characters for a decimal number',
     156              :       DigitString.isValid,
     157              :     ),
     158              :     'roman-numeral': StringValidator(
     159              :       'roman-numeral',
     160              :       'The string represents a valid '
     161              :           'series of characters for a Roman numeral',
     162              :       RomanNumerals.isValid,
     163              :     ),
     164              :     'isbn-13': StringValidator(
     165              :       'isbn-13',
     166              :       'The string represents a valid'
     167              :           ' International Standard Book Number (ISBN) if it meets the '
     168              :           'required check digit calculation',
     169              :       Isbn13.isValid,
     170              :     ),
     171              :     'doi': StringValidator(
     172              :       'doi',
     173              :       'The string represents a valid DOI',
     174              :       DOI.isValid,
     175              :     ),
     176              :     'ipv4': StringValidator(
     177              :       'ipv4',
     178              :       'A string instance is valid against this attribute if it is a valid '
     179              :           'IPv4 address according to RFC 2673 §3.2: four decimal octets '
     180              :           '0–255 separated by dots. Leading zeros in any octet are rejected '
     181              :           'to avoid ambiguous octal interpretation.',
     182              :       // Per-octet alternation rejects leading zeros and values > 255 in a
     183              :       // single pass without post-processing.
     184            2 :       (value) => RegExp(
     185              :         r'^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)'
     186              :         r'\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)'
     187              :         r'\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)'
     188              :         r'\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)$',
     189            1 :       ).hasMatch(value),
     190              :     ),
     191              :     'ipv6': StringValidator(
     192              :       'ipv6',
     193              :       'A string instance is valid against this attribute if it is a valid '
     194              :           'IPv6 address according to RFC 4291 §2.2. Both full eight-group '
     195              :           'and compressed (::) forms are accepted, including IPv4-mapped '
     196              :           'addresses. Validation uses a pure-Dart approach (no dart:io) '
     197              :           'so that the validator works in browser environments.',
     198              :       Ipv6.isValid,
     199              :     ),
     200              :     'hostname': StringValidator(
     201              :       'hostname',
     202              :       'A string instance is valid against this attribute if it is a valid '
     203              :           'Internet hostname per RFC 1123 §2.1. Labels are composed of '
     204              :           'ASCII letters, digits, and hyphens; must not start or end with '
     205              :           'a hyphen; must be 1–63 characters each; and the total length '
     206              :           'must not exceed 253 characters. Trailing dots are rejected. '
     207              :           'Matching is case-insensitive.',
     208              :       // Labels: start/end with alphanum, interior may include hyphens.
     209              :       // Single-char labels (one alphanumeric) are valid.
     210            1 :       (value) {
     211            4 :         if (value.isEmpty || value.endsWith('.') || value.length > 253) {
     212              :           return false;
     213              :         }
     214            1 :         final labelPattern = RegExp(
     215              :           r'^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?$',
     216              :         );
     217              :         return value
     218            1 :             .split('.')
     219            4 :             .every((label) => label.isNotEmpty && labelPattern.hasMatch(label));
     220              :       },
     221              :     ),
     222              :     'idn-hostname': StringValidator(
     223              :       'idn-hostname',
     224              :       'A string instance is valid against this attribute if it is a valid '
     225              :           'internationalized hostname per RFC 5890. This is a best-effort '
     226              :           'check: ASCII hostnames are validated per RFC 1123; labels '
     227              :           'containing Unicode characters are accepted if they satisfy the '
     228              :           'structural rules (no leading/trailing hyphen, label ≤ 63 chars, '
     229              :           'total ≤ 253 chars). Full IDNA 2008 / Punycode conformance is not '
     230              :           'enforced; that requires Punycode processing unavailable in '
     231              :           'pure Dart without external dependencies.',
     232              :       IdnHostname.isValid,
     233              :     ),
     234              :     'uri-reference': StringValidator(
     235              :       'uri-reference',
     236              :       'A string instance is valid against this attribute if it is a valid '
     237              :           'URI reference per RFC 3986 §4.1 — either an absolute URI or a '
     238              :           'relative reference. The check uses a structural approach: '
     239              :           'Uri.tryParse must succeed AND the string must not contain '
     240              :           'characters that are illegal in both forms (unescaped spaces, '
     241              :           'ASCII control characters, or unencoded angle brackets).',
     242            1 :       (value) {
     243              :         // Reject strings containing unescaped spaces, ASCII control
     244              :         // characters (0x00–0x1F, 0x7F), or literal angle brackets.
     245              :         // These are illegal in both absolute URIs and relative references
     246              :         // per RFC 3986, regardless of how permissive Uri.tryParse is.
     247            2 :         if (RegExp(r'[\x00-\x1F\x7F <>\[\]\\^`{|}]').hasMatch(value)) {
     248              :           return false;
     249              :         }
     250            1 :         return Uri.tryParse(value) != null;
     251              :       },
     252              :     ),
     253              :     'json-pointer': StringValidator(
     254              :       'json-pointer',
     255              :       'A string instance is valid against this attribute if it is a valid '
     256              :           'JSON Pointer per RFC 6901. A JSON Pointer is either an empty '
     257              :           'string (pointing to the root document) or a sequence of '
     258              :           'reference tokens each prefixed by /. Within a token, ~ must '
     259              :           'only appear as ~0 (representing ~) or ~1 (representing /).',
     260              :       // Regex: empty string OR one-or-more /token sequences where each
     261              :       // token contains any character except ~ (which must be ~0 or ~1).
     262            3 :       (value) => RegExp(r'^(/([^~]|~[01])*)*$').hasMatch(value),
     263              :     ),
     264              :     'relative-json-pointer': StringValidator(
     265              :       'relative-json-pointer',
     266              :       'A string instance is valid against this attribute if it is a valid '
     267              :           'Relative JSON Pointer per the IETF draft (bhutton). A Relative '
     268              :           'JSON Pointer begins with a non-negative integer (no leading zeros '
     269              :           'unless the value is 0) followed by either # (referring to the '
     270              :           'key/index of the referenced location) or a JSON Pointer '
     271              :           '(including the empty string).',
     272              :       // Regex breakdown:
     273              :       //   (0|[1-9][0-9]*)   — non-negative integer, no leading zeros
     274              :       //   (#|(/([^~]|~[01])*)*)  — either '#' or a JSON Pointer
     275            1 :       (value) =>
     276            2 :           RegExp(r'^(0|[1-9][0-9]*)(#|(/([^~]|~[01])*)*)$').hasMatch(value),
     277              :     ),
     278              :   };
     279              : }
     280              : 
     281            0 : bool isValidRegex(String value) {
     282              :   try {
     283            0 :     RegExp(value);
     284            0 :   } on FormatException {
     285              :     return false;
     286              :   }
     287              :   return true;
     288              : }
     289              : 
     290            1 : bool isValidDate(String value) {
     291              :   // Reject anything that isn't strictly YYYY-MM-DD.
     292            2 :   if (!RegExp(r'^\d{4}-\d{2}-\d{2}$').hasMatch(value)) return false;
     293            1 :   final d = DateTime.tryParse(value);
     294              :   if (d == null) return false;
     295              :   // Reject overflow dates (e.g. month 13) by re-formatting and comparing.
     296            1 :   final reformatted =
     297            3 :       '${d.year.toString().padLeft(4, '0')}-'
     298            3 :       '${d.month.toString().padLeft(2, '0')}-'
     299            3 :       '${d.day.toString().padLeft(2, '0')}';
     300            1 :   return reformatted == value;
     301              : }
        

Generated by: LCOV version 2.0-1