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 : }
|