1 /** @file uri.cpp Universal Resource Identifier.
2  * @ingroup base
3  *
4  * @authors Copyright &copy; 2010-2013 Daniel Swanson <danij@dengine.net>
5  * @authors Copyright &copy; 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