Skip to content
154 changes: 95 additions & 59 deletions app/inpututils.cpp
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would replace all QLatin1Char that you used with QStringLiteral

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think QLatin1Char is more efficient in this case because it does not create a whole Qstring object and it is used only for a character.
Are there any benefits to changing them to QStringLiteral in the modified functions?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After reading the documentation it doesn't look like it's that straight forward. This part talks mostly about what we are discussing. However I would like to highlight one thing in the beginning:

Generally, QString can be used everywhere and it will perform fine.

Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,10 @@ bool InputUtils::removeFile( const QString &filePath )
bool InputUtils::copyFile( const QString &srcPath, const QString &dstPath )
{
QString modSrcPath = srcPath;
if ( srcPath.startsWith( "file://" ) )
QUrl url( srcPath );
if ( url.isValid() && url.isLocalFile() )
{
modSrcPath = modSrcPath.replace( "file://", "" );
modSrcPath = url.toLocalFile();
}

QFileInfo fi( dstPath );
Expand Down Expand Up @@ -994,7 +995,7 @@ QString InputUtils::resolveTargetDir( const QString &homePath, const QVariantMap
if ( !expression.isEmpty() )
{
QString result = evaluateExpression( pair, activeProject, expression );
sanitizeFileName( result );
sanitizePath( result );
return result;
}
else
Expand Down Expand Up @@ -1043,38 +1044,31 @@ QString InputUtils::resolvePath( const QString &path, const QString &homePath, c

QString InputUtils::getRelativePath( const QString &path, const QString &prefixPath )
{
QString modPath = path;
QString filePrefix( "file://" );

if ( path.startsWith( filePrefix ) )
// clean the file prefix
QString cleanPath = path;
const QUrl url( path );
if ( url.isValid() && url.isLocalFile() )
{
modPath = modPath.replace( filePrefix, QString() );
cleanPath = url.toLocalFile();
}

if ( prefixPath.isEmpty() ) return modPath;

// Do not use a canonical path for non-existing path
if ( !QFileInfo::exists( path ) )
// if no prefix is provided, return the cleaned absolute path
if ( prefixPath.isEmpty() )
{
if ( !prefixPath.isEmpty() && modPath.startsWith( prefixPath ) )
{
return modPath.replace( prefixPath, QString() );
}
return cleanPath;
}
else
{
QDir absoluteDir( modPath );
QDir prefixDir( prefixPath );
QString canonicalPath = absoluteDir.canonicalPath();
QString prefixCanonicalPath = prefixDir.canonicalPath() + "/";

if ( prefixCanonicalPath.length() > 1 && canonicalPath.startsWith( prefixCanonicalPath ) )
{
return canonicalPath.replace( prefixCanonicalPath, QString() );
}
// use QDir to calculate the relative path
const QDir prefixDir( prefixPath );
QString relativePath = prefixDir.relativeFilePath( cleanPath );

// check if the path starts with ".." or is absolute (on Windows/different drives), it's not a "child"
if ( relativePath.startsWith( QLatin1String( ".." ) ) || QDir::isAbsolutePath( relativePath ) )
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if ( relativePath.startsWith( QLatin1String( ".." ) ) || QDir::isAbsolutePath( relativePath ) )
if ( relativePath.startsWith( QLatin1StringView( ".." ) ) || QDir::isAbsolutePath( relativePath ) )

QLatin1String is a synonym for QLatin1StringView, but it's softly deprecated

{
return {};
}

return QString();
return relativePath;
}

void InputUtils::logMessage( const QString &message, const QString &tag, Qgis::MessageLevel level )
Expand Down Expand Up @@ -1964,64 +1958,106 @@ QUrl InputUtils::iconFromGeometry( const Qgis::GeometryType &geometry )
}
}

void InputUtils::sanitizeFileName( QString &fileName )
QString InputUtils::sanitizeNode( const QString &input )
{
// regex captures ascii codes 0 to 31 and windows path forbidden characters <>:|?*"
const thread_local QRegularExpression illegalChars( QStringLiteral( "[\x00-\x19<>:|?*\"]" ) );
fileName.replace( illegalChars, QStringLiteral( "_" ) );
fileName = fileName.trimmed();
if ( input.isEmpty() ) return input;

// Trim whitespace immediately before the final extension, e.g. "name .jpg" -> "name.jpg"
const int lastDot = fileName.lastIndexOf( QChar( '.' ) );
if ( lastDot > 0 )
// trim the whitespace at the beginning and the end
QString cleanOutput = input;
cleanOutput = cleanOutput.trimmed();

// remove illegal characters before using QFileInfo
const static QRegularExpression illegalChars( QStringLiteral( "[\x00-\x1f<>:|?*\"/\\\\]" ) );
cleanOutput.replace( illegalChars, QStringLiteral( "_" ) );

// split base and suffix to trim whitespace correctly.
QFileInfo fi( cleanOutput );
QString base = fi.completeBaseName().trimmed();
QString suffix = fi.suffix().trimmed();

// handle Windows Reserved Names (CON, PRN, etc.)
#ifdef Q_OS_WIN
if ( base.trimmed().length() == 3 || base.trimmed().length() == 4 )
{
const QString base = fileName.first( lastDot ).trimmed();
const QString ext = fileName.sliced( lastDot );
fileName = base + ext;
const static QRegularExpression reservedNames(
QStringLiteral( "^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$" ),
QRegularExpression::CaseInsensitiveOption
);
if ( reservedNames.match( base.trimmed() ).hasMatch() )
{
base = base.trimmed() + QLatin1Char( '_' );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will get changed to QChar

}
}
#endif

// reassemble
if ( !suffix.isEmpty() )
{
return base + QLatin1Char( '.' ) + suffix;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will also get changed to QChar

}
return base;
}

void InputUtils::sanitizePath( QString &path )
{
const bool pathStartsWithFileURL = path.startsWith( "file://" );
if ( path.isEmpty() ) return;
QString cleanPath = path;

// check if the path has a file prefix
// QUrl treats ? as a query start, # as a fragment
// so we modify the cleanPath for the file prefix check
cleanPath.replace( QStringLiteral( "?" ), QStringLiteral( "_" ) );
cleanPath.replace( QStringLiteral( "#" ), QStringLiteral( "_" ) );

// parse file prefix and path
QUrl url = QUrl::fromUserInput( cleanPath );
bool isUrl = path.startsWith( QLatin1String( "file:" ), Qt::CaseInsensitive );
Comment on lines +2013 to +2014
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
QUrl url = QUrl::fromUserInput( cleanPath );
bool isUrl = path.startsWith( QLatin1String( "file:" ), Qt::CaseInsensitive );
const QUrl url = QUrl::fromUserInput( cleanPath );
const bool isUrl = path.startsWith( QStringLiteral( "file:" ), Qt::CaseInsensitive );


if ( pathStartsWithFileURL )
// if it has the file prefix, we will get rid of it
if ( isUrl )
{
// remove file:// prefix before sanitization
path.remove( 0, 7 );
cleanPath = url.toLocalFile();
}

const bool pathStartsWithSlash = path.startsWith( '/' );
// normalize separators
// convert all backslashes to forward slashes to ensure consistent splitting
cleanPath.replace( QStringLiteral( "\\" ), QStringLiteral( "/" ) );

const QStringList parts = path.split( '/', Qt::SkipEmptyParts );
QString sanitizedPath;
// split and sanitize each segment
QStringList parts = cleanPath.split( QStringLiteral( "/" ), Qt::SkipEmptyParts );
QStringList sanitizedParts;

for ( int i = 0; i < parts.size(); ++i )
{
QString part = parts.at( i );
sanitizeFileName( part );
const QString &part = parts[i];

if ( i > 0 )
// keep Windows Drive Letters (e.g. "C:")
if ( i == 0 && part.endsWith( QStringLiteral( ":" ) ) && part.size() == 2 )
{
sanitizedPath += '/';
sanitizedParts << part;
continue;
}

sanitizedPath += part;
sanitizedParts << sanitizeNode( part );
}

if ( pathStartsWithSlash )
// reconstruct with Unix-style separators (/)
QString result = sanitizedParts.join( QStringLiteral( "/" ) );

// handle Absolute Paths
if ( cleanPath.startsWith( QStringLiteral( "/" ) ) )
{
// restore leading slash
sanitizedPath = '/' + sanitizedPath;
result.prepend( QStringLiteral( "/" ) );
}

if ( pathStartsWithFileURL )
// restore file prefix protocol
if ( isUrl )
{
// restore file:// prefix
sanitizedPath = "file://" + sanitizedPath;
path = QUrl::fromLocalFile( result ).toString();
}
else
{
path = result;
}

path = sanitizedPath;
}

QSet<int> InputUtils::referencedAttributeIndexes( QgsVectorLayer *layer, const QString &expression )
Expand Down
12 changes: 6 additions & 6 deletions app/inpututils.h
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ class InputUtils: public QObject

/**
* Returns relative path of the file to given prefixPath. If prefixPath does not match a path parameter,
* returns an empty string. If a path starts with "file://", this prefix is ignored.
* returns an empty string. If a path starts with "file:///" or "file://", this prefix is ignored.
* \param path Absolute path to file
* \param prefixPath
*/
Expand Down Expand Up @@ -627,14 +627,14 @@ class InputUtils: public QObject
Q_INVOKABLE static bool isValidEmail( const QString &email );

/**
* Replaces invalid filename characters with underscores (_)
* Also trims whitespaces at the start and end of \a filename. If \a filename has an extension and
* last character before the . is a whitespace, it does not get trimmed.
* Replaces invalid path related characters with underscores '_'
* It can be used as standalone for any string to be sanitized
*/
static void sanitizeFileName( QString &fileName );

static QString sanitizeNode( const QString &input );

/**
* Splits path into components and sanitizes each component using sanitizeFileName().
* Splits path into components and sanitizes each component using sanitizeNode().
*/
static void sanitizePath( QString &path );

Expand Down
2 changes: 1 addition & 1 deletion app/projectwizard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ void ProjectWizard::createProject( QString const &projectNameRaw, FieldsModel *f
}

QString projectName( projectNameRaw );
InputUtils::sanitizeFileName( projectName );
projectName = InputUtils::sanitizeNode( projectName );

QString projectDir = CoreUtils::createUniqueProjectDirectory( mDataDir, projectName );
QString projectFilepath( QString( "%1/%2.qgz" ).arg( projectDir ).arg( projectName ) );
Expand Down
2 changes: 1 addition & 1 deletion app/qml/form/editors/MMFormGalleryEditor.qml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ MMPrivateComponents.MMBaseInput {
let absolutePath = model.PhotoPath

if ( absolutePath !== '' && __inputUtils.fileExists( absolutePath ) ) {
return "file://" + absolutePath
return "file:///" + absolutePath
}
return ''
}
Expand Down
6 changes: 3 additions & 3 deletions app/qml/form/editors/MMFormPhotoEditor.qml
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,10 @@ MMFormPhotoViewer {
target: root.sketchingController

function onTempPhotoSourceChanged( newPath ){
if ( internal.tempSketchedImageSource === "file://" + newPath ) {
if ( internal.tempSketchedImageSource === "file:///" + newPath ) {
internal.tempSketchedImageSource = ""
}
internal.tempSketchedImageSource = "file://" + newPath
internal.tempSketchedImageSource = "file:///" + newPath
}

function onSketchesSavingError(){
Expand Down Expand Up @@ -269,7 +269,7 @@ MMFormPhotoViewer {

if ( __inputUtils.fileExists( absolutePath ) ) {
root.photoState = "valid"
resolvedImageSource = "file://" + absolutePath
resolvedImageSource = "file:///" + absolutePath
tempSketchedImageSource = ""
}
else if ( __inputUtils.isValidUrl( absolutePath ) ) {
Expand Down
4 changes: 2 additions & 2 deletions app/qml/form/editors/MMFormPhotoViewer.qml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ MMPrivateComponents.MMBaseInput {
visible: photoStateGroup.state !== "notSet"

photoUrl: root.photoUrl
isLocalFile: root.photoUrl.startsWith( "file://" )
isLocalFile: root.photoUrl.startsWith( "file:///")
cache: false

fillMode: Image.PreserveAspectCrop
Expand Down Expand Up @@ -125,7 +125,7 @@ MMPrivateComponents.MMBaseInput {
iconSource: __style.drawIcon
iconColor: __style.forestColor

visible: root.editState === "enabled" && photoStateGroup.state !== "notSet" && __activeProject.photoSketchingEnabled && root.photoUrl.startsWith("file://")
visible: root.editState === "enabled" && photoStateGroup.state !== "notSet" && __activeProject.photoSketchingEnabled && root.photoUrl.startsWith("file:///")

onClicked: {
sketchingLoader.active = true
Expand Down
4 changes: 3 additions & 1 deletion app/test/testsketching.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,9 @@ void TestSketching::testLoadBackupSketch()
sketchingController.mPhotoSource = path;
sketchingController.mProjectName = QStringLiteral( "/this/is/long/path/to/image/test_sketching" );
sketchingController.prepareController();
QCOMPARE( sketchingController.mPhotoSource, "file://" + QDir::tempPath() + QStringLiteral( "/test_sketching" ) + QStringLiteral( "/MM_test_image.jpg" ) );
const QString localPath = QDir::tempPath() + QStringLiteral( "/test_sketching/MM_test_image.jpg" );
const QString expectedUrl = QUrl::fromLocalFile( localPath ).toString();
QCOMPARE( sketchingController.mPhotoSource, expectedUrl );
QCOMPARE( sketchingController.mOriginalPhotoSource, QUrl( path ).toLocalFile() );
QCOMPARE( spy.count(), 1 );
auto signalArgs = spy.takeLast();
Expand Down
45 changes: 18 additions & 27 deletions app/test/testutilsfunctions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ void TestUtilsFunctions::getRelativePath()
QString relativePath2 = mUtils->getRelativePath( path2, prefixPath );
QCOMPARE( fileName2, relativePath2 );

QString path3 = QStringLiteral( "file://" ) + path2;
QString path3 = QUrl( path2 ).toString();
QString relativePath3 = mUtils->getRelativePath( path3, prefixPath );
QCOMPARE( fileName2, relativePath3 );

Expand Down Expand Up @@ -978,30 +978,6 @@ void TestUtilsFunctions::testIsValidEmail()
QVERIFY( !InputUtils::isValidEmail( "brokenemail" ) );
QVERIFY( !InputUtils::isValidEmail( "" ) );
}

void TestUtilsFunctions::testSanitizeFileName()
{
// unchanged
QString str = QStringLiteral( "/simple/valid/filename.ext" );
InputUtils::sanitizeFileName( str );
QCOMPARE( str, QStringLiteral( "/simple/valid/filename.ext" ) );

// unchanged
str = QStringLiteral( "/complex/valid/Φ!l@#äme$%^&()-_=+[]{}`~;',.ext" );
InputUtils::sanitizeFileName( str );
QCOMPARE( str, QStringLiteral( "/complex/valid/Φ!l@#äme$%^&()-_=+[]{}`~;',.ext" ) );

// sanitized
str = QStringLiteral( "/sa ni*tized/f<i>l?n\"a:m|e .ext " );
InputUtils::sanitizeFileName( str );
QCOMPARE( str, QStringLiteral( "/sa ni_tized/f_i_l_n_a_m_e.ext" ) );

// sanitized
str = QStringLiteral( "/sa ni*tized/.f<i>l?n\"a:m|e .co .ext " );
InputUtils::sanitizeFileName( str );
QCOMPARE( str, QStringLiteral( "/sa ni_tized/.f_i_l_n_a_m_e .co.ext" ) );
}

void TestUtilsFunctions::testSanitizePath()
{
// unchanged
Expand All @@ -1017,7 +993,7 @@ void TestUtilsFunctions::testSanitizePath()
// unchanged - url prefix
str = QStringLiteral( "file://simple/valid/filename.ext" );
InputUtils::sanitizePath( str );
QCOMPARE( str, QStringLiteral( "file://simple/valid/filename.ext" ) );
QCOMPARE( str, QStringLiteral( "file:///simple/valid/filename.ext" ) );

// unchanged - url prefix with slash
str = QStringLiteral( "file:///simple/valid/filename.ext" );
Expand All @@ -1027,7 +1003,17 @@ void TestUtilsFunctions::testSanitizePath()
// unchanged
str = QStringLiteral( "/complex/valid/Φ!l@#äme$%^&()-_=+[]{}`~;',.ext" );
InputUtils::sanitizePath( str );
QCOMPARE( str, QStringLiteral( "/complex/valid/Φ!l@#äme$%^&()-_=+[]{}`~;',.ext" ) );
QCOMPARE( str, QStringLiteral( "/complex/valid/Φ!l@_äme$%^&()-_=+[]{}`~;',.ext" ) );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please note down somewhere that # is prohibited character now


// unchanged with partition letter on Windows
str = QStringLiteral( "C:/Users/simple/valid/filename.ext" );
InputUtils::sanitizePath( str );
QCOMPARE( str, QStringLiteral( "C:/Users/simple/valid/filename.ext" ) );

// unchanged with partition letter on Windows and file prefix
str = QStringLiteral( "file:///C:/Users/simple/valid/filename.ext" );
InputUtils::sanitizePath( str );
QCOMPARE( str, QStringLiteral( "file:///C:/Users/simple/valid/filename.ext" ) );

// sanitized
str = QStringLiteral( "/sa ni*tized/f<i>l?n\"a:m|e.ext " );
Expand All @@ -1048,4 +1034,9 @@ void TestUtilsFunctions::testSanitizePath()
str = QStringLiteral( "project name / project .qgz " );
InputUtils::sanitizePath( str );
QCOMPARE( str, QStringLiteral( "project name/project.qgz" ) );

// sanitized with partition letter
str = QStringLiteral( "C:/project name / project .qgz " );
InputUtils::sanitizePath( str );
QCOMPARE( str, QStringLiteral( "C:/project name/project.qgz" ) );
}
Loading
Loading