1 /** @file uri.cpp Universal Resource Identifier.
2 * @ingroup base
3 *
4 * @authors Copyright © 2010-2013 Daniel Swanson <danij@dengine.net>
5 * @authors Copyright © 2010-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
6 *
7 * @par License
8 * GPL: http://www.gnu.org/licenses/gpl.html
9 *
10 * <small>This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by the
12 * Free Software Foundation; either version 2 of the License, or (at your
13 * option) any later version. This program is distributed in the hope that it
14 * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
15 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
16 * Public License for more details. You should have received a copy of the GNU
17 * General Public License along with this program; if not, write to the Free
18 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
19 * 02110-1301 USA</small>
20 */
21
22 #include "doomsday/uri.h"
23 #include "doomsday/filesys/fs_main.h"
24 #include "doomsday/dualstring.h"
25 #include "doomsday/game.h"
26 #include "doomsday/doomsdayapp.h"
27
28 #include <de/str.h>
29 #include <de/reader.h>
30 #include <de/writer.h>
31 #include <de/unittest.h>
32 #include <de/NativePath>
33 #include <de/Reader>
34 #include <de/Writer>
35 #include <de/App>
36
37 #include <QDebug>
38 #include <QList>
39
40 namespace de {
41
42 static Uri::ResolverFunc resolverFunc;
43
44 /**
45 * Extracts the scheme from a string.
46 *
47 * @param stringWithScheme The scheme is removed from the string.
48 *
49 * @return Scheme, or empty string if no valid scheme was present.
50 */
extractScheme(String & stringWithScheme)51 static String extractScheme(String &stringWithScheme)
52 {
53 String scheme;
54 int pos = stringWithScheme.indexOf(':');
55 if (pos > URI_MINSCHEMELENGTH) // could be Windows-style driver letter "c:"
56 {
57 scheme = stringWithScheme.left(pos);
58 stringWithScheme.remove(0, pos + 1);
59 }
60 return scheme;
61 }
62
DENG2_PIMPL_NOREF(Uri)63 DENG2_PIMPL_NOREF(Uri)
64 {
65 Path path; ///< Path of the Uri.
66
67 DualString strPath; // Redundant; for legacy access, remove this!
68
69 DualString scheme; ///< Scheme of the Uri.
70
71 /// Cached copy of the resolved path.
72 Path resolvedPath;
73
74 /**
75 * The cached path only applies when this game is loaded.
76 *
77 * @note Add any other conditions here that result in different results for
78 * resolveUri().
79 */
80 void *resolvedForGame;
81
82 Impl() : resolvedForGame(0)
83 {}
84
85 Impl(Impl const &other)
86 : de::IPrivate(),
87 path (other.path),
88 strPath (other.strPath),
89 scheme (other.scheme),
90 resolvedPath (other.resolvedPath),
91 resolvedForGame(other.resolvedForGame)
92 {}
93
94 void clearCachedResolved()
95 {
96 resolvedPath.clear();
97 resolvedForGame = 0;
98 }
99
100 void parseRawUri(String rawUri, QChar sep, resourceclassid_t defaultResourceClass)
101 {
102 LOG_AS("Uri::parseRawUri");
103
104 clearCachedResolved();
105
106 scheme = extractScheme(rawUri); // scheme removed
107 if (sep != '/') rawUri.replace(sep, '/'); // force slashes as separator
108 path = rawUri;
109 strPath = path.toString(); // for legacy code
110
111 if (!scheme.isEmpty())
112 {
113 if (defaultResourceClass == RC_NULL || App_FileSystem().knownScheme(scheme))
114 {
115 // Scheme is accepted as is.
116 return;
117 }
118 LOG_RES_WARNING("Unknown scheme \"%s\" for path \"%s\", using default scheme instead") << scheme << strPath;
119 }
120
121 // Attempt to guess the scheme by interpreting the path?
122 if (defaultResourceClass == RC_IMPLICIT)
123 {
124 defaultResourceClass = DD_GuessFileTypeFromFileName(strPath).defaultClass();
125 }
126
127 if (VALID_RESOURCECLASSID(defaultResourceClass))
128 {
129 FS1::Scheme &fsScheme = App_FileSystem().scheme(ResourceClass::classForId(defaultResourceClass).defaultScheme());
130 scheme = fsScheme.name();
131 }
132 }
133
134 String resolveSymbol(QStringRef const &symbol) const
135 {
136 if (!resolverFunc)
137 {
138 return symbol.toString();
139 }
140 return resolverFunc(symbol.toString());
141 }
142
143 inline String parseExpression(QStringRef const &expression) const
144 {
145 // Presently the expression consists of a single symbol.
146 return resolveSymbol(expression);
147 }
148
149 String resolve() const
150 {
151 LOG_AS("Uri::resolve");
152
153 String result;
154
155 // Keep scanning the path for embedded expressions.
156 QStringRef expression;
157 int expEnd = 0, expBegin;
158 while ((expBegin = strPath.indexOf('$', expEnd)) >= 0)
159 {
160 // Is the next char the start-of-expression character?
161 if (strPath.at(expBegin + 1) == '(')
162 {
163 // Copy everything up to the '$'.
164 result += strPath.mid(expEnd, expBegin - expEnd);
165
166 // Skip over the '$'.
167 ++expBegin;
168
169 // Find the end-of-expression character.
170 expEnd = strPath.indexOf(')', expBegin);
171 if (expEnd < 0)
172 {
173 LOG_RES_WARNING("Ignoring expression \"" + strPath + "\": "
174 "missing a closing ')'");
175 expEnd = strPath.length();
176 }
177
178 // Skip over the '('.
179 ++expBegin;
180
181 // The range of the expression substring is now known.
182 expression = strPath.midRef(expBegin, expEnd - expBegin);
183
184 result += parseExpression(expression);
185 }
186 else
187 {
188 // No - copy the '$' and continue.
189 result += '$';
190 }
191
192 ++expEnd;
193 }
194
195 // Copy anything remaining.
196 result += strPath.mid(expEnd);
197
198 return result;
199 }
200
201 Impl &operator = (Impl const &) = delete; // no assignment
202 };
203
Uri()204 Uri::Uri() : d(new Impl)
205 {}
206
Uri(String const & percentEncoded)207 Uri::Uri(String const &percentEncoded) : d(new Impl)
208 {
209 if (!percentEncoded.isEmpty())
210 {
211 setUri(percentEncoded, RC_IMPLICIT, '/');
212 }
213 }
214
Uri(String const & percentEncoded,resourceclassid_t defaultResourceClass,QChar sep)215 Uri::Uri(String const &percentEncoded, resourceclassid_t defaultResourceClass, QChar sep)
216 : d(new Impl)
217 {
218 if (!percentEncoded.isEmpty())
219 {
220 setUri(percentEncoded, defaultResourceClass, sep);
221 }
222 }
223
Uri(String const & scheme,Path const & path)224 Uri::Uri(String const &scheme, Path const &path) : d(new Impl)
225 {
226 setScheme(scheme);
227 setPath(path);
228 }
229
Uri(resourceclassid_t resClass,Path const & path)230 Uri::Uri(resourceclassid_t resClass, Path const &path) : d(new Impl)
231 {
232 setUri(path.toString(), resClass, path.separator());
233 }
234
Uri(Path const & path)235 Uri::Uri(Path const &path) : d(new Impl)
236 {
237 setPath(path);
238 }
239
Uri(char const * nullTerminatedCStr)240 Uri::Uri(char const *nullTerminatedCStr) : d(new Impl)
241 {
242 setUri(nullTerminatedCStr);
243 }
244
fromUserInput(char ** argv,int argc,bool (* knownScheme)(String name))245 Uri Uri::fromUserInput(char **argv, int argc, bool (*knownScheme) (String name))
246 {
247 Uri output;
248 if (argv)
249 {
250 // [0: <scheme>:<path>] or [0: <scheme>] or [0: <path>].
251 switch (argc)
252 {
253 case 1: {
254 // Try to extract the scheme and encode the rest of the path.
255 String rawUri(argv[0]);
256 int pos = rawUri.indexOf(':');
257 if (pos >= 0)
258 {
259 output.setScheme(rawUri.left(pos));
260 rawUri.remove(0, pos + 1);
261 output.setPath(Path::normalize(QString(QByteArray(rawUri.toUtf8()).toPercentEncoding())));
262 }
263 // Just a scheme name?
264 else if (knownScheme && knownScheme(rawUri))
265 {
266 output.setScheme(rawUri);
267 }
268 else
269 {
270 // Just a path.
271 output.setPath(Path::normalize(QString(QByteArray(rawUri.toUtf8()).toPercentEncoding())));
272 }
273 break; }
274
275 // [0: <scheme>, 1: <path>]
276 case 2:
277 // Assign the scheme and encode the path.
278 output.setScheme(argv[0]);
279 output.setPath(Path::normalize(QString(QByteArray(argv[1]).toPercentEncoding())));
280 break;
281
282 default: break;
283 }
284 }
285 return output;
286 }
287
Uri(Uri const & other)288 Uri::Uri(Uri const &other) : d(new Impl(*other.d))
289 {}
290
fromNativePath(NativePath const & path,resourceclassid_t defaultResourceClass)291 Uri Uri::fromNativePath(NativePath const &path, resourceclassid_t defaultResourceClass)
292 {
293 return Uri(path.expand().withSeparators('/'), defaultResourceClass);
294 }
295
fromNativeDirPath(NativePath const & nativeDirPath,resourceclassid_t defaultResourceClass)296 Uri Uri::fromNativeDirPath(NativePath const &nativeDirPath, resourceclassid_t defaultResourceClass)
297 {
298 // Uri follows the convention of having a slash at the end for directories.
299 return Uri(nativeDirPath.expand().withSeparators('/') + '/', defaultResourceClass);
300 }
301
isEmpty() const302 bool Uri::isEmpty() const
303 {
304 return d->path.isEmpty();
305 }
306
operator ==(Uri const & other) const307 bool Uri::operator == (Uri const &other) const
308 {
309 if (this == &other) return true;
310
311 // First, lets check if the scheme differs.
312 if (d->scheme.compareWithoutCase(other.d->scheme)) return false;
313
314 // We can skip resolving if the paths are identical.
315 if (d->path == other.d->path) return true;
316
317 // We must be able to resolve both paths to compare.
318 try
319 {
320 // Do not match partial paths.
321 if (resolvedRef().length() != other.resolvedRef().length()) return false;
322
323 return resolvedRef().compareWithoutCase(other.resolvedRef()) == 0;
324 }
325 catch (ResolveError const &)
326 {
327 // Ignore the error.
328 }
329 return false;
330 }
331
clear()332 Uri &Uri::clear()
333 {
334 d->path.clear();
335 d->strPath.clear();
336 d->scheme.clear();
337 d->clearCachedResolved();
338 return *this;
339 }
340
scheme() const341 String const &Uri::scheme() const
342 {
343 return d->scheme;
344 }
345
path() const346 Path const &Uri::path() const
347 {
348 return d->path;
349 }
350
schemeCStr() const351 char const *Uri::schemeCStr() const
352 {
353 return d->scheme.utf8CStr();
354 }
355
pathCStr() const356 char const *Uri::pathCStr() const
357 {
358 return d->strPath.utf8CStr();
359 }
360
schemeStr() const361 ddstring_s const *Uri::schemeStr() const
362 {
363 return d->scheme.toStr();
364 }
365
pathStr() const366 ddstring_s const *Uri::pathStr() const
367 {
368 return d->strPath.toStr();
369 }
370
resolved() const371 String Uri::resolved() const
372 {
373 return resolvedRef();
374 }
375
resolvedRef() const376 String const &Uri::resolvedRef() const
377 {
378 void *currentGame = (void *) (!App::appExists() || DoomsdayApp::game().isNull()? 0 : &DoomsdayApp::game());
379
380 #ifndef LIBDENG_DISABLE_URI_RESOLVE_CACHING
381 if (d->resolvedForGame && d->resolvedForGame == currentGame)
382 {
383 // We can just return the previously prepared resolved URI.
384 return d->resolvedPath.toStringRef();
385 }
386 #endif
387
388 d->clearCachedResolved();
389
390 // Keep a copy of this, we'll likely need it many, many times.
391 d->resolvedPath = d->resolve();
392
393 DENG2_ASSERT(d->resolvedPath.separator() == QChar('/'));
394
395 d->resolvedForGame = currentGame;
396
397 return d->resolvedPath.toStringRef();
398 }
399
setScheme(String newScheme)400 Uri &Uri::setScheme(String newScheme)
401 {
402 d->scheme = newScheme;
403 d->clearCachedResolved();
404 return *this;
405 }
406
setPath(Path const & newPath)407 Uri &Uri::setPath(Path const &newPath)
408 {
409 // Force to slashes.
410 d->path = newPath.withSeparators('/');
411
412 d->strPath = d->path.toStringRef(); // legacy support
413 d->clearCachedResolved();
414 return *this;
415 }
416
setPath(String newPath,QChar sep)417 Uri &Uri::setPath(String newPath, QChar sep)
418 {
419 return setPath(Path(newPath.trimmed(), sep));
420 }
421
setPath(char const * newPathUtf8,char sep)422 Uri &Uri::setPath(char const *newPathUtf8, char sep)
423 {
424 return setPath(Path(QString::fromUtf8(newPathUtf8).trimmed(), sep));
425 }
426
setUri(String rawUri,resourceclassid_t defaultResourceClass,QChar sep)427 Uri &Uri::setUri(String rawUri, resourceclassid_t defaultResourceClass, QChar sep)
428 {
429 LOG_AS("Uri::setUri");
430 d->parseRawUri(rawUri.trimmed(), sep, defaultResourceClass);
431 return *this;
432 }
433
compose(ComposeAsTextFlags compositionFlags,QChar sep) const434 String Uri::compose(ComposeAsTextFlags compositionFlags, QChar sep) const
435 {
436 String text;
437 if (!(compositionFlags & OmitScheme))
438 {
439 if (!d->scheme.isEmpty())
440 {
441 text += d->scheme + ":";
442 }
443 }
444 if (!(compositionFlags & OmitPath))
445 {
446 QString path = d->path.withSeparators(sep);
447 if (compositionFlags & DecodePath)
448 {
449 path = QByteArray::fromPercentEncoding(path.toUtf8());
450 }
451 text += path;
452 }
453 return text;
454 }
455
asText() const456 String Uri::asText() const
457 {
458 return compose(DefaultComposeAsTextFlags | DecodePath);
459 }
460
operator >>(Writer & to) const461 void Uri::operator >> (Writer &to) const
462 {
463 to << d->scheme << d->path;
464 }
465
operator <<(Reader & from)466 void Uri::operator << (Reader &from)
467 {
468 clear();
469
470 from >> d->scheme >> d->path;
471
472 d->strPath = d->path;
473 }
474
setResolverFunc(ResolverFunc resolver)475 void Uri::setResolverFunc(ResolverFunc resolver)
476 {
477 resolverFunc = resolver;
478 }
479
readUri(reader_s * reader,String defaultScheme)480 void Uri::readUri(reader_s *reader, String defaultScheme)
481 {
482 clear();
483
484 ddstring_t scheme;
485 Str_InitStd(&scheme);
486 Str_Read(&scheme, reader);
487
488 ddstring_t path;
489 Str_InitStd(&path);
490 Str_Read(&path, reader);
491
492 if (Str_IsEmpty(&scheme) && !defaultScheme.isEmpty())
493 {
494 Str_Set(&scheme, defaultScheme.toUtf8().constData());
495 }
496
497 setScheme(Str_Text(&scheme));
498 setPath (Str_Text(&path ));
499 }
500
writeUri(writer_s * writer,int omitComponents) const501 void Uri::writeUri(writer_s *writer, int omitComponents) const
502 {
503 if (omitComponents & UCF_SCHEME)
504 {
505 ddstring_t emptyString;
506 Str_InitStatic(&emptyString, "");
507 Str_Write(&emptyString, writer);
508 }
509 else
510 {
511 Str_Write(DualString(scheme()).toStrUtf8(), writer);
512 }
513 Str_Write(DualString(path()).toStrUtf8(), writer);
514 }
515
516 #ifdef _DEBUG
517 # if !defined (DENG_MOBILE)
518
LIBDENG_DEFINE_UNITTEST(Uri)519 LIBDENG_DEFINE_UNITTEST(Uri)
520 {
521 try
522 {
523 // Test emptiness.
524 {
525 Uri u;
526 DENG_ASSERT(u.isEmpty());
527 DENG_ASSERT(u.path().segmentCount() == 1);
528 }
529
530 // Test a zero-length path.
531 {
532 Uri u("", RC_NULL);
533 DENG_ASSERT(u.isEmpty());
534 DENG_ASSERT(u.path().segmentCount() == 1);
535 }
536
537 // Equality and copying.
538 {
539 Uri a("some/thing", RC_NULL);
540 Uri b("/other/thing", RC_NULL);
541
542 DENG_ASSERT(a != b);
543
544 Uri c = a;
545 DENG_ASSERT(c == a);
546 DENG_ASSERT(c.path().reverseSegment(1).toString() == "some");
547
548 b = a;
549 DENG_ASSERT(b == a);
550 //qDebug() << b.reverseSegment(1);
551 DENG_ASSERT(b.path().reverseSegment(1).toString() == "some");
552 }
553
554 // Swapping.
555 {
556 Uri a("a/b/c", RC_NULL);
557 Uri b("d/e", RC_NULL);
558
559 DENG_ASSERT(a.path().segmentCount() == 3);
560 DENG_ASSERT(a.path().reverseSegment(1).toString() == "b");
561
562 std::swap(a, b);
563
564 DENG_ASSERT(a.path().segmentCount() == 2);
565 DENG_ASSERT(a.path().reverseSegment(1).toString() == "d");
566 DENG_ASSERT(b.path().segmentCount() == 3);
567 DENG_ASSERT(b.path().reverseSegment(1).toString() == "b");
568 }
569
570 // Test a Windows style path with a drive plus file path.
571 {
572 Uri u("c:/something.ext", RC_NULL);
573 DENG_ASSERT(u.path().segmentCount() == 2);
574
575 DENG_ASSERT(u.path().reverseSegment(0).length() == 13);
576 DENG_ASSERT(u.path().reverseSegment(0).toString() == "something.ext");
577
578 DENG_ASSERT(u.path().reverseSegment(1).length() == 2);
579 DENG_ASSERT(u.path().reverseSegment(1).toString() == "c:");
580 }
581
582 // Test a Unix style path with a zero-length root node name.
583 {
584 Uri u("/something.ext", RC_NULL);
585 DENG_ASSERT(u.path().segmentCount() == 2);
586
587 DENG_ASSERT(u.path().reverseSegment(0).length() == 13);
588 DENG_ASSERT(u.path().reverseSegment(0).toString() == "something.ext");
589
590 DENG_ASSERT(u.path().reverseSegment(1).length() == 0);
591 DENG_ASSERT(u.path().reverseSegment(1).toString() == "");
592 }
593
594 // Test a relative directory.
595 {
596 Uri u("some/dir/structure/", RC_NULL);
597 DENG_ASSERT(u.path().segmentCount() == 3);
598
599 DENG_ASSERT(u.path().reverseSegment(0).length() == 9);
600 DENG_ASSERT(u.path().reverseSegment(0).toString() == "structure");
601
602 DENG_ASSERT(u.path().reverseSegment(1).length() == 3);
603 DENG_ASSERT(u.path().reverseSegment(1).toString() == "dir");
604
605 DENG_ASSERT(u.path().reverseSegment(2).length() == 4);
606 DENG_ASSERT(u.path().reverseSegment(2).toString() == "some");
607 }
608 }
609 catch (Error const &er)
610 {
611 qWarning() << er.asText();
612 return false;
613 }
614 return true;
615 }
616
617 LIBDENG_RUN_UNITTEST(Uri)
618
619 # endif // DENG_MOBILE
620 #endif // _DEBUG
621
622 } // namespace de
623