QGIS API Documentation 4.1.0-Master (60fea48833c)
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 )
41 , mAttachmentDir( attachmentDir )
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( "\\\\" ) || src.startsWith( "//" ) || ( src[0].isLetter() && src[1] == ':' ) )
105 {
106 // UNC or absolute path
107 return vsiPrefix + src;
108 }
109#else
110 if ( src[0] == '/' )
111 {
112 // absolute path
113 return vsiPrefix + src;
114 }
115#endif
116
117 // so this one isn't absolute, but also doesn't start // with ./ or ../.
118 // That means that it was saved with an earlier version of "relative path support",
119 // where the source file had to exist and only the project directory was stripped
120 // from the filename.
121
122 const QFileInfo pfi( mBaseFileName );
123 const QString home = pfi.absolutePath();
124 if ( home.isEmpty() )
125 return vsiPrefix + src;
126
127 const QFileInfo fi( home + '/' + src );
128
129 if ( !fi.exists() )
130 {
131 return vsiPrefix + src;
132 }
133 else
134 {
135 return vsiPrefix + QDir::cleanPath( fi.absoluteFilePath() );
136 }
137 }
138
139 QString srcPath = src;
140 QString projPath = mBaseFileName;
141
142 if ( projPath.isEmpty() )
143 {
144 return vsiPrefix + src;
145 }
146
147#if defined( Q_OS_WIN )
148
149 // delimiter saved with pre 3.2x QGIS versions might be unencoded
150 thread_local const QRegularExpression delimiterRe( R"re(delimiter=([^&]+))re" );
151 const QRegularExpressionMatch match = delimiterRe.match( srcPath );
152 if ( match.hasMatch() )
153 {
154 const QString delimiter = match.captured( 0 ).replace( '\\', "%5C"_L1 );
155 srcPath.replace( match.captured( 0 ), delimiter );
156 }
157
158 srcPath.replace( '\\', '/' );
159 projPath.replace( '\\', '/' );
160
161 bool uncPath = projPath.startsWith( "//" );
162#endif
163
164 // Make sure the path is absolute (see GH #33200)
165 projPath = QFileInfo( projPath ).absoluteFilePath();
166
167 const QStringList srcElems = srcPath.split( '/', Qt::SkipEmptyParts );
168 QStringList projElems = projPath.split( '/', Qt::SkipEmptyParts );
169
170#if defined( Q_OS_WIN )
171 if ( uncPath )
172 {
173 projElems.insert( 0, "" );
174 projElems.insert( 0, "" );
175 }
176#endif
177
178 // remove project file element
179 projElems.removeLast();
180
181 // append source path elements
182 projElems << srcElems;
183 projElems.removeAll( u"."_s );
184
185 // resolve ..
186 int pos;
187 while ( ( pos = projElems.indexOf( ".."_L1 ) ) > 0 )
188 {
189 // remove preceding element and ..
190 projElems.removeAt( pos - 1 );
191 projElems.removeAt( pos - 1 );
192 }
193
194#if !defined( Q_OS_WIN )
195 // make path absolute
196 projElems.prepend( QString() );
197#endif
198
199 return vsiPrefix + projElems.join( '/'_L1 );
200}
201
202QString QgsPathResolver::setPathPreprocessor( const std::function<QString( const QString & )> &processor )
203{
204 QString id = QUuid::createUuid().toString();
205 sCustomResolvers()->emplace_back( std::make_pair( id, processor ) );
206 return id;
207}
208
210{
211 const size_t prevCount = sCustomResolvers()->size();
212 sCustomResolvers()->erase(
213 std::remove_if( sCustomResolvers()->begin(), sCustomResolvers()->end(), [id]( std::pair< QString, std::function< QString( const QString & ) > > &a ) { return a.first == id; } ),
214 sCustomResolvers()->end()
215 );
216 return prevCount != sCustomResolvers()->size();
217}
218
219QString QgsPathResolver::setPathWriter( const std::function<QString( const QString & )> &writer )
220{
221 QString id = QUuid::createUuid().toString();
222 sCustomWriters()->emplace_back( std::make_pair( id, writer ) );
223 return id;
224}
225
226bool QgsPathResolver::removePathWriter( const QString &id )
227{
228 const size_t prevCount = sCustomWriters()->size();
229 sCustomWriters()->erase(
230 std::remove_if( sCustomWriters()->begin(), sCustomWriters()->end(), [id]( std::pair< QString, std::function< QString( const QString & ) > > &a ) { return a.first == id; } ), sCustomWriters()->end()
231 );
232 return prevCount != sCustomWriters()->size();
233}
234
235QString QgsPathResolver::writePath( const QString &s ) const
236{
237 QString src = s;
238 if ( src.isEmpty() )
239 {
240 return src;
241 }
242
243 const QString localizedPath = QgsApplication::localizedDataPathRegistry()->localizedPath( src );
244 if ( !localizedPath.isEmpty() )
245 return u"localized:"_s + localizedPath;
246
247 const CustomResolvers customWriters = *sCustomWriters();
248 for ( const auto &writer : customWriters )
249 src = writer.second( src );
250
251 if ( src.startsWith( QgsApplication::pkgDataPath() + u"/resources"_s ) )
252 {
253 // replace inbuilt data folder path with "inbuilt:" prefix
254 return u"inbuilt:"_s + src.mid( QgsApplication::pkgDataPath().length() + 10 );
255 }
256
257 if ( !mAttachmentDir.isEmpty() && src.startsWith( mAttachmentDir ) )
258 {
259 // Replace attachment dir with "attachment:" prefix
260 return u"attachment:"_s + QFileInfo( src ).fileName();
261 }
262
263 if ( mBaseFileName.isEmpty() )
264 {
265 return src;
266 }
267
268 // Get projPath even if project has not been created yet
269 const QFileInfo pfi( QFileInfo( mBaseFileName ).path() );
270 // readPath does not resolve symlink, so writePath should not either
271 QString projPath = pfi.absoluteFilePath();
272
273 // If project directory doesn't exit, fallback to absoluteFilePath : symbolic
274 // links won't be handled correctly, but that's OK as the path is "virtual".
275 if ( projPath.isEmpty() )
276 projPath = pfi.absoluteFilePath();
277
278 if ( projPath.isEmpty() )
279 {
280 return src;
281 }
282
283 // Check if it is a publicSource uri and clean it
284 const QUrl url { src };
285 QString srcPath { src };
286 QString urlQuery;
287
288 if ( url.isLocalFile() )
289 {
290 srcPath = url.path();
291 urlQuery = url.query();
292 }
293
294 const QFileInfo srcFileInfo( srcPath );
295 // Guard against relative paths: If srcPath is already relative, QFileInfo will match
296 // files in the working directory, instead of project directory. Avoid by returning early.
297 if ( !srcFileInfo.isAbsolute() )
298 {
299 return srcPath;
300 }
301 if ( srcFileInfo.exists() )
302 // Do NOT resolve symlinks, but do remove '..' and '.'
303 srcPath = QDir::cleanPath( srcFileInfo.absoluteFilePath() );
304
305 // if this is a VSIFILE, remove the VSI prefix and append to final result
306 const QString vsiPrefix = QgsGdalUtils::vsiPrefixForPath( src );
307 if ( !vsiPrefix.isEmpty() )
308 {
309 srcPath.remove( 0, vsiPrefix.size() );
310 }
311
312#if defined( Q_OS_WIN )
313 const Qt::CaseSensitivity cs = Qt::CaseInsensitive;
314
315 srcPath.replace( '\\', '/' );
316
317 if ( srcPath.startsWith( "//" ) )
318 {
319 // keep UNC prefix
320 srcPath = "\\\\" + srcPath.mid( 2 );
321 }
322
323 projPath.replace( '\\', '/' );
324 if ( projPath.startsWith( "//" ) )
325 {
326 // keep UNC prefix
327 projPath = "\\\\" + projPath.mid( 2 );
328 }
329#else
330 const Qt::CaseSensitivity cs = Qt::CaseSensitive;
331#endif
332
333 QStringList projElems = projPath.split( '/', Qt::SkipEmptyParts );
334 QStringList srcElems = srcPath.split( '/', Qt::SkipEmptyParts );
335
336 projElems.removeAll( u"."_s );
337 srcElems.removeAll( u"."_s );
338
339 // remove common part
340 int n = 0;
341 while ( !srcElems.isEmpty() && !projElems.isEmpty() && srcElems[0].compare( projElems[0], cs ) == 0 )
342 {
343 srcElems.removeFirst();
344 projElems.removeFirst();
345 n++;
346 }
347
348 if ( n == 0 )
349 {
350 // no common parts; might not even be a file
351 return src;
352 }
353
354 if ( !projElems.isEmpty() )
355 {
356 // go up to the common directory
357 for ( int i = 0; i < projElems.size(); i++ )
358 {
359 srcElems.insert( 0, u".."_s );
360 }
361 }
362 else
363 {
364 // let it start with . nevertheless,
365 // so relative path always start with either ./ or ../
366 srcElems.insert( 0, u"."_s );
367 }
368
369 // Append url query if any
370 QString returnPath { vsiPrefix + srcElems.join( '/'_L1 ) };
371 if ( !urlQuery.isEmpty() )
372 {
373 returnPath.append( '?' );
374 returnPath.append( urlQuery );
375 }
376 return returnPath;
377}
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