Stroika Library 3.0d24
 
Loading...
Searching...
No Matches
URI.cpp
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2026. All rights reserved
3 */
4#include "Stroika/Foundation/StroikaPreComp.h"
5
9#include "Stroika/Foundation/Characters/String2Int.h"
12#include "Stroika/Foundation/Execution/Exceptions.h"
13#include "Stroika/Foundation/Execution/Throw.h"
14
15#include "URI.h"
16
17// Comment this in to turn on aggressive noisy DbgTrace in this module
18// #define USE_NOISY_TRACE_IN_THIS_MODULE_ 1
19
20using namespace Stroika::Foundation;
23using namespace Stroika::Foundation::IO;
25
27
28/*
29 ********************************************************************************
30 ************************************** URI *************************************
31 ********************************************************************************
32 */
33namespace {
34 String remove_dot_segments_ (const String& p, URI::NormalizationStyle normalization = URI::NormalizationStyle::eRFC3986)
35 {
36 // @todo - this is a fairly inefficient implementation, but so far hasn't shown up in profiles
37#if USE_NOISY_TRACE_IN_THIS_MODULE_
38 //Debug::TraceContextBumper{"remove_dot_segments_", "p={},normalization={}"_f, p, normalization};
39#endif
40 // from https://tools.ietf.org/html/rfc3986#section-5.2.4
41 vector<String> segments; // for our purpose here, segments may (or not in case of first) contain a leading /
42 StringBuilder accumulatingSegment;
43 for (Character c : p) {
44 if (c == '/' and not accumulatingSegment.empty ()) {
45 segments.push_back (accumulatingSegment.str ());
46 accumulatingSegment.clear ();
47 }
48 accumulatingSegment << c;
49 }
50 if (not accumulatingSegment.empty ()) {
51 segments.push_back (accumulatingSegment.str ());
52 }
53 vector<String> segments2; // apply ../. removal
54 bool lastSegmentShouldHaveSlash{false}; // not sure about this
55 for (const String& segment : segments) {
56 lastSegmentShouldHaveSlash = false;
57 if (segment == "."sv or segment == "/."sv) {
58 // drop it on the floor
59 if (segment[0] == '/') {
60 lastSegmentShouldHaveSlash = true;
61 }
62 }
63 else if (segment == ".."sv or segment == "/.."sv) {
64 if (not segments2.empty ()) {
65 segments2.pop_back ();
66 }
67 if (segment[0] == '/') {
68 lastSegmentShouldHaveSlash = true;
69 }
70 }
71 else {
72 segments2.push_back (segment);
73 }
74 }
75
76 StringBuilder result;
77 bool soFarEndsWithSlash = false;
78 for (const String& segment : segments2) {
79 if (normalization == URI::NormalizationStyle::eAggressive) {
80 if (segment.StartsWith ('/') and soFarEndsWithSlash) {
81 String add = segment.SubString (1);
82 if (not add.empty ()) {
83 result << add;
84 soFarEndsWithSlash = add.EndsWith ('/');
85 }
86 }
87 else {
88 result << segment;
89 soFarEndsWithSlash = segment.EndsWith ('/');
90 }
91 }
92 else {
93 result << segment;
94 }
95 }
96 if (lastSegmentShouldHaveSlash and not result.str ().EndsWith ("/"sv)) {
97 result << "/"sv;
98 }
99 return result;
100 };
101}
102
103URI URI::Parse (const String& rawURL)
104{
105#if USE_NOISY_TRACE_IN_THIS_MODULE_
106 Debug::TraceContextBumper{"IO::Network::URI::Parse", "{}"_f, rawURL};
107#endif
108 // https://tools.ietf.org/html/rfc3986#appendix-B
109 static const RegularExpression kParseURLRegExp_{"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?"_RegEx};
112 optional<String> path;
116 if (s) {
117 if (not s->empty ()) {
118 return s;
119 }
120 }
121 return nullopt;
122 };
123 (void)rawURL.AsASCII (); // for throw check side-effect
124 if (rawURL.Matches (kParseURLRegExp_, nullptr, &scheme, nullptr, &authority, &path, nullptr, &query, nullptr, &fragment)) {
126 UniformResourceIdentification::PCTDecode2String (path.value_or (String{})), emptyStr2Missing (query), emptyStr2Missing (fragment)};
127 }
128 else {
129 static const Execution::RuntimeErrorException kException_{"Ill-formed URI"sv};
130 Execution::Throw (kException_); // doesn't match regexp in https://tools.ietf.org/html/rfc3986#appendix-B
131 }
132}
133
134URI URI::ParseRelative (const String& rawRelativeURL)
135{
136#if USE_NOISY_TRACE_IN_THIS_MODULE_
137 Debug::TraceContextBumper{"IO::Network::URI::ParseRelative", "{}"_f, rawRelativeURL};
138#endif
139 // https://tools.ietf.org/html/rfc3986#appendix-B
140 static const RegularExpression kParseRelativeURLRegExp_{"([^?#]*)(\\?([^#]*))?(#(.*))?"_RegEx};
143 optional<String> path;
147 if (s) {
148 if (not s->empty ()) {
149 return s;
150 }
151 }
152 return nullopt;
153 };
154 (void)rawRelativeURL.AsASCII (); // for throw check side-effect
155 if (rawRelativeURL.Matches (kParseRelativeURLRegExp_, &path, nullptr, &query, nullptr, &fragment)) {
156 return URI{nullopt, nullopt, UniformResourceIdentification::PCTDecode2String (path.value_or (String{})), emptyStr2Missing (query),
158 }
159 else {
160 static const Execution::RuntimeErrorException kException_{"Ill-formed relative URI"sv};
161 Execution::Throw (kException_); // doesn't match regexp in https://tools.ietf.org/html/rfc3986#appendix-B
162 }
163}
164
165String URI::AsString_ (optional<StringPCTEncodedFlag> pctEncode) const
166{
167 // https://github.com/SophistSolutions/Stroika/issues/1132 (STK-1000) -- issue about maybe needed more nuanced approach
168 StringPCTEncodedFlag usingPCTEncodeFlag = pctEncode.value_or (eDecoded);
169 AssertExternallySynchronizedChecker::ReadContext declareContext{fThisAssertExternallySynchronized_};
170 StringBuilder result;
171 if (fScheme_) {
172 // From https://tools.ietf.org/html/rfc3986#appendix-A
173 // scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
174 // no need to pct encode this
175 Assert (fScheme_->All ([] (Character c) { return c.IsASCII (); }));
176 result << *fScheme_ << ":"sv;
177 }
178 if (fAuthority_) {
179 Assert (fAuthority_->As<String> (usingPCTEncodeFlag).All ([] (Character c) { return c.IsASCII (); }));
180 result << "//"sv << fAuthority_->As<String> (usingPCTEncodeFlag);
181 }
182
183 if (fAuthority_ and not(fPath_.empty () or fPath_.StartsWith ("/"sv))) {
184 // NOT SURE HOW TO HANDLE
185 static const Execution::RuntimeErrorException kException_{"This is not a legal URI to encode (authority present, but path not empty or absolute)"sv};
187 }
188
189 if (usingPCTEncodeFlag == eDecoded) {
190 result << fPath_;
191 }
192 else {
194 .allowSubDelims = false, .allowGenDelims = false, .allowPChar = true, .allowFragOrQueryChars = false, .allowPathCharacters = true};
195 result << UniformResourceIdentification::PCTEncode2String (fPath_, kPathEncodeOptions_);
196 }
197
198 if (fQuery_) {
200 .allowSubDelims = false, .allowGenDelims = false, .allowPChar = false, .allowFragOrQueryChars = true};
201 if (usingPCTEncodeFlag == eDecoded) {
202 result << "?"sv << *fQuery_;
203 }
204 else {
205 result << "?"sv << UniformResourceIdentification::PCTEncode2String (*fQuery_, kQueryEncodeOptions_);
206 }
207 }
208 if (fFragment_) {
209 if (usingPCTEncodeFlag == eDecoded) {
210 result << "#"sv << *fFragment_;
211 }
212 else {
213 static constexpr UniformResourceIdentification::PCTEncodeOptions kFragmentEncodeOptions_{false, false, false, true};
214 result << "#"sv << UniformResourceIdentification::PCTEncode2String (*fFragment_, kFragmentEncodeOptions_);
215 }
216 }
217 Ensure (result.str ().All ([] (Character c) { return c.IsASCII (); }));
218 return result.str ();
219}
220
221URI::operator bool () const
222{
223 AssertExternallySynchronizedChecker::ReadContext declareContext{fThisAssertExternallySynchronized_};
224 if (fScheme_) {
225 return true;
226 }
227 if (fAuthority_) {
228 return true;
229 }
230 if (not fPath_.empty ()) {
231 return true;
232 }
233 if (fQuery_) {
234 return true;
235 }
236 if (fFragment_) {
237 return true;
238 }
239 return false;
240}
241
243{
244 AssertExternallySynchronizedChecker::ReadContext declareContext{fThisAssertExternallySynchronized_};
245 static const RegularExpression kSelectDir_ = "(.*\\/)[^\\/]*"_RegEx;
247 (void)fPath_.Matches (kSelectDir_, &baseDir);
248 return baseDir.value_or (String{});
249}
250
252{
253 AssertExternallySynchronizedChecker::ReadContext declareContext{fThisAssertExternallySynchronized_};
254 optional<SchemeType> scheme = fScheme_;
255 if (scheme) {
256 scheme = scheme->Normalize ();
257 }
258 optional<Authority> authority = fAuthority_;
259 if (authority) {
260 authority = authority->Normalize ();
261 }
262 String path = remove_dot_segments_ (fPath_, normalization); // review https://tools.ietf.org/html/rfc3986#section-6.2.2.3 - this algorithm for removing dots was from merge code, so not sure it applies here
263 return URI{scheme, authority, path, fQuery_, fFragment_};
264}
265
267{
268 // dont use As<String> () because this can throw if bad string - and no need to pct-encode here
269 AssertExternallySynchronizedChecker::ReadContext declareContext{fThisAssertExternallySynchronized_};
270 StringBuilder result;
271 if (fScheme_) {
272 result << *fScheme_ << ":"sv;
273 }
274 if (fAuthority_) {
275 result << "//"sv << fAuthority_->As<String> ();
276 }
277 result << fPath_;
278 if (fQuery_) {
279 result << "?"sv << *fQuery_;
280 }
281 if (fFragment_) {
282 result << "#"sv << *fFragment_;
283 }
284 return result;
285}
286
287void URI::CheckValidPathForAuthority_ (const optional<Authority>& authority, const String& path)
288{
289 /*
290 * https://tools.ietf.org/html/rfc3986#section-3.3
291 * If a URI contains an authority component, then the path component
292 * must either be empty or begin with a slash ("/") character
293 */
294 if (authority and (not path.empty () and not path.StartsWith ("/"sv))) {
295 static const Execution::RuntimeErrorException kException_{"A URI with an authority must have an empty path, or an absolute path"sv};
297 }
298}
299
300URI URI::Combine (const URI& overridingURI) const
301{
302 AssertExternallySynchronizedChecker::ReadContext declareContext{fThisAssertExternallySynchronized_};
303
304 /*
305 * This is not strictly according to Hoyle, but it avoids a common inconvenience with the Scheme check below. And avoids having to write a lot of
306 * code like:
307 * if (l) {
308 * return l.Combine (r);
309 * }
310 * else {
311 * return r;
312 * }
313 */
314 if (not *this) {
315 return overridingURI;
316 }
317
318 /*
319 * From https://tools.ietf.org/html/rfc3986#section-5
320 * "Note that only the scheme component is required to be present in a base URI; the other components may be empty or undefined."
321 */
322 URI baseURI = Normalize ();
323 if (not baseURI.GetScheme ()) {
324 static const Execution::RuntimeErrorException kException_{"Scheme is required in base URI to combine with another URI"sv};
326 }
327 auto merge = [&] (const String& base, const String& rhs) -> String {
328 // @see https://tools.ietf.org/html/rfc3986#section-5.2.3
329 if (baseURI.GetAuthority () and base.empty ()) {
330 return "/"sv + rhs;
331 }
332 static const RegularExpression kSelectDir_ = "(.*\\/)[^\\/]*"_RegEx;
335 return baseDir.value_or (String{}) + rhs;
336 };
337
338 Assert (remove_dot_segments_ ("/a/b/c/./../../g") == "/a/g"); // from https://tools.ietf.org/html/rfc3986#section-5.2.4
339 Assert (remove_dot_segments_ ("mid/content=5/../6") == "mid/6"); // ditto
340
341 // Algorithm copied from https://tools.ietf.org/html/rfc3986#section-5.2.2
342 URI result;
343
344 /*
345 * Skipped this part
346 * -- A non-strict parser may ignore a scheme in the reference
347 * -- if it is identical to the base URI's scheme.
348 * --
349 * if ((not strict) and (R.scheme == Base.scheme)) then
350 * undefine(R.scheme);
351 * endif;
352 */
353 if (overridingURI.GetScheme ()) {
354 result.SetScheme (overridingURI.GetScheme ());
355 result.SetAuthority (overridingURI.GetAuthority ());
356 result.SetPath (remove_dot_segments_ (overridingURI.GetPath ()));
357 result.SetQuery (overridingURI.GetQuery<String> ());
358 }
359 else {
360 result.SetScheme (baseURI.GetScheme ());
361 if (overridingURI.GetAuthority ()) {
362 result.SetAuthority (overridingURI.GetAuthority ());
363 result.SetPath (remove_dot_segments_ (overridingURI.GetPath ()));
364 result.SetQuery (overridingURI.GetQuery<String> ());
365 }
366 else {
367 result.SetAuthority (baseURI.GetAuthority ());
368 if (overridingURI.GetPath ().empty ()) {
369 result.SetPath (baseURI.GetPath ());
370 result.SetQuery (overridingURI.GetQuery<String> () ? overridingURI.GetQuery<String> () : baseURI.GetQuery<String> ());
371 }
372 else {
373 if (overridingURI.GetPath ().StartsWith ("/"sv)) {
374 result.SetPath (remove_dot_segments_ (overridingURI.GetPath ()));
375 }
376 else {
377 result.SetPath (remove_dot_segments_ (merge (baseURI.GetPath (), overridingURI.GetPath ())));
378 }
379 result.SetQuery (overridingURI.GetQuery<String> ());
380 }
381 }
382 }
383 result.SetFragment (overridingURI.GetFragment ());
384 return result;
385}
386
387strong_ordering URI::TWC_ (const URI& lhs, const URI& rhs)
388{
389 using namespace UniformResourceIdentification;
390 if (auto cmp = Common::StdCompat::compare_three_way{}(lhs.GetScheme (), rhs.GetScheme ()); cmp != strong_ordering::equal) {
391 return cmp;
392 }
393 if (auto cmp = Common::StdCompat::compare_three_way{}(lhs.GetAuthority (), rhs.GetAuthority ()); cmp != strong_ordering::equal) {
394 return cmp;
395 }
396 if (auto cmp = Common::StdCompat::compare_three_way{}(lhs.GetPath (), rhs.GetPath ()); cmp != strong_ordering::equal) {
397 return cmp;
398 }
399 if (auto cmp = Common::StdCompat::compare_three_way{}(lhs.GetQuery (), rhs.GetQuery ()); cmp != strong_ordering::equal) {
400 return cmp;
401 }
402 if (auto cmp = Common::StdCompat::compare_three_way{}(lhs.GetFragment (), rhs.GetFragment ()); cmp != strong_ordering::equal) {
403 return cmp;
404 }
405 return strong_ordering::equal;
406}
407
408/*
409 ********************************************************************************
410 *********** hash<Stroika::Foundation::IO::Network::URI> ************************
411 ********************************************************************************
412 */
413size_t std::hash<Stroika::Foundation::IO::Network::URI>::operator() (const Stroika::Foundation::IO::Network::URI& arg) const
414{
415 return hash<Characters::String> () (arg.As<Characters::String> ());
416}
RegularExpression is a compiled regular expression which can be used to match on a String class.
Similar to String, but intended to more efficiently construct a String. Mutable type (String is large...
String is like std::u32string, except it is much easier to use, often much more space efficient,...
Definition String.h:201
nonvirtual bool Matches(const RegularExpression &regEx) const
Definition String.cpp:1145
nonvirtual bool EndsWith(const Character &c, CompareOptions co=eWithCase) const
Definition String.cpp:1100
nonvirtual String SubString(SZ from) const
nonvirtual bool StartsWith(const Character &c, CompareOptions co=eWithCase) const
Definition String.cpp:1071
NOT a real mutex - just a debugging infrastructure support tool so in debug builds can be assured thr...
nonvirtual String GetAuthorityRelativeResourceDir() const
Return the path component, excluding any text after the final /.
Definition URI.cpp:242
static URI Parse(const String &rawURL)
Definition URI.cpp:103
nonvirtual String ToString() const
Definition URI.cpp:266
nonvirtual T As(optional< StringPCTEncodedFlag > pctEncoded={}) const
nonvirtual void SetPath(const String &path)
Definition URI.inl:84
nonvirtual URI Combine(const URI &overridingURI) const
Combine overridingURI possibly relative url with this base url, to produce a new URI.
Definition URI.cpp:300
static URI ParseRelative(const String &rawRelativeURL)
Definition URI.cpp:134
nonvirtual URI Normalize(NormalizationStyle normalization=NormalizationStyle::eDefault) const
Produce a normalized representation of the URI.
Definition URI.cpp:251
nonvirtual void SetScheme(const optional< SchemeType > &scheme)
Definition URI.inl:41
static optional< Authority > Parse(const String &rawURLAuthorityText)
nonvirtual bool All(const function< bool(ArgByValueType< T >)> &testEachElt) const
return true iff argument predicate returns true for each element of the iterable
void Throw(T &&e2Throw)
identical to builtin C++ 'throw' except that it does helpful, type dependent DbgTrace() messages firs...
Definition Throw.inl:43