QtQml: Allow basic name quoting in qmldir files

This adds support for quoted file names and for escaping quotes and
backslashes with '\'. It breaks support for file names that genuinely
start with a quote.

[ChangeLog][QtQml] You can now quote file names in qmldir files using
'"'. This is so that you can write file names with spaces in them. '\'
functions as an escape character, allowing you to escape quotes that are
part of file names and backslash itself.

Task-number: QTBUG-131916
Change-Id: Ia89e60153d3fafa273034f81da37166f30284c76
Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
This commit is contained in:
Ulf Hermann 2024-12-10 13:31:44 +01:00
parent 33c13102bf
commit bac4723a30
8 changed files with 186 additions and 7 deletions

View File

@ -62,9 +62,59 @@ inline static void scanToEnd(const QChar *&ch) {
++ch;
}
inline static void scanWord(const QChar *&ch) {
inline static QString scanWord(const QChar *&ch) {
const QChar *begin = ch;
while (!ch->isSpace() && !ch->isNull())
++ch;
return QString(begin, ch - begin);
}
QString QQmlDirParser::scanQuotedWord(const QChar *&ch, quint16 lineNumber, quint16 columnNumber)
{
Q_ASSERT(*ch == QLatin1Char('"'));
++ch;
QString result;
const QChar *begin = ch;
while (*ch != QLatin1Char('"')) {
if (ch->isNull()) {
reportError(lineNumber, columnNumber,
QStringLiteral("file ends inside a quoted string"));
result.append(begin, ch - begin);
return result;
}
if (*ch == QLatin1Char('\n') || *ch == QLatin1Char('\r')) {
reportError(lineNumber, columnNumber,
QStringLiteral("line breaks in quoted strings are not supported as they "
"are not portable between different operating systems"));
result.append(begin, ch - begin);
return result;
}
if (*ch == QLatin1Char('\\')) {
result.append(begin, ch - begin);
++ch;
++columnNumber;
if (*ch != QLatin1Char('"') && *ch != QLatin1Char('\\')) {
reportError(lineNumber, columnNumber,
QStringLiteral("only '\"' and '\\' can be escaped"));
return result;
}
begin = ch;
}
++ch;
++columnNumber;
}
result.append(begin, ch - begin);
Q_ASSERT(*ch == QLatin1Char('"'));
++ch;
return result;
}
/*!
@ -142,16 +192,18 @@ bool QQmlDirParser::parse(const QString &source)
scanToEnd(ch);
break;
}
const QChar *start = ch;
scanWord(ch);
if (sectionCount < 4) {
sections[sectionCount++] = source.mid(start-source.constData(), ch-start);
} else {
reportError(lineNumber, start-lineStart, QLatin1String("unexpected token"));
if (sectionCount >= 4) {
reportError(lineNumber, ch - lineStart, QLatin1String("unexpected token"));
scanToEnd(ch);
invalidLine = true;
break;
}
sections[sectionCount++] = (*ch == QLatin1Char('"'))
? scanQuotedWord(ch, lineNumber, ch - lineStart)
: scanWord(ch);
scanSpace(ch);
} while (*ch != QLatin1Char('\n') && !ch->isNull());

View File

@ -140,6 +140,7 @@ public:
private:
bool maybeAddComponent(const QString &typeName, const QString &fileName, const QString &version, QHash<QString,Component> &hash, int lineNumber = -1, bool multi = true);
void reportError(quint16 line, quint16 column, const QString &message);
QString scanQuotedWord(const QChar *&ch, quint16 lineNumber, quint16 columnNumber);
private:
QList<QQmlJS::DiagnosticMessage> _errors;

View File

@ -0,0 +1,11 @@
# "Comment"
module ModuleNamespace
plugin PluginA "plugina.so" # More "comment"
ComponentA 1.0 "componenta\"1_0.qml"
ScriptA 1.0 "scripta 1_0.js"
# "
ComponentA 1.5 "componenta\\1_5.qml"
ComponentB 1.5 "componentb 1_5.qml

View File

@ -0,0 +1,11 @@
# "Comment"
module ModuleNamespace
plugin PluginA "plugina.so" # More "comment"
ComponentA 1.0 "componenta\"1_0.qml"
ScriptA 1.0 "scripta 1_0.js"
# "
ComponentA 1.5 "componenta\\1_5.qml"
ComponentB 1.5 "componentb 1_5.qml\

View File

@ -0,0 +1,11 @@
# "Comment"
module ModuleNamespace
plugin PluginA "plugin\a.so" # More "comment"
ComponentA 1.0 "componenta\"1_0.qml"
ScriptA 1.0 "scripta 1_0.js"
# "
ComponentA 1.5 "componenta\\1_5.qml"
ComponentB 1.5 "componentb 1_5.qml"

View File

@ -0,0 +1,12 @@
# "Comment"
module ModuleNamespace
plugin PluginA "plugin
a.so" # More "comment"
ComponentA 1.0 "componenta\"1_0.qml"
ScriptA 1.0 "scripta 1_0.js"
# "
ComponentA 1.5 "componenta\\1_5.qml"
ComponentB 1.5 "componentb 1_5.qml"

View File

@ -0,0 +1,11 @@
# "Comment"
module ModuleNamespace
plugin PluginA "plugina.so" # More "comment"
ComponentA 1.0 "componenta\"1_0.qml"
ScriptA 1.0 "scripta 1_0.js"
# "
ComponentA 1.5 "componenta\\1_5.qml"
ComponentB 1.5 "componentb 1_5.qml"

View File

@ -335,6 +335,76 @@ void tst_qqmldirparser::parse_data()
<< QStringList()
<< false;
QTest::newRow("quoted")
<< "quoted/qmldir"
<< QString()
<< QStringList()
<< (QStringList() << "PluginA|plugina.so")
<< QStringList()
<< (QStringList() << "ComponentA|componenta\"1_0.qml|1|0|false"
<< "ComponentA|componenta\\1_5.qml|1|5|false"
<< "ComponentB|componentb 1_5.qml|1|5|false")
<< (QStringList() << "ScriptA|scripta 1_0.js|1|0")
<< QStringList()
<< false;
QTest::newRow("ends-in-quoted")
<< "ends-in-quoted/qmldir"
<< QString()
<< (QStringList() << "qmldir:11:33: file ends inside a quoted string")
<< (QStringList() << "PluginA|plugina.so")
<< QStringList()
<< (QStringList() << "ComponentA|componenta\"1_0.qml|1|0|false"
<< "ComponentA|componenta\\1_5.qml|1|5|false"
<< "ComponentB|componentb 1_5.qml|1|5|false")
<< (QStringList() << "ScriptA|scripta 1_0.js|1|0")
<< QStringList()
<< false;
QTest::newRow("invalid-escaped")
<< "invalid-escaped/qmldir"
<< QString()
<< (QStringList()
<< "qmldir:4:22: only '\"' and '\\' can be escaped"
<< "qmldir:4: plugin directive requires one or two arguments, but 3 were provided")
<< QStringList()
<< QStringList()
<< (QStringList() << "ComponentA|componenta\"1_0.qml|1|0|false"
<< "ComponentA|componenta\\1_5.qml|1|5|false"
<< "ComponentB|componentb 1_5.qml|1|5|false")
<< (QStringList() << "ScriptA|scripta 1_0.js|1|0")
<< QStringList()
<< false;
QTest::newRow("escaped-end-of-file")
<< "escaped-end-of-file/qmldir"
<< QString()
<< (QStringList() << "qmldir:11:34: only '\"' and '\\' can be escaped")
<< (QStringList() << "PluginA|plugina.so")
<< QStringList()
<< (QStringList() << "ComponentA|componenta\"1_0.qml|1|0|false"
<< "ComponentA|componenta\\1_5.qml|1|5|false"
<< "ComponentB|componentb 1_5.qml|1|5|false")
<< (QStringList() << "ScriptA|scripta 1_0.js|1|0")
<< QStringList()
<< false;
QTest::newRow("line-break-in-quoted")
<< "line-break-in-quoted/qmldir"
<< QString()
<< (QStringList()
<< "qmldir:4:21: line breaks in quoted strings are not supported as they are not "
"portable between different operating systems"
<< "qmldir:5: a component declaration requires two or three arguments, but 1 were provided")
<< (QStringList() << "PluginA|plugin")
<< QStringList()
<< (QStringList() << "ComponentA|componenta\"1_0.qml|1|0|false"
<< "ComponentA|componenta\\1_5.qml|1|5|false"
<< "ComponentB|componentb 1_5.qml|1|5|false")
<< (QStringList() << "ScriptA|scripta 1_0.js|1|0")
<< QStringList()
<< false;
QTest::newRow("designersupported-yes")
<< "designersupported-yes/qmldir"
<< QString()