QGIS API Documentation 3.30.0-'s-Hertogenbosch (f186b8efe0)
qgspathresolver.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgspathresolver.cpp
3 --------------------------------------
4 Date : February 2017
5 Copyright : (C) 2017 by Martin Dobias
6 Email : wonder dot sk at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
16#include "qgspathresolver.h"
18
19#include "qgis.h"
20#include "qgsapplication.h"
21#include <QDir>
22#include <QFileInfo>
23#include <QUrl>
24#include <QUuid>
25
26#if defined(Q_OS_WIN)
27#include <QRegularExpression>
28#endif
29
30typedef std::vector< std::pair< QString, std::function< QString( const QString & ) > > > CustomResolvers;
31Q_GLOBAL_STATIC( CustomResolvers, sCustomResolvers )
32Q_GLOBAL_STATIC( CustomResolvers, sCustomWriters )
33
34QgsPathResolver::QgsPathResolver( const QString &baseFileName, const QString &attachmentDir )
35 : mBaseFileName( baseFileName ), mAttachmentDir( attachmentDir )
36{
37}
38
39
40QString QgsPathResolver::readPath( const QString &f ) const
41{
42 QString filename = f;
43
44 const CustomResolvers customResolvers = *sCustomResolvers();
45 for ( const auto &resolver : customResolvers )
46 filename = resolver.second( filename );
47
48 if ( filename.isEmpty() )
49 return QString();
50
51 QString src = filename;
52 if ( src.startsWith( QLatin1String( "inbuilt:" ) ) )
53 {
54 // strip away "inbuilt:" prefix, replace with actual inbuilt data folder path
55 return QgsApplication::pkgDataPath() + QStringLiteral( "/resources" ) + src.mid( 8 );
56 }
57
58 if ( src.startsWith( QLatin1String( "localized:" ) ) )
59 {
60 QStringList parts = src.split( "|" );
61 // strip away "localized:" prefix, replace with actual inbuilt data folder path
62 parts[0] = QgsApplication::localizedDataPathRegistry()->globalPath( parts[0].mid( 10 ) ) ;
63 if ( !parts[0].isEmpty() )
64 {
65 return parts.join( "|" );
66 }
67 else
68 {
69 return QString();
70 }
71 }
72 if ( src.startsWith( QLatin1String( "attachment:" ) ) )
73 {
74 // resolve attachment w.r.t. temporary path where project archive is extracted
75 return QDir( mAttachmentDir ).absoluteFilePath( src.mid( 11 ) );
76 }
77
78 if ( mBaseFileName.isNull() )
79 {
80 return src;
81 }
82
83 // if this is a VSIFILE, remove the VSI prefix and append to final result
84 QString vsiPrefix = qgsVsiPrefix( src );
85 if ( ! vsiPrefix.isEmpty() )
86 {
87 // unfortunately qgsVsiPrefix returns prefix also for files like "/x/y/z.gz"
88 // so we need to check if we really have the prefix
89 if ( src.startsWith( QLatin1String( "/vsi" ), Qt::CaseInsensitive ) )
90 src.remove( 0, vsiPrefix.size() );
91 else
92 vsiPrefix.clear();
93 }
94
95 // relative path should always start with ./ or ../
96 if ( !src.startsWith( QLatin1String( "./" ) ) && !src.startsWith( QLatin1String( "../" ) ) )
97 {
98#if defined(Q_OS_WIN)
99 if ( src.startsWith( "\\\\" ) ||
100 src.startsWith( "//" ) ||
101 ( src[0].isLetter() && src[1] == ':' ) )
102 {
103 // UNC or absolute path
104 return vsiPrefix + src;
105 }
106#else
107 if ( src[0] == '/' )
108 {
109 // absolute path
110 return vsiPrefix + src;
111 }
112#endif
113
114 // so this one isn't absolute, but also doesn't start // with ./ or ../.
115 // That means that it was saved with an earlier version of "relative path support",
116 // where the source file had to exist and only the project directory was stripped
117 // from the filename.
118
119 const QFileInfo pfi( mBaseFileName );
120 const QString home = pfi.absolutePath();
121 if ( home.isEmpty() )
122 return vsiPrefix + src;
123
124 const QFileInfo fi( home + '/' + src );
125
126 if ( !fi.exists() )
127 {
128 return vsiPrefix + src;
129 }
130 else
131 {
132 return vsiPrefix + fi.canonicalFilePath();
133 }
134 }
135
136 QString srcPath = src;
137 QString projPath = mBaseFileName;
138
139 if ( projPath.isEmpty() )
140 {
141 return vsiPrefix + src;
142 }
143
144#if defined(Q_OS_WIN)
145
146 // delimiter saved with pre 3.2x QGIS versions might be unencoded
147 thread_local const QRegularExpression delimiterRe( R"re(delimiter=([^&]+))re" );
148 const QRegularExpressionMatch match = delimiterRe.match( srcPath );
149 if ( match.hasMatch() )
150 {
151 const QString delimiter = match.captured( 0 ).replace( '\\', QLatin1String( "%5C" ) );
152 srcPath.replace( match.captured( 0 ), delimiter );
153 }
154
155 srcPath.replace( '\\', '/' );
156 projPath.replace( '\\', '/' );
157
158 bool uncPath = projPath.startsWith( "//" );
159#endif
160
161 // Make sure the path is absolute (see GH #33200)
162 projPath = QFileInfo( projPath ).absoluteFilePath();
163
164#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
165 QStringList srcElems = srcPath.split( '/', QString::SkipEmptyParts );
166 QStringList projElems = projPath.split( '/', QString::SkipEmptyParts );
167#else
168 const QStringList srcElems = srcPath.split( '/', Qt::SkipEmptyParts );
169 QStringList projElems = projPath.split( '/', Qt::SkipEmptyParts );
170#endif
171
172#if defined(Q_OS_WIN)
173 if ( uncPath )
174 {
175 projElems.insert( 0, "" );
176 projElems.insert( 0, "" );
177 }
178#endif
179
180 // remove project file element
181 projElems.removeLast();
182
183 // append source path elements
184 projElems << srcElems;
185 projElems.removeAll( QStringLiteral( "." ) );
186
187 // resolve ..
188 int pos;
189 while ( ( pos = projElems.indexOf( QLatin1String( ".." ) ) ) > 0 )
190 {
191 // remove preceding element and ..
192 projElems.removeAt( pos - 1 );
193 projElems.removeAt( pos - 1 );
194 }
195
196#if !defined(Q_OS_WIN)
197 // make path absolute
198 projElems.prepend( QString() );
199#endif
200
201 return vsiPrefix + projElems.join( QLatin1Char( '/' ) );
202}
203
204QString QgsPathResolver::setPathPreprocessor( const std::function<QString( const QString & )> &processor )
205{
206 QString id = QUuid::createUuid().toString();
207 sCustomResolvers()->emplace_back( std::make_pair( id, processor ) );
208 return id;
209}
210
212{
213 const size_t prevCount = sCustomResolvers()->size();
214 sCustomResolvers()->erase( std::remove_if( sCustomResolvers()->begin(), sCustomResolvers()->end(), [id]( std::pair< QString, std::function< QString( const QString & ) > > &a )
215 {
216 return a.first == id;
217 } ), sCustomResolvers()->end() );
218 return prevCount != sCustomResolvers()->size();
219}
220
221QString QgsPathResolver::setPathWriter( const std::function<QString( const QString & )> &writer )
222{
223 QString id = QUuid::createUuid().toString();
224 sCustomWriters()->emplace_back( std::make_pair( id, writer ) );
225 return id;
226}
227
228bool QgsPathResolver::removePathWriter( const QString &id )
229{
230 const size_t prevCount = sCustomWriters->size();
231 sCustomWriters()->erase( std::remove_if( sCustomWriters->begin(), sCustomWriters->end(), [id]( std::pair< QString, std::function< QString( const QString & ) > > &a )
232 {
233 return a.first == id;
234 } ), sCustomWriters->end() );
235 return prevCount != sCustomWriters->size();
236}
237
238QString QgsPathResolver::writePath( const QString &s ) const
239{
240 QString src = s;
241 if ( src.isEmpty() )
242 {
243 return src;
244 }
245
246 const QString localizedPath = QgsApplication::localizedDataPathRegistry()->localizedPath( src );
247 if ( !localizedPath.isEmpty() )
248 return QStringLiteral( "localized:" ) + localizedPath;
249
250 const CustomResolvers customWriters = *sCustomWriters();
251 for ( const auto &writer : customWriters )
252 src = writer.second( src );
253
254 if ( src.startsWith( QgsApplication::pkgDataPath() + QStringLiteral( "/resources" ) ) )
255 {
256 // replace inbuilt data folder path with "inbuilt:" prefix
257 return QStringLiteral( "inbuilt:" ) + src.mid( QgsApplication::pkgDataPath().length() + 10 );
258 }
259
260 if ( !mAttachmentDir.isEmpty() && src.startsWith( mAttachmentDir ) )
261 {
262 // Replace attachment dir with "attachment:" prefix
263 return QStringLiteral( "attachment:" ) + QFileInfo( src ).fileName();
264 }
265
266 if ( mBaseFileName.isEmpty() )
267 {
268 return src;
269 }
270
271 // Get projPath even if project has not been created yet
272 const QFileInfo pfi( QFileInfo( mBaseFileName ).path() );
273 QString projPath = pfi.canonicalFilePath();
274
275 // If project directory doesn't exit, fallback to absoluteFilePath : symbolic
276 // links won't be handled correctly, but that's OK as the path is "virtual".
277 if ( projPath.isEmpty() )
278 projPath = pfi.absoluteFilePath();
279
280 if ( projPath.isEmpty() )
281 {
282 return src;
283 }
284
285 // Check if it is a publicSource uri and clean it
286 const QUrl url { src };
287 QString srcPath { src };
288 QString urlQuery;
289
290 if ( url.isLocalFile( ) )
291 {
292 srcPath = url.path();
293 urlQuery = url.query();
294 }
295
296 const QFileInfo srcFileInfo( srcPath );
297 if ( srcFileInfo.exists() )
298 srcPath = srcFileInfo.canonicalFilePath();
299
300 // if this is a VSIFILE, remove the VSI prefix and append to final result
301 const QString vsiPrefix = qgsVsiPrefix( src );
302 if ( ! vsiPrefix.isEmpty() )
303 {
304 srcPath.remove( 0, vsiPrefix.size() );
305 }
306
307#if defined( Q_OS_WIN )
308 const Qt::CaseSensitivity cs = Qt::CaseInsensitive;
309
310 srcPath.replace( '\\', '/' );
311
312 if ( srcPath.startsWith( "//" ) )
313 {
314 // keep UNC prefix
315 srcPath = "\\\\" + srcPath.mid( 2 );
316 }
317
318 projPath.replace( '\\', '/' );
319 if ( projPath.startsWith( "//" ) )
320 {
321 // keep UNC prefix
322 projPath = "\\\\" + projPath.mid( 2 );
323 }
324#else
325 const Qt::CaseSensitivity cs = Qt::CaseSensitive;
326#endif
327
328#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
329 QStringList projElems = projPath.split( '/', QString::SkipEmptyParts );
330 QStringList srcElems = srcPath.split( '/', QString::SkipEmptyParts );
331#else
332 QStringList projElems = projPath.split( '/', Qt::SkipEmptyParts );
333 QStringList srcElems = srcPath.split( '/', Qt::SkipEmptyParts );
334#endif
335
336 projElems.removeAll( QStringLiteral( "." ) );
337 srcElems.removeAll( QStringLiteral( "." ) );
338
339 // remove common part
340 int n = 0;
341 while ( !srcElems.isEmpty() &&
342 !projElems.isEmpty() &&
343 srcElems[0].compare( projElems[0], cs ) == 0 )
344 {
345 srcElems.removeFirst();
346 projElems.removeFirst();
347 n++;
348 }
349
350 if ( n == 0 )
351 {
352 // no common parts; might not even be a file
353 return src;
354 }
355
356 if ( !projElems.isEmpty() )
357 {
358 // go up to the common directory
359 for ( int i = 0; i < projElems.size(); i++ )
360 {
361 srcElems.insert( 0, QStringLiteral( ".." ) );
362 }
363 }
364 else
365 {
366 // let it start with . nevertheless,
367 // so relative path always start with either ./ or ../
368 srcElems.insert( 0, QStringLiteral( "." ) );
369 }
370
371 // Append url query if any
372 QString returnPath { vsiPrefix + srcElems.join( QLatin1Char( '/' ) ) };
373 if ( ! urlQuery.isEmpty() )
374 {
375 returnPath.append( '?' );
376 returnPath.append( urlQuery );
377 }
378 return returnPath;
379}
static QString pkgDataPath()
Returns the common root path of all application data directories.
static QgsLocalizedDataPathRegistry * localizedDataPathRegistry()
Returns the registry of data repositories These are used as paths for basemaps, logos,...
QString globalPath(const QString &localizedPath) const
Returns the global path if the file has been found in one of the paths, an empty string otherwise.
QString localizedPath(const QString &globalPath) const
Returns the localized path if the file has been found in one of the path, an empty string otherwise.
Resolves relative paths into absolute paths and vice versa.
QString writePath(const QString &filename) const
Prepare a filename to save it to the project file.
static bool removePathPreprocessor(const QString &id)
Removes the custom pre-processor function with matching id.
static QString setPathPreprocessor(const std::function< QString(const QString &filename)> &processor)
Sets a path pre-processor function, which allows for manipulation of paths and data sources prior to ...
QString readPath(const QString &filename) const
Turn filename read from the project file to an absolute path.
static bool removePathWriter(const QString &id)
Removes the custom writer function with matching id.
static QString setPathWriter(const std::function< QString(const QString &filename)> &writer)
Sets a path writer function, which allows for manipulation of paths and data sources prior to writing...
QString qgsVsiPrefix(const QString &path)
Definition: qgis.cpp:192
Q_GLOBAL_STATIC(QReadWriteLock, sDefinitionCacheLock)
std::vector< std::pair< QString, std::function< QString(const QString &) > > > CustomResolvers