QGIS API Documentation 3.99.0-Master (21b3aa880ba)
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 <QUrl>
26#include <QUuid>
27
28#if defined(Q_OS_WIN)
29#include <QRegularExpression>
30#endif
31
32typedef std::vector< std::pair< QString, std::function< QString( const QString & ) > > > CustomResolvers;
33Q_GLOBAL_STATIC( CustomResolvers, sCustomResolvers )
34Q_GLOBAL_STATIC( CustomResolvers, sCustomWriters )
35
36QgsPathResolver::QgsPathResolver( const QString &baseFileName, const QString &attachmentDir )
37 : mBaseFileName( baseFileName ), mAttachmentDir( attachmentDir )
38{
39}
40
41
42QString QgsPathResolver::readPath( const QString &f ) const
43{
44 QString filename = f;
45
46 const CustomResolvers customResolvers = *sCustomResolvers();
47 for ( const auto &resolver : customResolvers )
48 filename = resolver.second( filename );
49
50 if ( filename.isEmpty() )
51 return QString();
52
53 QString src = filename;
54 if ( src.startsWith( QLatin1String( "inbuilt:" ) ) )
55 {
56 // strip away "inbuilt:" prefix, replace with actual inbuilt data folder path
57 return QgsApplication::pkgDataPath() + QStringLiteral( "/resources" ) + src.mid( 8 );
58 }
59
60 if ( src.startsWith( QLatin1String( "localized:" ) ) )
61 {
62 QStringList parts = src.split( "|" );
63 // strip away "localized:" prefix, replace with actual inbuilt data folder path
64 parts[0] = QgsApplication::localizedDataPathRegistry()->globalPath( parts[0].mid( 10 ) ) ;
65 if ( !parts[0].isEmpty() )
66 {
67 return parts.join( "|" );
68 }
69 else
70 {
71 return QString();
72 }
73 }
74 if ( src.startsWith( QLatin1String( "attachment:" ) ) )
75 {
76 // resolve attachment w.r.t. temporary path where project archive is extracted
77 return QDir( mAttachmentDir ).absoluteFilePath( src.mid( 11 ) );
78 }
79
80 if ( mBaseFileName.isNull() )
81 {
82 return src;
83 }
84
85 // if this is a VSIFILE, remove the VSI prefix and append to final result
86 QString vsiPrefix = QgsGdalUtils::vsiPrefixForPath( src );
87 if ( ! vsiPrefix.isEmpty() )
88 {
89 // unfortunately qgsVsiPrefix returns prefix also for files like "/x/y/z.gz"
90 // so we need to check if we really have the prefix
91 if ( src.startsWith( QLatin1String( "/vsi" ), Qt::CaseInsensitive ) )
92 src.remove( 0, vsiPrefix.size() );
93 else
94 vsiPrefix.clear();
95 }
96
97 // relative path should always start with ./ or ../
98 if ( !src.startsWith( QLatin1String( "./" ) ) && !src.startsWith( QLatin1String( "../" ) ) )
99 {
100#if defined(Q_OS_WIN)
101 if ( src.startsWith( "\\\\" ) ||
102 src.startsWith( "//" ) ||
103 ( src[0].isLetter() && src[1] == ':' ) )
104 {
105 // UNC or absolute path
106 return vsiPrefix + src;
107 }
108#else
109 if ( src[0] == '/' )
110 {
111 // absolute path
112 return vsiPrefix + src;
113 }
114#endif
115
116 // so this one isn't absolute, but also doesn't start // with ./ or ../.
117 // That means that it was saved with an earlier version of "relative path support",
118 // where the source file had to exist and only the project directory was stripped
119 // from the filename.
120
121 const QFileInfo pfi( mBaseFileName );
122 const QString home = pfi.absolutePath();
123 if ( home.isEmpty() )
124 return vsiPrefix + src;
125
126 const QFileInfo fi( home + '/' + src );
127
128 if ( !fi.exists() )
129 {
130 return vsiPrefix + src;
131 }
132 else
133 {
134 return vsiPrefix + QDir::cleanPath( fi.absoluteFilePath() );
135 }
136 }
137
138 QString srcPath = src;
139 QString projPath = mBaseFileName;
140
141 if ( projPath.isEmpty() )
142 {
143 return vsiPrefix + src;
144 }
145
146#if defined(Q_OS_WIN)
147
148 // delimiter saved with pre 3.2x QGIS versions might be unencoded
149 thread_local const QRegularExpression delimiterRe( R"re(delimiter=([^&]+))re" );
150 const QRegularExpressionMatch match = delimiterRe.match( srcPath );
151 if ( match.hasMatch() )
152 {
153 const QString delimiter = match.captured( 0 ).replace( '\\', QLatin1String( "%5C" ) );
154 srcPath.replace( match.captured( 0 ), delimiter );
155 }
156
157 srcPath.replace( '\\', '/' );
158 projPath.replace( '\\', '/' );
159
160 bool uncPath = projPath.startsWith( "//" );
161#endif
162
163 // Make sure the path is absolute (see GH #33200)
164 projPath = QFileInfo( projPath ).absoluteFilePath();
165
166 const QStringList srcElems = srcPath.split( '/', Qt::SkipEmptyParts );
167 QStringList projElems = projPath.split( '/', Qt::SkipEmptyParts );
168
169#if defined(Q_OS_WIN)
170 if ( uncPath )
171 {
172 projElems.insert( 0, "" );
173 projElems.insert( 0, "" );
174 }
175#endif
176
177 // remove project file element
178 projElems.removeLast();
179
180 // append source path elements
181 projElems << srcElems;
182 projElems.removeAll( QStringLiteral( "." ) );
183
184 // resolve ..
185 int pos;
186 while ( ( pos = projElems.indexOf( QLatin1String( ".." ) ) ) > 0 )
187 {
188 // remove preceding element and ..
189 projElems.removeAt( pos - 1 );
190 projElems.removeAt( pos - 1 );
191 }
192
193#if !defined(Q_OS_WIN)
194 // make path absolute
195 projElems.prepend( QString() );
196#endif
197
198 return vsiPrefix + projElems.join( QLatin1Char( '/' ) );
199}
200
201QString QgsPathResolver::setPathPreprocessor( const std::function<QString( const QString & )> &processor )
202{
203 QString id = QUuid::createUuid().toString();
204 sCustomResolvers()->emplace_back( std::make_pair( id, processor ) );
205 return id;
206}
207
209{
210 const size_t prevCount = sCustomResolvers()->size();
211 sCustomResolvers()->erase( std::remove_if( sCustomResolvers()->begin(), sCustomResolvers()->end(), [id]( std::pair< QString, std::function< QString( const QString & ) > > &a )
212 {
213 return a.first == id;
214 } ), sCustomResolvers()->end() );
215 return prevCount != sCustomResolvers()->size();
216}
217
218QString QgsPathResolver::setPathWriter( const std::function<QString( const QString & )> &writer )
219{
220 QString id = QUuid::createUuid().toString();
221 sCustomWriters()->emplace_back( std::make_pair( id, writer ) );
222 return id;
223}
224
225bool QgsPathResolver::removePathWriter( const QString &id )
226{
227 const size_t prevCount = sCustomWriters()->size();
228 sCustomWriters()->erase( std::remove_if( sCustomWriters()->begin(), sCustomWriters()->end(), [id]( std::pair< QString, std::function< QString( const QString & ) > > &a )
229 {
230 return a.first == id;
231 } ), sCustomWriters()->end() );
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 QStringLiteral( "localized:" ) + 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() + QStringLiteral( "/resources" ) ) )
252 {
253 // replace inbuilt data folder path with "inbuilt:" prefix
254 return QStringLiteral( "inbuilt:" ) + 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 QStringLiteral( "attachment:" ) + 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( QStringLiteral( "." ) );
337 srcElems.removeAll( QStringLiteral( "." ) );
338
339 // remove common part
340 int n = 0;
341 while ( !srcElems.isEmpty() &&
342 !projElems.isEmpty() &&
343 srcElems[0].compare( projElems[0], cs ) == 0 )
344 {
345 srcElems.removeFirst();
346 projElems.removeFirst();
347 n++;
348 }
349
350 if ( n == 0 )
351 {
352 // no common parts; might not even be a file
353 return src;
354 }
355
356 if ( !projElems.isEmpty() )
357 {
358 // go up to the common directory
359 for ( int i = 0; i < projElems.size(); i++ )
360 {
361 srcElems.insert( 0, QStringLiteral( ".." ) );
362 }
363 }
364 else
365 {
366 // let it start with . nevertheless,
367 // so relative path always start with either ./ or ../
368 srcElems.insert( 0, QStringLiteral( "." ) );
369 }
370
371 // Append url query if any
372 QString returnPath { vsiPrefix + srcElems.join( QLatin1Char( '/' ) ) };
373 if ( ! urlQuery.isEmpty() )
374 {
375 returnPath.append( '?' );
376 returnPath.append( urlQuery );
377 }
378 return returnPath;
379}
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