Branch data Line data Source code
1 : : /*
2 : : * Copyright (C) 2026 by Thun Lu. All rights reserved.
3 : : * Author: Thun Lu <thun.lu@zohomail.cn>
4 : : * Repo: https://github.com/thun-res/vlink
5 : : * _ __ __ _ __
6 : : * | | / / / / (_) ____ / /__
7 : : * | | / / / / / / / __ \ / //_/
8 : : * | |/ / / /___ / / / / / / / ,<
9 : : * |___/ /_____/ /_/ /_/ /_/ /_/|_|
10 : : *
11 : : * Licensed under the Apache License, Version 2.0 (the "License");
12 : : * you may not use this file except in compliance with the License.
13 : : * You may obtain a copy of the License at
14 : : *
15 : : * http://www.apache.org/licenses/LICENSE-2.0
16 : : *
17 : : * Unless required by applicable law or agreed to in writing, software
18 : : * distributed under the License is distributed on an "AS IS" BASIS,
19 : : * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 : : * See the License for the specific language governing permissions and
21 : : * limitations under the License.
22 : : */
23 : :
24 : : #include "./impl/url_parser.h"
25 : :
26 : : #include <exception>
27 : : #include <map>
28 : : #include <string>
29 : : #include <utility>
30 : :
31 : : #include "./base/exception.h"
32 : :
33 : : namespace vlink {
34 : :
35 : 8 : [[maybe_unused]] static int64_t parse_port_value(const std::string& port_str) {
36 : : try {
37 [ + - ]: 8 : auto parsed = std::stoul(port_str);
38 : :
39 [ + + ]: 8 : if VUNLIKELY (parsed > 65535U) {
40 [ + - + - : 1 : throw Exception::RuntimeError("Port out of range (must be 0-65535): \"" + port_str + "\".");
+ - ]
41 : : }
42 : :
43 : 7 : return static_cast<int64_t>(parsed);
44 [ - + ]: 1 : } catch (const std::exception&) {
45 [ + - + - : 1 : throw Exception::RuntimeError("Invalid port \"" + port_str + "\".");
+ - ]
46 : 1 : }
47 : : }
48 : :
49 : : // UrlParser
50 : 52 : UrlParser::UrlParser(const char* str, Category category, Separator separator)
51 : 52 : : category_(category), separator_(separator) {
52 [ + - + + ]: 70 : setup(std::string(str), category);
53 : 124 : }
54 : :
55 : 576 : UrlParser::UrlParser(const std::string& str, Category category, Separator separator)
56 : 576 : : category_(category), separator_(separator) {
57 [ + + ]: 576 : setup(str, category);
58 : 600 : }
59 : :
60 : 10 : UrlParser::UrlParser(const std::map<Component, std::string>& components, Category category, bool rooted,
61 : 10 : Separator separator)
62 : 10 : : category_(category), is_rooted_(rooted), separator_(separator) {
63 [ + - + - ]: 10 : if VLIKELY (components.count(Component::kTransport) != 0) {
64 [ + - + + ]: 10 : if VUNLIKELY (components.at(Component::kTransport).empty()) {
65 [ + - ]: 1 : throw Exception::RuntimeError("The URL transport prefix cannot be empty.");
66 : : }
67 : :
68 [ + - + - ]: 9 : transport_ = components.at(Component::kTransport);
69 : : } else {
70 [ # # ]: 0 : throw Exception::RuntimeError("A URL must include a transport prefix.");
71 : : }
72 : :
73 [ + + ]: 9 : if (category == Category::kHierarchical) {
74 [ + - + + ]: 8 : if VUNLIKELY (components.count(Component::kContent) != 0) {
75 [ + - ]: 1 : throw Exception::RuntimeError("The content component is only valid for non-hierarchical URLs.");
76 : : }
77 : :
78 [ + - ]: 7 : bool has_username = components.count(Component::kUsername) != 0;
79 [ + - ]: 7 : bool has_password = components.count(Component::kPassword) != 0;
80 : :
81 [ + + + + ]: 7 : if (has_username && has_password) {
82 [ + - + - ]: 1 : username_ = components.at(Component::kUsername);
83 [ + - + - ]: 1 : password_ = components.at(Component::kPassword);
84 [ + + + - : 6 : } else if VUNLIKELY ((has_username && !has_password) || (!has_username && has_password)) {
+ + + - -
+ - + +
+ ]
85 [ + - ]: 1 : throw Exception::RuntimeError("If a username or password is supplied, both must be provided.");
86 : : }
87 : :
88 [ + - + - ]: 6 : if (components.count(Component::kHost) != 0) {
89 [ + - + - ]: 6 : host_ = components.at(Component::kHost);
90 : : }
91 : :
92 [ + - + + ]: 6 : if (components.count(Component::kPort) != 0) {
93 [ + - + - ]: 1 : port_ = parse_port_value(components.at(Component::kPort));
94 : : }
95 : :
96 [ + - + + ]: 6 : if VLIKELY (components.count(Component::kPath) != 0) {
97 [ + - + - ]: 5 : path_ = components.at(Component::kPath);
98 : : } else {
99 [ + - ]: 1 : throw Exception::RuntimeError("A path is required on a hierarchical URL, even an empty path.");
100 : : }
101 : :
102 : : } else {
103 [ + - + - : 1 : if VUNLIKELY (components.count(Component::kUsername) != 0 || components.count(Component::kPassword) != 0 ||
+ - - + +
- + - - +
+ - + - -
+ + - + -
- + - + ]
104 : : components.count(Component::kHost) != 0 || components.count(Component::kPort) != 0 ||
105 : : components.count(Component::kPath) != 0) {
106 [ # # ]: 0 : throw Exception::RuntimeError("None of the hierarchical components are allowed in a non-hierarchical URL.");
107 : : }
108 : :
109 [ + - + - ]: 1 : if VLIKELY (components.count(Component::kContent) != 0) {
110 [ + - + - ]: 1 : content_ = components.at(Component::kContent);
111 : : } else {
112 [ # # ]: 0 : throw Exception::RuntimeError("A non-hierarchical URL requires a content component, even if it is empty.");
113 : : }
114 : : }
115 : :
116 [ + - + + ]: 6 : if (components.count(Component::kQuery) != 0) {
117 [ + - + - ]: 1 : query_ = components.at(Component::kQuery);
118 : : }
119 : :
120 [ + - + + ]: 6 : if (components.count(Component::kFragment) != 0) {
121 [ + - + - ]: 1 : fragment_ = components.at(Component::kFragment);
122 : : }
123 : :
124 [ + - ]: 6 : init_query_dictionary();
125 : 42 : }
126 : :
127 : 4 : UrlParser::UrlParser(const UrlParser& other, const std::map<Component, std::string>& replacements)
128 : 4 : : category_(other.category_), is_rooted_(other.is_rooted_), separator_(other.separator_) {
129 [ + - + + : 4 : transport_ = (replacements.count(Component::kTransport)) ? replacements.at(Component::kTransport) : other.transport_;
+ - + - ]
130 : :
131 [ + - ]: 4 : if (category_ == Category::kHierarchical) {
132 [ + - - + : 4 : username_ = (replacements.count(Component::kUsername)) ? replacements.at(Component::kUsername) : other.username_;
- - + - ]
133 [ + - - + : 4 : password_ = (replacements.count(Component::kPassword)) ? replacements.at(Component::kPassword) : other.password_;
- - + - ]
134 [ + - + + : 4 : host_ = (replacements.count(Component::kHost)) ? replacements.at(Component::kHost) : other.host_;
+ - + - ]
135 [ + - - + : 4 : port_ = (replacements.count(Component::kPort)) ? parse_port_value(replacements.at(Component::kPort)) : other.port_;
- - - - ]
136 [ + - - + : 4 : path_ = (replacements.count(Component::kPath)) ? replacements.at(Component::kPath) : other.path_;
- - + - ]
137 : : } else {
138 [ # # # # : 0 : content_ = (replacements.count(Component::kContent)) ? replacements.at(Component::kContent) : other.content_;
# # # # ]
139 : : }
140 : :
141 [ + - + + : 4 : query_ = (replacements.count(Component::kQuery)) ? replacements.at(Component::kQuery) : other.query_;
+ - + - ]
142 [ + - + + : 4 : fragment_ = (replacements.count(Component::kFragment)) ? replacements.at(Component::kFragment) : other.fragment_;
+ - + - ]
143 : :
144 [ + - ]: 4 : init_query_dictionary();
145 : 4 : }
146 : :
147 : 586 : const std::string& UrlParser::get_transport() const { return transport_; }
148 : :
149 : 5 : UrlParser::Category UrlParser::get_category() const { return category_; }
150 : :
151 : 4 : const std::string& UrlParser::get_content() const {
152 [ + + ]: 4 : if VUNLIKELY (category_ != Category::kNonHierarchical) {
153 [ + - ]: 1 : throw Exception::RuntimeError("The content Component is only valid for non-hierarchical URLs.");
154 : : }
155 : :
156 : 3 : return content_;
157 : : }
158 : :
159 : 5 : const std::string& UrlParser::get_username() const {
160 [ + + ]: 5 : if VUNLIKELY (category_ != Category::kHierarchical) {
161 [ + - ]: 1 : throw Exception::RuntimeError("The username Component is only valid for hierarchical URLs.");
162 : : }
163 : :
164 : 4 : return username_;
165 : : }
166 : :
167 : 5 : const std::string& UrlParser::get_password() const {
168 [ + + ]: 5 : if VUNLIKELY (category_ != Category::kHierarchical) {
169 [ + - ]: 1 : throw Exception::RuntimeError("The password Component is only valid for hierarchical URLs.");
170 : : }
171 : :
172 : 4 : return password_;
173 : : }
174 : :
175 : 589 : const std::string& UrlParser::get_host() const {
176 [ + + ]: 589 : if VUNLIKELY (category_ != Category::kHierarchical) {
177 [ + - ]: 1 : throw Exception::RuntimeError("The host Component is only valid for hierarchical URLs.");
178 : : }
179 : :
180 : 588 : return host_;
181 : : }
182 : :
183 : 9 : int64_t UrlParser::get_port() const {
184 [ + + ]: 9 : if VUNLIKELY (category_ != Category::kHierarchical) {
185 [ + - ]: 1 : throw Exception::RuntimeError("The port Component is only valid for hierarchical URLs.");
186 : : }
187 : :
188 : 8 : return port_;
189 : : }
190 : :
191 : 584 : const std::string& UrlParser::get_path() const {
192 [ + + ]: 584 : if VUNLIKELY (category_ != Category::kHierarchical) {
193 [ + - ]: 1 : throw Exception::RuntimeError("The path Component is only valid for hierarchical URLs.");
194 : : }
195 : :
196 : 583 : return path_;
197 : : }
198 : :
199 : 3 : const std::string& UrlParser::get_query() const { return query_; }
200 : :
201 : 585 : const std::map<std::string, std::string>& UrlParser::get_query_dictionary() const { return query_dict_; }
202 : :
203 : 578 : const std::string& UrlParser::get_fragment() const { return fragment_; }
204 : :
205 : 7 : std::string UrlParser::to_string() const {
206 : 7 : std::string full_url;
207 [ + - ]: 7 : full_url.append(transport_);
208 [ + - ]: 7 : full_url.append(":");
209 : :
210 [ + + ]: 7 : if (category_ == Category::kNonHierarchical) {
211 [ + - ]: 1 : full_url.append(content_);
212 [ + - ]: 6 : } else if (content_.length() > path_.length()) {
213 [ + - ]: 6 : full_url.append("//");
214 : :
215 [ + + + - : 6 : if (!(username_.empty() || password_.empty())) {
+ + ]
216 [ + - ]: 1 : full_url.append(username_);
217 [ + - ]: 1 : full_url.append(":");
218 [ + - ]: 1 : full_url.append(password_);
219 [ + - ]: 1 : full_url.append("@");
220 : : }
221 : :
222 [ + - ]: 6 : full_url.append(host_);
223 : :
224 [ + + ]: 6 : if (port_ != 0) {
225 [ + - ]: 1 : full_url.append(":");
226 [ + - + - ]: 1 : full_url.append(std::to_string(port_));
227 : : }
228 : : }
229 : :
230 [ + + ]: 7 : if (is_rooted_) {
231 [ + - ]: 5 : full_url.append("/");
232 : : }
233 : :
234 [ + - ]: 7 : full_url.append(path_);
235 : :
236 [ + + ]: 7 : if (!query_.empty()) {
237 [ + - ]: 1 : full_url.append("?");
238 [ + - ]: 1 : full_url.append(query_);
239 : : }
240 : :
241 [ + + ]: 7 : if (!fragment_.empty()) {
242 [ + - ]: 1 : full_url.append("#");
243 [ + - ]: 1 : full_url.append(fragment_);
244 : : }
245 : :
246 : 7 : return full_url;
247 : : } // LCOV_EXCL_LINE GCOVR_EXCL_LINE
248 : :
249 : 628 : void UrlParser::setup(const std::string& str, Category category) {
250 : : (void)category;
251 : :
252 : 628 : size_t const url_length = str.length();
253 : :
254 [ + + ]: 628 : if VUNLIKELY (url_length == 0) {
255 [ + - ]: 1 : throw Exception::RuntimeError("URLs cannot be of zero length.");
256 : : }
257 : :
258 [ + + ]: 627 : std::string::const_iterator cursor = parse_transport(str, str.begin());
259 [ + + ]: 621 : cursor = parse_content(str, (cursor + 1));
260 : :
261 [ + + + + : 617 : if ((cursor != str.end()) && (*cursor == '?')) {
+ + ]
262 [ + - ]: 201 : cursor = parse_query(str, (cursor + 1));
263 : : }
264 : :
265 [ + + + - : 617 : if ((cursor != str.end()) && (*cursor == '#')) {
+ + ]
266 [ + - ]: 16 : cursor = parse_fragment(str, (cursor + 1));
267 : : }
268 : :
269 [ + + ]: 617 : init_query_dictionary();
270 : 616 : }
271 : :
272 : 627 : std::string::const_iterator UrlParser::parse_transport(const std::string& str,
273 : : std::string::const_iterator transport_start) {
274 : 627 : std::string::const_iterator transport_end = transport_start;
275 : :
276 [ + + + + : 3172 : while ((transport_end != str.end()) && (*transport_end != ':')) {
+ + ]
277 [ + + + + : 2547 : if VUNLIKELY (!std::isalnum(*transport_end) && (*transport_end != '-') && (*transport_end != '+') &&
+ + + - +
+ + - +
+ ]
278 : : (*transport_end != '.')) {
279 [ + - + - : 2 : throw Exception::RuntimeError("Invalid character found in the URL transport prefix. Supplied URL was: \"" + str +
+ - ]
280 : 4 : "\".");
281 : : }
282 : :
283 : 2545 : ++transport_end;
284 : : }
285 : :
286 [ + + ]: 625 : if VUNLIKELY (transport_end == str.end()) {
287 [ + - + - : 3 : throw Exception::RuntimeError("End of URL found while parsing the transport prefix. Supplied URL was: \"" + str +
+ - ]
288 : 6 : "\".");
289 : : }
290 : :
291 [ + + ]: 622 : if VUNLIKELY (transport_start == transport_end) {
292 [ + - + - : 1 : throw Exception::RuntimeError("The URL transport prefix cannot be zero-length. Supplied URL was: \"" + str + "\".");
+ - ]
293 : : }
294 : :
295 [ + - ]: 621 : transport_ = std::string(transport_start, transport_end);
296 : :
297 : 621 : return transport_end;
298 : : }
299 : :
300 : 621 : std::string::const_iterator UrlParser::parse_content(const std::string& str,
301 : : std::string::const_iterator content_start) {
302 : 621 : std::string::const_iterator content_end = content_start;
303 : :
304 [ + + + + : 10759 : while ((content_end != str.end()) && (*content_end != '?') && (*content_end != '#')) {
+ + + + ]
305 : 10138 : ++content_end;
306 : : }
307 : :
308 [ + - ]: 621 : content_ = std::string(content_start, content_end);
309 : :
310 [ + + + - : 621 : if ((category_ == Category::kHierarchical) && (!content_.empty())) {
+ + ]
311 : 616 : std::string::const_iterator path_start = content_.begin();
312 : 616 : std::string::const_iterator path_end = content_.end();
313 : :
314 [ + - + + ]: 616 : if (!content_.compare(0, 2, "//")) {
315 : 615 : std::string::const_iterator authority_cursor = (content_.begin() + 2);
316 : :
317 [ + + ]: 615 : if (content_.find_first_of('@') != std::string::npos) {
318 [ + + ]: 4 : std::string::const_iterator userpass_divider = parse_username(str, content_, authority_cursor);
319 [ + - ]: 3 : authority_cursor = parse_password(str, content_, (userpass_divider + 1));
320 : 3 : ++authority_cursor;
321 : : }
322 : :
323 [ + + ]: 614 : authority_cursor = parse_host(str, content_, authority_cursor);
324 : :
325 [ + + + + : 613 : if ((authority_cursor != content_.end()) && (*authority_cursor == ':')) {
+ + ]
326 [ + + ]: 8 : authority_cursor = parse_port(str, content_, (authority_cursor + 1));
327 : : }
328 : :
329 [ + + + - : 611 : if ((authority_cursor != content_.end()) && (*authority_cursor == '/')) {
+ + ]
330 : 376 : is_rooted_ = true;
331 : 376 : path_start = authority_cursor + 1;
332 : : }
333 : :
334 [ + + ]: 611 : if (authority_cursor == content_.end()) {
335 : 235 : path_start = content_.end();
336 : : }
337 [ + - + - ]: 1 : } else if (!content_.compare(0, 1, "/")) {
338 : 1 : is_rooted_ = true;
339 : 1 : ++path_start;
340 : : }
341 : :
342 [ + - ]: 612 : path_ = std::string(path_start, path_end);
343 : : }
344 : :
345 : 617 : return content_end;
346 : : }
347 : :
348 : 4 : std::string::const_iterator UrlParser::parse_username(const std::string& str, const std::string& content,
349 : : std::string::const_iterator username_start) {
350 : : (void)content;
351 : :
352 : 4 : std::string::const_iterator username_end = username_start;
353 : :
354 [ + - + + : 18 : while (username_end != content.end() && *username_end != ':') {
+ + ]
355 [ + + ]: 15 : if VUNLIKELY (*username_end == '@') {
356 [ + - ]: 1 : throw Exception::RuntimeError(
357 [ + - + - ]: 2 : "A username in the authority must be followed by ':password'. Supplied URL was: \"" + str + "\".");
358 : : }
359 : :
360 : 14 : ++username_end;
361 : : }
362 : :
363 [ - + ]: 3 : if VUNLIKELY (username_end == content.end()) {
364 [ # # ]: 0 : throw Exception::RuntimeError(
365 : : "A username in the authority must be followed by ':password'. Supplied URL was: \"" + // LCOV_EXCL_LINE
366 : : // GCOVR_EXCL_LINE
367 : : str + "\"."); // LCOV_EXCL_LINE GCOVR_EXCL_LINE
368 : : }
369 : :
370 [ + - ]: 3 : username_ = std::string(username_start, username_end);
371 : :
372 : 3 : return username_end;
373 : : }
374 : :
375 : 3 : std::string::const_iterator UrlParser::parse_password(const std::string& str, const std::string& content,
376 : : std::string::const_iterator password_start) {
377 : : (void)content;
378 : :
379 : 3 : std::string::const_iterator password_end = password_start;
380 : :
381 [ + - + + : 14 : while (password_end != content.end() && *password_end != '@') {
+ + ]
382 : 11 : ++password_end;
383 : : }
384 : :
385 [ - + ]: 3 : if VUNLIKELY (password_end == content.end()) {
386 [ # # ]: 0 : throw Exception::RuntimeError(
387 : : "A password in the authority must be followed by '@'. Supplied URL was: \"" + // LCOV_EXCL_LINE GCOVR_EXCL_LINE
388 : : str + // LCOV_EXCL_LINE GCOVR_EXCL_LINE
389 : : "\"."); // LCOV_EXCL_LINE GCOVR_EXCL_LINE
390 : : }
391 : :
392 [ + - ]: 3 : password_ = std::string(password_start, password_end);
393 : :
394 : 3 : return password_end;
395 : : }
396 : :
397 : 614 : std::string::const_iterator UrlParser::parse_host(const std::string& str, const std::string& content,
398 : : std::string::const_iterator host_start) {
399 : 614 : std::string::const_iterator host_end = host_start;
400 : :
401 [ + + ]: 4654 : while (host_end != content.end()) {
402 [ + + ]: 4420 : if (*host_end == '[') {
403 [ + + + + : 15 : while ((host_end != content.end()) && (*host_end != ']')) {
+ + ]
404 : 13 : ++host_end;
405 : : }
406 : :
407 [ + + ]: 2 : if VUNLIKELY (host_end == content.end()) {
408 [ + - ]: 1 : throw Exception::RuntimeError(
409 : : "End of content Component encountered "
410 : : "while parsing the host Component. "
411 [ + - ]: 1 : "Supplied URL was: \"" +
412 [ + - ]: 3 : str + "\".");
413 : : }
414 : :
415 : 1 : ++host_end;
416 : :
417 : 1 : break;
418 : : }
419 : :
420 [ + + + + : 4418 : if ((*host_end == ':') || (*host_end == '/')) {
+ + ]
421 : 378 : break;
422 : : }
423 : :
424 : 4040 : ++host_end;
425 : : }
426 : :
427 [ + - ]: 613 : host_ = std::string(host_start, host_end);
428 : 613 : return host_end;
429 : : }
430 : :
431 : 8 : std::string::const_iterator UrlParser::parse_port(const std::string& str, const std::string& content,
432 : : std::string::const_iterator port_start) {
433 : 8 : std::string::const_iterator port_end = port_start;
434 : :
435 [ + + + + : 38 : while ((port_end != content.end()) && (*port_end != '/')) {
+ + ]
436 [ + + ]: 31 : if VUNLIKELY (!std::isdigit(*port_end)) {
437 [ + - ]: 1 : throw Exception::RuntimeError(
438 : : "Invalid character while parsing the port. "
439 [ + - ]: 1 : "Supplied URL was: \"" +
440 [ + - ]: 3 : str + "\".");
441 : : }
442 : :
443 : 30 : ++port_end;
444 : : }
445 : :
446 [ + - ]: 7 : if (port_start != port_end) {
447 [ + - + + ]: 9 : port_ = parse_port_value(std::string(port_start, port_end));
448 : : }
449 : :
450 : 6 : return port_end;
451 : : }
452 : :
453 : 201 : std::string::const_iterator UrlParser::parse_query(const std::string& str, std::string::const_iterator query_start) {
454 : 201 : std::string::const_iterator query_end = query_start;
455 : :
456 [ + + + + : 2846 : while ((query_end != str.end()) && (*query_end != '#')) {
+ + ]
457 : 2645 : ++query_end;
458 : : }
459 : :
460 [ + - ]: 201 : query_ = std::string(query_start, query_end);
461 : :
462 : 201 : return query_end;
463 : : }
464 : :
465 : 16 : std::string::const_iterator UrlParser::parse_fragment(const std::string& str,
466 : : std::string::const_iterator fragment_start) {
467 [ + - ]: 16 : fragment_ = std::string(fragment_start, str.end());
468 : :
469 : 16 : return str.end();
470 : : }
471 : :
472 : 627 : void UrlParser::init_query_dictionary() {
473 [ + + ]: 627 : if (!query_.empty()) {
474 [ + + ]: 203 : char separator = (separator_ == Separator::kAmpersand) ? '&' : ';';
475 : 203 : size_t carat = 0;
476 : 203 : size_t stanza_end = query_.find_first_of(separator);
477 : :
478 : : do {
479 : : std::string stanza =
480 [ + + + - ]: 252 : query_.substr(carat, ((stanza_end != std::string::npos) ? (stanza_end - carat) : std::string::npos));
481 : 252 : size_t key_value_divider = stanza.find_first_of('=');
482 [ + - ]: 252 : std::string key = stanza.substr(0, key_value_divider);
483 : 252 : std::string value;
484 : :
485 [ + + ]: 252 : if (key_value_divider != std::string::npos) {
486 [ + - ]: 251 : value = stanza.substr((key_value_divider + 1));
487 : : }
488 : :
489 [ + - + + ]: 252 : if VUNLIKELY (query_dict_.count(key) != 0) {
490 [ + - ]: 1 : throw Exception::RuntimeError("Bad key in the query string!");
491 : : }
492 : :
493 [ + - ]: 251 : query_dict_.emplace(key, std::move(value));
494 [ + + ]: 251 : carat = ((stanza_end != std::string::npos) ? (stanza_end + 1) : std::string::npos);
495 : 251 : stanza_end = query_.find_first_of(separator, carat);
496 [ + + + + ]: 254 : } while ((stanza_end != std::string::npos) || (carat != std::string::npos));
497 : : }
498 : 626 : }
499 : :
500 : : } // namespace vlink
|