QGIS API Documentation 3.37.0-Master (fdefdf9c27f)
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 const QStringList srcElems = srcPath.split( '/', Qt::SkipEmptyParts );
166 QStringList projElems = projPath.split( '/', Qt::SkipEmptyParts );
167
168#if defined(Q_OS_WIN)
169 if ( uncPath )
170 {
171 projElems.insert( 0, "" );
172 projElems.insert( 0, "" );
173 }
174#endif
175
176 // remove project file element
177 projElems.removeLast();
178
179 // append source path elements
180 projElems << srcElems;
181 projElems.removeAll( QStringLiteral( "." ) );
182
183 // resolve ..
184 int pos;
185 while ( ( pos = projElems.indexOf( QLatin1String( ".." ) ) ) > 0 )
186 {
187 // remove preceding element and ..
188 projElems.removeAt( pos - 1 );
189 projElems.removeAt( pos - 1 );
190 }
191
192#if !defined(Q_OS_WIN)
193 // make path absolute
194 projElems.prepend( QString() );
195#endif
196
197 return vsiPrefix + projElems.join( QLatin1Char( '/' ) );
198}
199
200QString QgsPathResolver::setPathPreprocessor( const std::function<QString( const QString & )> &processor )
201{
202 QString id = QUuid::createUuid().toString();
203 sCustomResolvers()->emplace_back( std::make_pair( id, processor ) );
204 return id;
205}
206
208{
209 const size_t prevCount = sCustomResolvers()->size();
210 sCustomResolvers()->erase( std::remove_if( sCustomResolvers()->begin(), sCustomResolvers()->end(), [id]( std::pair< QString, std::function< QString( const QString & ) > > &a )
211 {
212 return a.first == id;
213 } ), sCustomResolvers()->end() );
214 return prevCount != sCustomResolvers()->size();
215}
216
217QString QgsPathResolver::setPathWriter( const std::function<QString( const QString & )> &writer )
218{
219 QString id = QUuid::createUuid().toString();
220 sCustomWriters()->emplace_back( std::make_pair( id, writer ) );
221 return id;
222}
223
224bool QgsPathResolver::removePathWriter( const QString &id )
225{
226 const size_t prevCount = sCustomWriters->size();
227 sCustomWriters()->erase( std::remove_if( sCustomWriters->begin(), sCustomWriters->end(), [id]( std::pair< QString, std::function< QString( const QString & ) > > &a )
228 {
229 return a.first == id;
230 } ), sCustomWriters->end() );
231 return prevCount != sCustomWriters->size();
232}
233
234QString QgsPathResolver::writePath( const QString &s ) const
235{
236 QString src = s;
237 if ( src.isEmpty() )
238 {
239 return src;
240 }
241
242 const QString localizedPath = QgsApplication::localizedDataPathRegistry()->localizedPath( src );
243 if ( !localizedPath.isEmpty() )
244 return QStringLiteral( "localized:" ) + localizedPath;
245
246 const CustomResolvers customWriters = *sCustomWriters();
247 for ( const auto &writer : customWriters )
248 src = writer.second( src );
249
250 if ( src.startsWith( QgsApplication::pkgDataPath() + QStringLiteral( "/resources" ) ) )
251 {
252 // replace inbuilt data folder path with "inbuilt:" prefix
253 return QStringLiteral( "inbuilt:" ) + src.mid( QgsApplication::pkgDataPath().length() + 10 );
254 }
255
256 if ( !mAttachmentDir.isEmpty() && src.startsWith( mAttachmentDir ) )
257 {
258 // Replace attachment dir with "attachment:" prefix
259 return QStringLiteral( "attachment:" ) + QFileInfo( src ).fileName();
260 }
261
262 if ( mBaseFileName.isEmpty() )
263 {
264 return src;
265 }
266
267 // Get projPath even if project has not been created yet
268 const QFileInfo pfi( QFileInfo( mBaseFileName ).path() );
269 QString projPath = pfi.canonicalFilePath();
270
271 // If project directory doesn't exit, fallback to absoluteFilePath : symbolic
272 // links won't be handled correctly, but that's OK as the path is "virtual".
273 if ( projPath.isEmpty() )
274 projPath = pfi.absoluteFilePath();
275
276 if ( projPath.isEmpty() )
277 {
278 return src;
279 }
280
281 // Check if it is a publicSource uri and clean it
282 const QUrl url { src };
283 QString srcPath { src };
284 QString urlQuery;
285
286 if ( url.isLocalFile( ) )
287 {
288 srcPath = url.path();
289 urlQuery = url.query();
290 }
291
292 const QFileInfo srcFileInfo( srcPath );
293 if ( srcFileInfo.exists() )
294 srcPath = srcFileInfo.canonicalFilePath();
295
296 // if this is a VSIFILE, remove the VSI prefix and append to final result
297 const QString vsiPrefix = QgsGdalUtils::vsiPrefixForPath( src );
298 if ( ! vsiPrefix.isEmpty() )
299 {
300 srcPath.remove( 0, vsiPrefix.size() );
301 }
302
303#if defined( Q_OS_WIN )
304 const Qt::CaseSensitivity cs = Qt::CaseInsensitive;
305
306 srcPath.replace( '\\', '/' );
307
308 if ( srcPath.startsWith( "//" ) )
309 {
310 // keep UNC prefix
311 srcPath = "\\\\" + srcPath.mid( 2 );
312 }
313
314 projPath.replace( '\\', '/' );
315 if ( projPath.startsWith( "//" ) )
316 {
317 // keep UNC prefix
318 projPath = "\\\\" + projPath.mid( 2 );
319 }
320#else
321 const Qt::CaseSensitivity cs = Qt::CaseSensitive;
322#endif
323
324 QStringList projElems = projPath.split( '/', Qt::SkipEmptyParts );
325 QStringList srcElems = srcPath.split( '/', Qt::SkipEmptyParts );
326
327 projElems.removeAll( QStringLiteral( "." ) );
328 srcElems.removeAll( QStringLiteral( "." ) );
329
330 // remove common part
331 int n = 0;
332 while ( !srcElems.isEmpty() &&
333 !projElems.isEmpty() &&
334 srcElems[0].compare( projElems[0], cs ) == 0 )
335 {
336 srcElems.removeFirst();
337 projElems.removeFirst();
338 n++;
339 }
340
341 if ( n == 0 )
342 {
343 // no common parts; might not even be a file
344 return src;
345 }
346
347 if ( !projElems.isEmpty() )
348 {
349 // go up to the common directory
350 for ( int i = 0; i < projElems.size(); i++ )
351 {
352 srcElems.insert( 0, QStringLiteral( ".." ) );
353 }
354 }
355 else
356 {
357 // let it start with . nevertheless,
358 // so relative path always start with either ./ or ../
359 srcElems.insert( 0, QStringLiteral( "." ) );
360 }
361
362 // Append url query if any
363 QString returnPath { vsiPrefix + srcElems.join( QLatin1Char( '/' ) ) };
364 if ( ! urlQuery.isEmpty() )
365 {
366 returnPath.append( '?' );
367 returnPath.append( urlQuery );
368 }
369 return returnPath;
370}
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