QGIS API Documentation 3.99.0-Master (d270888f95f)
Loading...
Searching...
No Matches
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"
17
18#include "qgis.h"
19#include "qgsapplication.h"
20#include "qgsgdalutils.h"
22
23#include <QDir>
24#include <QFileInfo>
25#include <QString>
26#include <QUrl>
27#include <QUuid>
28
29using namespace Qt::StringLiterals;
30
31#if defined(Q_OS_WIN)
32#include <QRegularExpression>
33#endif
34
35typedef std::vector< std::pair< QString, std::function< QString( const QString & ) > > > CustomResolvers;
36Q_GLOBAL_STATIC( CustomResolvers, sCustomResolvers )
37Q_GLOBAL_STATIC( CustomResolvers, sCustomWriters )
38
39QgsPathResolver::QgsPathResolver( const QString &baseFileName, const QString &attachmentDir )
40 : mBaseFileName( baseFileName ), mAttachmentDir( attachmentDir )
41{
42}
43
44
45QString QgsPathResolver::readPath( const QString &f ) const
46{
47 QString filename = f;
48
49 const CustomResolvers customResolvers = *sCustomResolvers();
50 for ( const auto &resolver : customResolvers )
51 filename = resolver.second( filename );
52
53 if ( filename.isEmpty() )
54 return QString();
55
56 QString src = filename;
57 if ( src.startsWith( "inbuilt:"_L1 ) )
58 {
59 // strip away "inbuilt:" prefix, replace with actual inbuilt data folder path
60 return QgsApplication::pkgDataPath() + u"/resources"_s + src.mid( 8 );
61 }
62
63 if ( src.startsWith( "localized:"_L1 ) )
64 {
65 QStringList parts = src.split( "|" );
66 // strip away "localized:" prefix, replace with actual inbuilt data folder path
67 parts[0] = QgsApplication::localizedDataPathRegistry()->globalPath( parts[0].mid( 10 ) ) ;
68 if ( !parts[0].isEmpty() )
69 {
70 return parts.join( "|" );
71 }
72 else
73 {
74 return QString();
75 }
76 }
77 if ( src.startsWith( "attachment:"_L1 ) )
78 {
79 // resolve attachment w.r.t. temporary path where project archive is extracted
80 return QDir( mAttachmentDir ).absoluteFilePath( src.mid( 11 ) );
81 }
82
83 if ( mBaseFileName.isNull() )
84 {
85 return src;
86 }
87
88 // if this is a VSIFILE, remove the VSI prefix and append to final result
89 QString vsiPrefix = QgsGdalUtils::vsiPrefixForPath( src );
90 if ( ! vsiPrefix.isEmpty() )
91 {
92 // unfortunately qgsVsiPrefix returns prefix also for files like "/x/y/z.gz"
93 // so we need to check if we really have the prefix
94 if ( src.startsWith( "/vsi"_L1, Qt::CaseInsensitive ) )
95 src.remove( 0, vsiPrefix.size() );
96 else
97 vsiPrefix.clear();
98 }
99
100 // relative path should always start with ./ or ../
101 if ( !src.startsWith( "./"_L1 ) && !src.startsWith( "../"_L1 ) )
102 {
103#if defined(Q_OS_WIN)
104 if ( src.startsWith( "\\\\" ) ||
105 src.startsWith( "//" ) ||
106 ( src[0].isLetter() && src[1] == ':' ) )
107 {
108 // UNC or absolute path
109 return vsiPrefix + src;
110 }
111#else
112 if ( src[0] == '/' )
113 {
114 // absolute path
115 return vsiPrefix + src;
116 }
117#endif
118
119 // so this one isn't absolute, but also doesn't start // with ./ or ../.
120 // That means that it was saved with an earlier version of "relative path support",
121 // where the source file had to exist and only the project directory was stripped
122 // from the filename.
123
124 const QFileInfo pfi( mBaseFileName );
125 const QString home = pfi.absolutePath();
126 if ( home.isEmpty() )
127 return vsiPrefix + src;
128
129 const QFileInfo fi( home + '/' + src );
130
131 if ( !fi.exists() )
132 {
133 return vsiPrefix + src;
134 }
135 else
136 {
137 return vsiPrefix + QDir::cleanPath( fi.absoluteFilePath() );
138 }
139 }
140
141 QString srcPath = src;
142 QString projPath = mBaseFileName;
143
144 if ( projPath.isEmpty() )
145 {
146 return vsiPrefix + src;
147 }
148
149#if defined(Q_OS_WIN)
150
151 // delimiter saved with pre 3.2x QGIS versions might be unencoded
152 thread_local const QRegularExpression delimiterRe( R"re(delimiter=([^&]+))re" );
153 const QRegularExpressionMatch match = delimiterRe.match( srcPath );
154 if ( match.hasMatch() )
155 {
156 const QString delimiter = match.captured( 0 ).replace( '\\', "%5C"_L1 );
157 srcPath.replace( match.captured( 0 ), delimiter );
158 }
159
160 srcPath.replace( '\\', '/' );
161 projPath.replace( '\\', '/' );
162
163 bool uncPath = projPath.startsWith( "//" );
164#endif
165
166 // Make sure the path is absolute (see GH #33200)
167 projPath = QFileInfo( projPath ).absoluteFilePath();
168
169 const QStringList srcElems = srcPath.split( '/', Qt::SkipEmptyParts );
170 QStringList projElems = projPath.split( '/', Qt::SkipEmptyParts );
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( u"."_s );
186
187 // resolve ..
188 int pos;
189 while ( ( pos = projElems.indexOf( ".."_L1 ) ) > 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( '/'_L1 );
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 u"localized:"_s + 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() + u"/resources"_s ) )
255 {
256 // replace inbuilt data folder path with "inbuilt:" prefix
257 return u"inbuilt:"_s + 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 u"attachment:"_s + 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 // readPath does not resolve symlink, so writePath should not either
274 QString projPath = pfi.absoluteFilePath();
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 // Guard against relative paths: If srcPath is already relative, QFileInfo will match
299 // files in the working directory, instead of project directory. Avoid by returning early.
300 if ( !srcFileInfo.isAbsolute() )
301 {
302 return srcPath;
303 }
304 if ( srcFileInfo.exists() )
305 // Do NOT resolve symlinks, but do remove '..' and '.'
306 srcPath = QDir::cleanPath( srcFileInfo.absoluteFilePath() );
307
308 // if this is a VSIFILE, remove the VSI prefix and append to final result
309 const QString vsiPrefix = QgsGdalUtils::vsiPrefixForPath( src );
310 if ( ! vsiPrefix.isEmpty() )
311 {
312 srcPath.remove( 0, vsiPrefix.size() );
313 }
314
315#if defined( Q_OS_WIN )
316 const Qt::CaseSensitivity cs = Qt::CaseInsensitive;
317
318 srcPath.replace( '\\', '/' );
319
320 if ( srcPath.startsWith( "//" ) )
321 {
322 // keep UNC prefix
323 srcPath = "\\\\" + srcPath.mid( 2 );
324 }
325
326 projPath.replace( '\\', '/' );
327 if ( projPath.startsWith( "//" ) )
328 {
329 // keep UNC prefix
330 projPath = "\\\\" + projPath.mid( 2 );
331 }
332#else
333 const Qt::CaseSensitivity cs = Qt::CaseSensitive;
334#endif
335
336 QStringList projElems = projPath.split( '/', Qt::SkipEmptyParts );
337 QStringList srcElems = srcPath.split( '/', Qt::SkipEmptyParts );
338
339 projElems.removeAll( u"."_s );
340 srcElems.removeAll( u"."_s );
341
342 // remove common part
343 int n = 0;
344 while ( !srcElems.isEmpty() &&
345 !projElems.isEmpty() &&
346 srcElems[0].compare( projElems[0], cs ) == 0 )
347 {
348 srcElems.removeFirst();
349 projElems.removeFirst();
350 n++;
351 }
352
353 if ( n == 0 )
354 {
355 // no common parts; might not even be a file
356 return src;
357 }
358
359 if ( !projElems.isEmpty() )
360 {
361 // go up to the common directory
362 for ( int i = 0; i < projElems.size(); i++ )
363 {
364 srcElems.insert( 0, u".."_s );
365 }
366 }
367 else
368 {
369 // let it start with . nevertheless,
370 // so relative path always start with either ./ or ../
371 srcElems.insert( 0, u"."_s );
372 }
373
374 // Append url query if any
375 QString returnPath { vsiPrefix + srcElems.join( '/'_L1 ) };
376 if ( ! urlQuery.isEmpty() )
377 {
378 returnPath.append( '?' );
379 returnPath.append( urlQuery );
380 }
381 return returnPath;
382}
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.
QString writePath(const QString &filename) const
Prepare a filename to save it to the project file.
QgsPathResolver(const QString &baseFileName=QString(), const QString &attachmentDir=QString())
Initialize path resolver with a base filename. Null filename means no conversion between relative/abs...
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