QGIS API Documentation 3.41.0-Master (ff097244f9c)
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"
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 + QDir::cleanPath( fi.absoluteFilePath() );
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 // readPath does not resolve symlink, so writePath should not either
270 QString projPath = pfi.absoluteFilePath();
271
272 // If project directory doesn't exit, fallback to absoluteFilePath : symbolic
273 // links won't be handled correctly, but that's OK as the path is "virtual".
274 if ( projPath.isEmpty() )
275 projPath = pfi.absoluteFilePath();
276
277 if ( projPath.isEmpty() )
278 {
279 return src;
280 }
281
282 // Check if it is a publicSource uri and clean it
283 const QUrl url { src };
284 QString srcPath { src };
285 QString urlQuery;
286
287 if ( url.isLocalFile( ) )
288 {
289 srcPath = url.path();
290 urlQuery = url.query();
291 }
292
293 const QFileInfo srcFileInfo( srcPath );
294 // Guard against relative paths: If srcPath is already relative, QFileInfo will match
295 // files in the working directory, instead of project directory. Avoid by returning early.
296 if ( !srcFileInfo.isAbsolute() )
297 {
298 return srcPath;
299 }
300 if ( srcFileInfo.exists() )
301 // Do NOT resolve symlinks, but do remove '..' and '.'
302 srcPath = QDir::cleanPath( srcFileInfo.absoluteFilePath() );
303
304 // if this is a VSIFILE, remove the VSI prefix and append to final result
305 const QString vsiPrefix = QgsGdalUtils::vsiPrefixForPath( src );
306 if ( ! vsiPrefix.isEmpty() )
307 {
308 srcPath.remove( 0, vsiPrefix.size() );
309 }
310
311#if defined( Q_OS_WIN )
312 const Qt::CaseSensitivity cs = Qt::CaseInsensitive;
313
314 srcPath.replace( '\\', '/' );
315
316 if ( srcPath.startsWith( "//" ) )
317 {
318 // keep UNC prefix
319 srcPath = "\\\\" + srcPath.mid( 2 );
320 }
321
322 projPath.replace( '\\', '/' );
323 if ( projPath.startsWith( "//" ) )
324 {
325 // keep UNC prefix
326 projPath = "\\\\" + projPath.mid( 2 );
327 }
328#else
329 const Qt::CaseSensitivity cs = Qt::CaseSensitive;
330#endif
331
332 QStringList projElems = projPath.split( '/', Qt::SkipEmptyParts );
333 QStringList srcElems = srcPath.split( '/', Qt::SkipEmptyParts );
334
335 projElems.removeAll( QStringLiteral( "." ) );
336 srcElems.removeAll( QStringLiteral( "." ) );
337
338 // remove common part
339 int n = 0;
340 while ( !srcElems.isEmpty() &&
341 !projElems.isEmpty() &&
342 srcElems[0].compare( projElems[0], cs ) == 0 )
343 {
344 srcElems.removeFirst();
345 projElems.removeFirst();
346 n++;
347 }
348
349 if ( n == 0 )
350 {
351 // no common parts; might not even be a file
352 return src;
353 }
354
355 if ( !projElems.isEmpty() )
356 {
357 // go up to the common directory
358 for ( int i = 0; i < projElems.size(); i++ )
359 {
360 srcElems.insert( 0, QStringLiteral( ".." ) );
361 }
362 }
363 else
364 {
365 // let it start with . nevertheless,
366 // so relative path always start with either ./ or ../
367 srcElems.insert( 0, QStringLiteral( "." ) );
368 }
369
370 // Append url query if any
371 QString returnPath { vsiPrefix + srcElems.join( QLatin1Char( '/' ) ) };
372 if ( ! urlQuery.isEmpty() )
373 {
374 returnPath.append( '?' );
375 returnPath.append( urlQuery );
376 }
377 return returnPath;
378}
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