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