LCOV - code coverage report
Current view: top level - src/formats - urn.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 55.4 % 56 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. See the AUTHORS file for details.
       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              : import 'package:collection/collection.dart';
      16              : 
      17              : /// A URN is a URI that uses the "urn:" scheme.
      18              : ///
      19              : /// This format supports
      20              : /// [RFC 8141: Uniform Resource Names (URNs)](https://www.rfc-editor.org/info/rfc8141).
      21              : ///
      22              : /// Unlike a URL, resolution of a URN generally requires another service.
      23              : class Urn {
      24              :   static const schemeName = 'urn';
      25              : 
      26              :   /// The namespace identifier.
      27              :   final String nid;
      28              : 
      29              :   /// The namespace specific string.
      30              :   final String nss;
      31              : 
      32              :   /// The fragment (f-component).
      33              :   final String fragment;
      34              : 
      35              :   /// Components to be used by resolution services.
      36              :   ///
      37              :   /// See: RFC 8141 2.3.1  r-component
      38              :   /// The rComponent isn't parsed further
      39              :   final String rComponent;
      40              : 
      41              :   /// Parameters to be used by the named resource.
      42              :   ///
      43              :   /// See: RFC 8141 2.3.2  q-component
      44              :   final Map<String, String> _qComponents;
      45              : 
      46              :   /// Constructor.
      47              :   ///
      48              :   /// As per RFC 8141 Section 3.1, the NSS and NID
      49              :   /// are converted to lower-case.
      50            2 :   Urn({
      51              :     required String nid,
      52              :     required String nss,
      53              :     this.fragment = '',
      54              :     this.rComponent = '',
      55              :     Map<String, String>? qComponents,
      56            2 :   }) : nid = nid.toLowerCase(),
      57            2 :        nss = nss.toLowerCase(),
      58            3 :        _qComponents = Map.from(qComponents ?? {});
      59              : 
      60            1 :   Map<String, String> get qComponentParameters =>
      61            2 :       UnmodifiableMapView(_qComponents);
      62              : 
      63            1 :   @override
      64            1 :   String toString() => [
      65            3 :     'urn:$nid:$nss',
      66            4 :     if (rComponent.isNotEmpty) '?+$rComponent',
      67            2 :     if (_qComponents.isNotEmpty)
      68            0 :       '?=${_qComponents.keys.map((k) => '$k=$_qComponents[k]').join("&")}',
      69            4 :     if (fragment.isNotEmpty) '#$fragment',
      70            1 :   ].join();
      71              : 
      72            0 :   Uri toUri() => Uri(
      73              :     scheme: schemeName,
      74            0 :     path: '$nid:$nss',
      75            0 :     query: [
      76            0 :       if (rComponent.isNotEmpty) '?+$rComponent',
      77            0 :       if (_qComponents.isNotEmpty)
      78            0 :         '?=${_qComponents.keys.map((k) => '$k=$_qComponents[k]').join("&")}',
      79            0 :     ].join(),
      80            0 :     fragment: fragment,
      81              :   );
      82              : 
      83              :   /// Equality is defined in RFC 8141 Section 3: URN equivalence.
      84              :   ///
      85              :   /// Note that:
      86              :   ///
      87              :   /// "If an r-component, q-component, or f-component (or any combination
      88              :   /// thereof) is included in a URN, it MUST be ignored for purposes of
      89              :   /// determining URN-equivalence."
      90            1 :   @override
      91              :   bool operator ==(Object other) =>
      92            7 :       other is Urn && other.nid == nid && other.nss == nss;
      93              : 
      94            0 :   @override
      95            0 :   int get hashCode => Object.hashAll([nid, nss]);
      96              : 
      97            0 :   static Urn? tryParseUri(Uri uri) {
      98            0 :     if (uri.scheme != schemeName) {
      99            0 :       throw ArgumentError.value(uri, 'uri', 'Not a URN');
     100              :     }
     101              : 
     102            0 :     var match = RegExp('^(?<nid>$_nid):(?<nss>$_nss)\$').firstMatch(uri.path);
     103              : 
     104              :     if (match == null) {
     105              :       return null;
     106              :     }
     107              : 
     108            0 :     var nid = match.namedGroup('nid');
     109            0 :     var nss = match.namedGroup('nss');
     110              : 
     111              :     if (nid == null || nss == null) {
     112              :       return null;
     113              :     }
     114              : 
     115            0 :     var query = '?${uri.query}';
     116              : 
     117            0 :     var rqComponentsMatch = RegExp(
     118              :       '(\\?\\+$_rComponent)?'
     119              :       '(\\?\\=$_qComponent)?',
     120            0 :     ).firstMatch(query);
     121              : 
     122            0 :     Map<String, String> qMap = {};
     123              :     String rcomponent = '';
     124              : 
     125              :     if (rqComponentsMatch != null) {
     126            0 :       rcomponent = rqComponentsMatch.namedGroup('rcomponent') ?? '';
     127            0 :       qMap = _parseQComponent(rqComponentsMatch.namedGroup('qcomponent') ?? '');
     128              :     }
     129              : 
     130            0 :     return Urn(
     131              :       nid: nid,
     132              :       nss: nss,
     133              :       qComponents: qMap,
     134              :       rComponent: rcomponent,
     135            0 :       fragment: uri.fragment,
     136              :     );
     137              :   }
     138              : 
     139              :   // unreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~"
     140              :   static const _unreserved = r'[a-zA-Z0-9\-._~]';
     141              : 
     142              :   // HEXDIG    =  DIGIT / "A" / "B" / "C" / "D" / "E" / "F" ; RFC 5234
     143              :   static const _hexDig = r'[0-9A-Fa-f]';
     144              : 
     145              :   // pct-encoded   = "%" HEXDIG HEXDIG
     146              :   static const _pctEncoded = '(%$_hexDig{2})';
     147              : 
     148              :   // sub-delims   = "!" / "$" / "&" / "'" / "(" / ")"
     149              :   //                 / "*" / "+" / "," / ";" / "="
     150              :   static const _subDelims = r"[!$&'()*+,;=]";
     151              : 
     152              :   // pchar    = unreserved / pct-encoded / sub-delims / ":" / "@"
     153              :   static const _pchar = '$_unreserved|$_pctEncoded|$_subDelims|[:@]';
     154              : 
     155              :   // NID = (alphanum) 0*30(ldh) (alphanum)
     156              :   // ldh = alphanum / "-"
     157              :   static const _nid = r'[a-zA-Z0-9][a-zA-Z0-9-]{0,30}[a-zA-Z0-9]';
     158              : 
     159              :   // NSS = pchar *(pchar / "/")
     160              :   static const _nss = '($_pchar)($_pchar|[/])*';
     161              : 
     162              :   // fragment      = *( pchar / "/" / "?" )
     163              :   static const _fragment = '(?<fragment>($_pchar|[/?])*)';
     164              : 
     165              :   // rq-components = [ "?+" r-component ]
     166              :   //                 [ "?=" q-component ]
     167              :   static const _rComponent = '(?<rcomponent>($_pchar)($_pchar|[/]|\\?(?!=))*)';
     168              :   static const _qComponent = '(?<qcomponent>($_pchar)($_pchar|[/?])*)';
     169              : 
     170              :   // assigned-name = "urn" ":" NID ":" NSS
     171              :   static const _assignedName = '(urn|URN):(?<nid>$_nid):(?<nss>$_nss)';
     172              : 
     173              :   static const _namestring =
     174              :       '$_assignedName'
     175              :       '(\\?\\+$_rComponent)?'
     176              :       '(\\?\\=$_qComponent)?'
     177              :       '(#$_fragment)?';
     178              : 
     179            6 :   static final _namestringRegEx = RegExp('^$_namestring\$');
     180              : 
     181              :   /// Parses the [input] URN and returns a [Urn] object.
     182            2 :   static Urn? tryParse(String input) {
     183            4 :     var match = _namestringRegEx.firstMatch(input);
     184              : 
     185              :     if (match == null) {
     186              :       return null;
     187              :     }
     188              : 
     189            2 :     var nid = match.namedGroup('nid');
     190            2 :     var nss = match.namedGroup('nss');
     191              : 
     192            4 :     var qMap = _parseQComponent(match.namedGroup('qcomponent') ?? '');
     193              : 
     194              :     if (nid == null || nss == null) {
     195              :       return null;
     196              :     }
     197              : 
     198            2 :     return Urn(
     199              :       nid: nid,
     200              :       nss: nss,
     201            2 :       rComponent: match.namedGroup('rcomponent') ?? '',
     202              :       qComponents: qMap,
     203            2 :       fragment: match.namedGroup('fragment') ?? '',
     204              :     );
     205              :   }
     206              : 
     207            2 :   static Map<String, String> _parseQComponent(String qcomponents) {
     208            2 :     var qMap = <String, String>{};
     209            2 :     if (qcomponents.isNotEmpty) {
     210            1 :       var qItems = qcomponents.split('&');
     211              : 
     212            2 :       for (var item in qItems) {
     213            1 :         var pair = item.split('=');
     214            3 :         qMap[pair[0]] = pair[1];
     215              :       }
     216              :     }
     217              :     return qMap;
     218              :   }
     219              : }
        

Generated by: LCOV version 2.0-1