Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix layer datasources incorrectly resolving to matching working directory names #60109

Merged
merged 5 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions src/core/qgsmaplayer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -565,17 +565,16 @@ bool QgsMapLayer::readLayerXml( const QDomElement &layerElement, QgsReadWriteCon
mnl = layerElement.namedItem( QStringLiteral( "datasource" ) );
mne = mnl.toElement();
const QString dataSourceRaw = mne.text();
mDataSource = provider.isEmpty() ? dataSourceRaw : QgsProviderRegistry::instance()->relativeToAbsoluteUri( provider, dataSourceRaw, context );

// if the layer needs authentication, ensure the master password is set
const thread_local QRegularExpression rx( "authcfg=([a-z]|[A-Z]|[0-9]){7}" );
if ( rx.match( mDataSource ).hasMatch()
if ( rx.match( dataSourceRaw ).hasMatch()
&& !QgsApplication::authManager()->setMasterPassword( true ) )
{
return false;
}

mDataSource = decodedSource( mDataSource, provider, context );
mDataSource = decodedSource( dataSourceRaw, provider, context );

// Set the CRS from project file, asking the user if necessary.
// Make it the saved CRS to have WMS layer projected correctly.
Expand Down Expand Up @@ -777,10 +776,7 @@ bool QgsMapLayer::writeLayerXml( QDomElement &layerElement, QDomDocument &docume

// data source
QDomElement dataSource = document.createElement( QStringLiteral( "datasource" ) );
const QgsDataProvider *provider = dataProvider();
const QString providerKey = provider ? provider->name() : QString();
const QString srcRaw = encodedSource( source(), context );
const QString src = providerKey.isEmpty() ? srcRaw : QgsProviderRegistry::instance()->absoluteToRelativeUri( providerKey, srcRaw, context );
const QString src = encodedSource( source(), context );
const QDomText dataSourceText = document.createTextNode( src );
dataSource.appendChild( dataSourceText );
layerElement.appendChild( dataSource );
Expand Down
6 changes: 6 additions & 0 deletions src/core/qgspathresolver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,12 @@ QString QgsPathResolver::writePath( const QString &s ) const
}

const QFileInfo srcFileInfo( srcPath );
// Guard against relative paths: If srcPath is already relative, QFileInfo will match
// files in the working directory, instead of project directory. Avoid by returning early.
if ( !srcFileInfo.isAbsolute() )
{
return srcPath;
}
if ( srcFileInfo.exists() )
// Do NOT resolve symlinks, but do remove '..' and '.'
srcPath = QDir::cleanPath( srcFileInfo.absoluteFilePath() );
Expand Down
79 changes: 79 additions & 0 deletions tests/src/core/testqgsproject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ class TestQgsProject : public QObject
void testSymlinks4LayerShapefileBroken();
void testSymlinks5ProjectFile();
void testSymlinks6ProjectFolder();
void regression60100();
};

void TestQgsProject::init()
Expand Down Expand Up @@ -1534,5 +1535,83 @@ void TestQgsProject::testSymlinks6ProjectFolder()
QCOMPARE( loadedLayer->source(), symlinkprojDir + "/points.shp" );
}

void TestQgsProject::regression60100()
{
/*
* Regression test for QGIS issue #60100 (https://github.com/qgis/QGIS/issues/60100)
* This test ensures that when saving a QGIS project with relative paths,
* the correct layer datasource is preserved, even when the current working
* directory (CWD) contains a file with the same name as the layer datasource.
*
* Previous behavior:
* - If a file with the same name as a layer datasource existed in the CWD,
* the layer path in the saved project would point to the file in the CWD,
* rather than the intended file in the project directory (PROJDIR).
*
* Test steps:
* 1. Create a temporary directory structure with two subfolders: WORKDIR and PROJDIR.
* 2. Copy a `points.geojson` file to both WORKDIR and PROJDIR.
* 3. Create a new QGIS project in PROJDIR and add the `points.geojson` file from PROJDIR as a layer.
* 4. Change the working directory to WORKDIR and save the project.
* 5. Verify that the saved project references the correct datasource (`./points.geojson` in PROJDIR)
* and does not erroneously reference the file in WORKDIR.
*/
// Create directory structure with 2 subfolders
const QTemporaryDir baseDir;
const QDir base( baseDir.path() );
base.mkdir( QStringLiteral( "WORKDIR" ) );
base.mkdir( QStringLiteral( "PROJDIR" ) );
const QString workDirPath = baseDir.path() + QStringLiteral( "/WORKDIR" );
const QString projDirPath = baseDir.path() + QStringLiteral( "/PROJDIR" );

// Save our old CWD and switch to the new WORKDIR
const QString oldCWD = QDir::currentPath();
QVERIFY( QDir::setCurrent( workDirPath ) );

// Copy points.geojson to both subfolders
const QString testDataDir( TEST_DATA_DIR );
const QString pointsPath = testDataDir + QStringLiteral( "/points.geojson" );
QFile::copy( pointsPath, workDirPath + QStringLiteral( "/points.geojson" ) );
QFile::copy( pointsPath, projDirPath + QStringLiteral( "/points.geojson" ) );

// Create a new/empty project in PROJDIR
const QString projectPath = projDirPath + QStringLiteral( "/project.qgs" );
std::unique_ptr<QgsProject> project = std::make_unique<QgsProject>();

// Add the local points.geojson (in PROJDIR) as a layer
std::unique_ptr<QgsVectorLayer> layer = std::make_unique<QgsVectorLayer>(
projDirPath + QStringLiteral( "/points.geojson" ),
QStringLiteral( "Test Points" ),
QStringLiteral( "ogr" )
);
project->addMapLayer( layer.release() );

// Write (save) the project to disk. This used to pick up the WRONG file and save it to the proj.
project->write( projectPath );

// Restore old working directory
QVERIFY( QDir::setCurrent( oldCWD ) );

// Verify the layer path in the project file
QDomDocument doc;
QFile projectFile( projectPath );
bool res = projectFile.open( QIODevice::ReadOnly );
Q_ASSERT( res );
res = static_cast<bool>( doc.setContent( &projectFile ) );
Q_ASSERT( res );
projectFile.close();

const QDomElement docElem = doc.documentElement();
const QDomElement layersElem = docElem.firstChildElement( QStringLiteral( "projectlayers" ) );
QDomElement layerElem = layersElem.firstChildElement();
while ( !layerElem.isNull() )
{
const QString layerSource = layerElem.firstChildElement( QStringLiteral( "datasource" ) ).text();
// Should NOT be "../WORKDIR/points.geojson"
QCOMPARE( layerSource, QStringLiteral( "./points.geojson" ) );
layerElem = layerElem.nextSiblingElement();
}
}

QGSTEST_MAIN( TestQgsProject )
#include "testqgsproject.moc"
Loading