diff options
-rw-r--r-- | doc/src/snippets/code/src_corelib_io_qurl.cpp | 2 | ||||
-rw-r--r-- | src/corelib/io/io.pri | 2 | ||||
-rw-r--r-- | src/corelib/io/qurlquery.cpp | 745 | ||||
-rw-r--r-- | src/corelib/io/qurlquery.h | 117 | ||||
-rw-r--r-- | tests/auto/corelib/io/qurlquery/qurlquery.pro | 5 | ||||
-rw-r--r-- | tests/auto/corelib/io/qurlquery/tst_qurlquery.cpp | 695 |
6 files changed, 1565 insertions, 1 deletions
diff --git a/doc/src/snippets/code/src_corelib_io_qurl.cpp b/doc/src/snippets/code/src_corelib_io_qurl.cpp index ba17c02fb3..8fd4b8ee9f 100644 --- a/doc/src/snippets/code/src_corelib_io_qurl.cpp +++ b/doc/src/snippets/code/src_corelib_io_qurl.cpp @@ -68,7 +68,7 @@ sock.connectToHost(url.host(), url.port(80)); //! [4] -http://www.example.com/cgi-bin/drawgraph.cgi?type-pie/color-green +http://www.example.com/cgi-bin/drawgraph.cgi?type(pie)color(green) //! [4] diff --git a/src/corelib/io/io.pri b/src/corelib/io/io.pri index a70b3c9012..8602e60744 100644 --- a/src/corelib/io/io.pri +++ b/src/corelib/io/io.pri @@ -28,6 +28,7 @@ HEADERS += \ io/qresource_iterator_p.h \ io/qstandardpaths.h \ io/qurl.h \ + io/qurlquery.h \ io/qurltlds_p.h \ io/qtldurl_p.h \ io/qsettings.h \ @@ -65,6 +66,7 @@ SOURCES += \ io/qresource_iterator.cpp \ io/qstandardpaths.cpp \ io/qurl.cpp \ + io/qurlquery.cpp \ io/qurlrecode.cpp \ io/qsettings.cpp \ io/qfsfileengine.cpp \ diff --git a/src/corelib/io/qurlquery.cpp b/src/corelib/io/qurlquery.cpp new file mode 100644 index 0000000000..2f1975722f --- /dev/null +++ b/src/corelib/io/qurlquery.cpp @@ -0,0 +1,745 @@ +/**************************************************************************** +** +** Copyright (C) 2012 Intel Corporation +** Contact: http://www.qt-project.org/ +** +** This file is part of the QtCore module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** GNU Lesser General Public License Usage +** This file may be used under the terms of the GNU Lesser General Public +** License version 2.1 as published by the Free Software Foundation and +** appearing in the file LICENSE.LGPL included in the packaging of this +** file. Please review the following information to ensure the GNU Lesser +** General Public License version 2.1 requirements will be met: +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU General +** Public License version 3.0 as published by the Free Software Foundation +** and appearing in the file LICENSE.GPL included in the packaging of this +** file. Please review the following information to ensure the GNU General +** Public License version 3.0 requirements will be met: +** http://www.gnu.org/copyleft/gpl.html. +** +** Other Usage +** Alternatively, this file may be used in accordance with the terms and +** conditions contained in a signed written agreement between you and Nokia. +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qurlquery.h" + +QT_BEGIN_NAMESPACE + +/*! + \class QUrlQuery + + \brief The QUrlQuery class provides a way to manipulate a key-value pairs in + a URL's query. + + \reentrant + \ingroup io + \ingroup network + \ingroup shared + + It is used to parse the query strings found in URLs like the following: + + \img qurl-querystring.png + + Query strings like the above are used to transmit options in the URL and are + usually decoded into multiple key-value pairs. The one above would contain + two entries in its list, with keys "type" and "color". QUrlQuery can also be + used to create a query string suitable for use in QUrl::setQuery() from the + individual components of the query. + + The most common way of parsing a query string is to initialize it in the + constructor by passing it the query string. Otherwise, the setQuery() method + can be used to set the query to be parsed. That method can also be used to + parse a query with non-standard delimiters, after having set them using the + setQueryDelimiters() function. + + The encoded query string can be obtained again using query(). This will take + all the internally-stored items and encode the string using the delimiters. + + \section1 Encoding + + All of the getter methods in QUrlQuery support an optional parameter of type + QUrl::ComponentFormattingOptions, including query(), which dictate how to + encode the data in question. Regardless of the mode, the returned value must + still be considered a percent-encoded string, as there are certain values + which cannot be expressed in decoded form (like control characters, byte + sequences not decodable to UTF-8). For that reason, the percent character is + always represented by the string "%25". + + \section2 Handling of spaces and plus ("+") + + Web browsers usually encode spaces found in HTML FORM elements to a plus sign + ("+") and plus signs to its percent-encoded form (%2B). However, the Internet + specifications governing URLs do not consider spaces and the plus character + equivalent. + + For that reason, QUrlQuery never encodes the space character to "+" and will + never decode "+" to a space character. Instead, space characters will be + rendered "%20" in encoded form. + + To support encoding like that of HTML forms, QUrlQuery also never decodes the + "%2B" sequence to a plus sign nor encode a plus sign. In fact, any "%2B" or + "+" sequences found in the keys, values, or query string are left exactly + like written (except for the uppercasing of "%2b" to "%2B"). + + \section1 Non-standard delimiters + + By default, QUrlQuery uses an equal sign ("=") to separate a key from its + value, and an ampersand ("&") to separate key-value pairs from each other. It + is possible to change the delimiters that QUrlQuery uses for parsing and for + reconstructing the query by calling setQueryDelimiters(). + + Non-standard delimiters should be chosen from among what RFC 3986 calls + "sub-delimiters". They are: + + \code + sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + / "*" / "+" / "," / ";" / "=" + \endcode + + Use of other characters is not supported and may result in unexpected + behaviour. QUrlQuery does not verify that you passed a valid delimiter. + + \sa QUrl +*/ + +typedef QList<QPair<QString, QString> > Map; + +int qt_urlRecode(QString &appendTo, const QChar *begin, const QChar *end, + QUrl::ComponentFormattingOptions encoding, const ushort *tableModifications); + +class QUrlQueryPrivate : public QSharedData +{ +public: + QUrlQueryPrivate(const QString &query = QString()) + : valueDelimiter(QUrlQuery::defaultQueryValueDelimiter()), + pairDelimiter(QUrlQuery::defaultQueryPairDelimiter()) + { if (!query.isEmpty()) setQuery(query); } + + QString recodeFromUser(const QString &input) const; + QString recodeToUser(const QString &input, QUrl::ComponentFormattingOptions encoding) const; + + void setQuery(const QString &query); + + void addQueryItem(const QString &key, const QString &value) + { itemList.append(qMakePair(recodeFromUser(key), recodeFromUser(value))); } + int findRecodedKey(const QString &key, int from = 0) const + { + for (int i = from; i < itemList.size(); ++i) + if (itemList.at(i).first == key) + return i; + return itemList.size(); + } + Map::const_iterator findKey(const QString &key) const + { return itemList.constBegin() + findRecodedKey(recodeFromUser(key)); } + Map::iterator findKey(const QString &key) + { return itemList.begin() + findRecodedKey(recodeFromUser(key)); } + + // use QMap so we end up sorting the items by key + Map itemList; + QChar valueDelimiter; + QChar pairDelimiter; +}; + +template<> void QSharedDataPointer<QUrlQueryPrivate>::detach() +{ + if (d && d->ref.load() == 1) + return; + QUrlQueryPrivate *x = (d ? new QUrlQueryPrivate(*d) + : new QUrlQueryPrivate); + x->ref.ref(); + if (d && !d->ref.deref()) + delete d; + d = x; +} + +// Here's how we do the encoding in QUrlQuery +// The RFC says these are the delimiters: +// gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" +// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" +// / "*" / "+" / "," / ";" / "=" +// And the definition of query is: +// query = *( pchar / "/" / "?" ) +// pchar = unreserved / pct-encoded / sub-delims / ":" / "@" +// +// The strict definition of query says that it can have unencoded any +// unreserved, sub-delim, ":", "@", "/" and "?". Or, by exclusion, excluded +// delimiters are "#", "[" and "]" -- if those are present, they must be +// percent-encoded. +// +// The internal storage in the Map is equivalent to PrettyDecoded. That means +// the getter methods, when called with the default encoding value, will not +// have to recode anything (except for toString()). +// +// The "+" sub-delimiter is always left untouched. We never encode "+" to "%2B" +// nor do we decode "%2B" to "+", no matter what the user asks. +// +// The rest of the delimiters are kept in their decoded forms and that's +// considered non-ambiguous. That includes the pair and value delimiters +// themselves. +// +// But when recreating the query string, in toString(), we must take care of +// the special delimiters: the pair and value delimiters and those that the +// definition says must be encoded ("#" / "[" / "]") + +#define decode(x) ushort(x) +#define leave(x) ushort(0x100 | (x)) +#define encode(x) ushort(0x200 | (x)) +static const ushort prettyDecodedActions[] = { leave('+'), 0 }; + +inline QString QUrlQueryPrivate::recodeFromUser(const QString &input) const +{ + // note: duplicated in setQuery() + QString output; + if (qt_urlRecode(output, input.constData(), input.constData() + input.length(), + QUrl::DecodeUnicode | QUrl::DecodeAllDelimiters | QUrl::DecodeSpaces, + prettyDecodedActions)) + return output; + return input; +} + +inline bool idempotentRecodeToUser(QUrl::ComponentFormattingOptions encoding) +{ + return encoding == QUrl::PrettyDecoded || encoding == (QUrl::PrettyDecoded | QUrl::DecodeAllDelimiters); +} + +inline QString QUrlQueryPrivate::recodeToUser(const QString &input, QUrl::ComponentFormattingOptions encoding) const +{ + // our internal formats are stored in "PrettyDecoded" form + // and there are no ambiguous characters + if (idempotentRecodeToUser(encoding)) + return input; + + bool decodeUnambiguous = encoding & QUrl::DecodeUnambiguousDelimiters; + encoding &= ~QUrl::DecodeAllDelimiters; + + if (decodeUnambiguous) { + QString output; + if (qt_urlRecode(output, input.constData(), input.constData() + input.length(), + encoding | QUrl::DecodeAllDelimiters, prettyDecodedActions)) + return output; + return input; + } + + // re-encode the gen-delims that the RFC says must be encoded the + // query delimiter pair; the non-delimiters will get encoded too + ushort actions[] = { encode(pairDelimiter.unicode()), encode(valueDelimiter.unicode()), + encode('#'), encode('['), encode(']'), 0 }; + QString output; + if (qt_urlRecode(output, input.constData(), input.constData() + input.length(), encoding, actions)) + return output; + return input; +} + +void QUrlQueryPrivate::setQuery(const QString &query) +{ + itemList.clear(); + const QChar *pos = query.constData(); + const QChar *const end = pos + query.size(); + while (pos != end) { + const QChar *begin = pos; + const QChar *delimiter = 0; + while (pos != end) { + // scan for the component parts of this pair + if (!delimiter && pos->unicode() == valueDelimiter) + delimiter = pos; + if (pos->unicode() == pairDelimiter) + break; + ++pos; + } + if (!delimiter) + delimiter = pos; + + // pos is the end of this pair (the end of the string or the pair delimiter) + // delimiter points to the value delimiter or to the end of this pair + + QString key; + if (!qt_urlRecode(key, begin, delimiter, + QUrl::DecodeUnicode | QUrl::DecodeAllDelimiters | QUrl::DecodeSpaces, + prettyDecodedActions)) + key = QString(begin, delimiter - begin); + + if (delimiter == pos) { + // the value delimiter wasn't found, store a null value + itemList.append(qMakePair(key, QString())); + } else if (delimiter + 1 == pos) { + // if the delimiter was found but the value is empty, store empty-but-not-null + itemList.append(qMakePair(key, QString(0, Qt::Uninitialized))); + } else { + QString value; + if (!qt_urlRecode(value, delimiter + 1, pos, + QUrl::DecodeUnicode | QUrl::DecodeAllDelimiters | QUrl::DecodeSpaces, + prettyDecodedActions)) + value = QString(delimiter + 1, pos - delimiter - 1); + itemList.append(qMakePair(key, value)); + } + + if (pos != end) + ++pos; + } +} + +// allow QUrlQueryPrivate to detach from null +template <> inline QUrlQueryPrivate * +QSharedDataPointer<QUrlQueryPrivate>::clone() +{ + return d ? new QUrlQueryPrivate(*d) : new QUrlQueryPrivate; +} + +/*! + Constructs an empty QUrlQuery object. A query can be set afterwards by + calling setQuery() or items can be added by using addQueryItem(). + + \sa setQuery(), addQueryItem() +*/ +QUrlQuery::QUrlQuery() + : d(0) +{ +} + +/*! + Constructs a QUrlQuery object and parses the \a queryString query string, + using the default query delimiters. To parse a query string using other + delimiters, you should first set them using setQueryDelimiters() and then + set the query with setQuery(). +*/ +QUrlQuery::QUrlQuery(const QString &queryString) + : d(queryString.isEmpty() ? 0 : new QUrlQueryPrivate(queryString)) +{ +} + +/*! + Constructs a QUrlQuery object and parses the query string found in the \a + url URL, using the default query delimiters. To parse a query string using + other delimiters, you should first set them using setQueryDelimiters() and + then set the query with setQuery(). + + \sa QUrl::query() +*/ +QUrlQuery::QUrlQuery(const QUrl &url) + : d(0) +{ + // use internals to avoid unnecessary recoding + // ### FIXME: actually do it + if (url.hasQuery()) + d = new QUrlQueryPrivate(QString::fromUtf8(url.encodedQuery())); +} + +/*! + Copies the contents of the \a other QUrlQuery object, including the query + delimiters. +*/ +QUrlQuery::QUrlQuery(const QUrlQuery &other) + : d(other.d) +{ +} + +/*! + Copies the contents of the \a other QUrlQuery object, including the query + delimiters. +*/ +QUrlQuery &QUrlQuery::operator =(const QUrlQuery &other) +{ + d = other.d; + return *this; +} + +/*! + Destroys this QUrlQuery object. +*/ +QUrlQuery::~QUrlQuery() +{ + // d auto-deletes +} + +/*! + Returns true if this object and the \a other object contain the same + contents, in the same order, and use the same query delimiters. +*/ +bool QUrlQuery::operator ==(const QUrlQuery &other) const +{ + if (d == other.d) + return true; + if (d && other.d) + return d->valueDelimiter == other.d->valueDelimiter && + d->pairDelimiter == other.d->pairDelimiter && + d->itemList == other.d->itemList; + return false; +} + +/*! + Returns true if this QUrlQUery object contains no key-value pairs, such as + after being default-constructed or after parsing an empty query string. + + \sa setQuery(), clear() +*/ +bool QUrlQuery::isEmpty() const +{ + return d ? d->itemList.isEmpty() : true; +} + +/*! + \internal +*/ +bool QUrlQuery::isDetached() const +{ + return d && d->ref.load() == 1; +} + +/*! + Clears this QUrlQuery object by removing all of the key-value pairs + currently stored. If the query delimiters have been changed, this function + will leave them with their changed values. + + \sa isEmpty(), setQueryDelimiters() +*/ +void QUrlQuery::clear() +{ + if (d.constData()) + d->itemList.clear(); +} + +/*! + Parses the query string in \a queryString and sets the internal items to + the values found there. If any delimiters have been specified with + setQueryDelimiters(), this function will use them instead of the default + delimiters to parse the string. +*/ +void QUrlQuery::setQuery(const QString &queryString) +{ + d->setQuery(queryString); +} + +static void recodeAndAppend(QString &to, const QString &input, + QUrl::ComponentFormattingOptions encoding, const ushort *tableModifications) +{ + if (!qt_urlRecode(to, input.constData(), input.constData() + input.length(), encoding, tableModifications)) + to += input; +} + +/*! + Returns the reconstructed query string, formed from the key-value pairs + currently stored in this QUrlQuery object and separated by the query + delimiters chosen for this object. The keys and values are encoded using + the options given by the \a encoding paramter. + + For this function, the only ambiguous delimiter is the hash ("#"), as in + URLs it is used to separate the query string from the fragment that may + follow. + + The order of the key-value pairs in the returned string is exactly the same + as in the original query. + + \sa setQuery(), QUrl::setQuery(), QUrl::fragment(), \l{#Encoding}{Encoding} +*/ +QString QUrlQuery::query(QUrl::ComponentFormattingOptions encoding) const +{ + if (!d) + return QString(); + + // unlike the component encoding, for the whole query we need to modify a little: + // - the "#" character is ambiguous, so we decode it only in DecodeAllDelimiters mode + // - the query delimiter pair must always be encoded + // - the "[" and "]" sub-delimiters and the non-delimiters very on DecodeUnambiguousDelimiters + // so: + // - full encoding: encode the non-delimiters, the pair, "#", "[" and "]" + // - pretty decode: decode the non-delimiters, "[" and "]"; encode the pair and "#" + // - decode all: decode the non-delimiters, "[", "]", "#"; encode the pair + + // start with what's always encoded + ushort tableActions[7] = { + leave('+'), // 0 + encode(d->pairDelimiter.unicode()), // 1 + encode(d->valueDelimiter.unicode()), // 2 + }; + if ((encoding & QUrl::DecodeAllDelimiters) == QUrl::DecodeAllDelimiters) { + // full decoding: we only encode the characters above + tableActions[3] = 0; + encoding |= QUrl::DecodeAllDelimiters; + } else { + tableActions[3] = encode('#'); + if (encoding & QUrl::DecodeUnambiguousDelimiters) { + tableActions[4] = 0; + encoding |= QUrl::DecodeAllDelimiters; + } else { + tableActions[4] = encode('['); + tableActions[5] = encode(']'); + tableActions[6] = 0; + encoding &= ~QUrl::DecodeAllDelimiters; + } + } + + QString result; + Map::const_iterator it = d->itemList.constBegin(); + Map::const_iterator end = d->itemList.constEnd(); + + { + int size = 0; + for ( ; it != end; ++it) + size += it->first.length() + 1 + it->second.length() + 1; + result.reserve(size + size / 4); + } + + for (it = d->itemList.constBegin(); it != end; ++it) { + if (!result.isEmpty()) + result += QChar(d->pairDelimiter); + recodeAndAppend(result, it->first, encoding, tableActions); + if (!it->second.isNull()) { + result += QChar(d->valueDelimiter); + recodeAndAppend(result, it->second, encoding, tableActions); + } + } + return result; +} + +/*! + Sets the characters used for delimiting between keys and values, + and between key-value pairs in the URL's query string. The default + value delimiter is '=' and the default pair delimiter is '&'. + + \img qurl-querystring.png + + \a valueDelimiter will be used for separating keys from values, + and \a pairDelimiter will be used to separate key-value pairs. + Any occurrences of these delimiting characters in the encoded + representation of the keys and values of the query string are + percent encoded when returned in query(). + + If \a valueDelimiter is set to '(' and \a pairDelimiter is ')', + the above query string would instead be represented like this: + + \snippet doc/src/snippets/code/src_corelib_io_qurl.cpp 4 + + \note Non-standard delimiters should be chosen from among what RFC 3986 calls + "sub-delimiters". They are: + + \code + sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + / "*" / "+" / "," / ";" / "=" + \endcode + + Use of other characters is not supported and may result in unexpected + behaviour. This method does not verify that you passed a valid delimiter. + + \sa queryValueDelimiter(), queryPairDelimiter() +*/ +void QUrlQuery::setQueryDelimiters(QChar valueDelimiter, QChar pairDelimiter) +{ + d->valueDelimiter = valueDelimiter.unicode(); + d->pairDelimiter = pairDelimiter.unicode(); +} + +/*! + Returns the character used to delimit between keys and values when + reconstructing the query string in query() or when parsing in setQuery(). + + \sa setQueryDelimiters(), queryPairDelimiter() +*/ +QChar QUrlQuery::queryValueDelimiter() const +{ + return d ? d->valueDelimiter : defaultQueryValueDelimiter(); +} + +/*! + Returns the character used to delimit between keys-value pairs when + reconstructing the query string in query() or when parsing in setQuery(). + + \sa setQueryDelimiters(), queryValueDelimiter() +*/ +QChar QUrlQuery::queryPairDelimiter() const +{ + return d ? d->pairDelimiter : defaultQueryPairDelimiter(); +} + +/*! + Sets the items in this QUrlQuery object to \a query. The order of the + elements in \a query is preserved. + + \note This method does not treat spaces (ASCII 0x20) and plus ("+") signs + as the same, like HTML forms do. If you need spaces to be represented as + plus signs, use actual plus signs. + + \sa queryItems(), isEmpty() +*/ +void QUrlQuery::setQueryItems(const QList<QPair<QString, QString> > &query) +{ + clear(); + if (query.isEmpty()) + return; + + QUrlQueryPrivate *dd = d; + QList<QPair<QString, QString> >::const_iterator it = query.constBegin(), + end = query.constEnd(); + for ( ; it != end; ++it) + dd->addQueryItem(it->first, it->second); +} + +/*! + Returns the query string of the URL, as a map of keys and values, using the + options specified in \a encoding to encode the items. The order of the + elements is the same as the one found in the query string or set with + setQueryItems(). + + \sa setQueryItems(), \l{#Encoding}{Encoding} +*/ +QList<QPair<QString, QString> > QUrlQuery::queryItems(QUrl::ComponentFormattingOptions encoding) const +{ + if (!d) + return QList<QPair<QString, QString> >(); + if (idempotentRecodeToUser(encoding)) + return d->itemList; + + QList<QPair<QString, QString> > result; + Map::const_iterator it = d->itemList.constBegin(); + Map::const_iterator end = d->itemList.constEnd(); + for ( ; it != end; ++it) + result << qMakePair(d->recodeToUser(it->first, encoding), + d->recodeToUser(it->second, encoding)); + return result; +} + +/*! + Returns true if there is a query string pair whose key is equal + to \a key from the URL. + + \sa addQueryItem(), queryItemValue() +*/ +bool QUrlQuery::hasQueryItem(const QString &key) const +{ + if (!d) + return false; + return d->findKey(key) != d->itemList.constEnd(); +} + +/*! + Appends the pair \a key = \a value to the end of the query string of the + URL. This method does not overwrite existing items that might exist with + the same key. + + \note This method does not treat spaces (ASCII 0x20) and plus ("+") signs + as the same, like HTML forms do. If you need spaces to be represented as + plus signs, use actual plus signs. + + \sa hasQueryItem(), queryItemValue() +*/ +void QUrlQuery::addQueryItem(const QString &key, const QString &value) +{ + d->addQueryItem(key, value); +} + +/*! + Returns the query value associated with key \a key from the URL, using the + options specified in \a encoding to encode the return value. If the key \a + key is not found, this function returns an empty string. If you need to + distinguish between an empty value and a non-existent key, you should check + for the key's presence first using hasQueryItem(). + + If the key \a key is multiply defined, this function will return the first + one found, in the order they were present in the query string or added + using addQueryItem(). + + \sa addQueryItem(), allQueryItemValues(), \l{#Encoding}{Encoding} +*/ +QString QUrlQuery::queryItemValue(const QString &key, QUrl::ComponentFormattingOptions encoding) const +{ + QString result; + if (d) { + Map::const_iterator it = d->findKey(key); + if (it != d->itemList.constEnd()) + result = d->recodeToUser(it->second, encoding); + } + return result; +} + +/*! + Returns the a list of query string values whose key is equal to \a key from + the URL, using the options specified in \a encoding to encode the return + value. If the key \a key is not found, this function returns an empty list. + + \sa queryItemValue(), addQueryItem() +*/ +QStringList QUrlQuery::allQueryItemValues(const QString &key, QUrl::ComponentFormattingOptions encoding) const +{ + QStringList result; + if (d) { + QString encodedKey = d->recodeFromUser(key); + int idx = d->findRecodedKey(encodedKey); + while (idx < d->itemList.size()) { + result << d->recodeToUser(d->itemList.at(idx).second, encoding); + idx = d->findRecodedKey(encodedKey, idx + 1); + } + } + return result; +} + +/*! + Removes the query string pair whose key is equal to \a key from the URL. If + there are multiple items with a key equal to \a key, it removes the first + item in the order they were present in the query string or added with + addQueryItem(). + + \sa removeAllQueryItems() +*/ +void QUrlQuery::removeQueryItem(const QString &key) +{ + if (d) { + Map::iterator it = d->findKey(key); + if (it != d->itemList.end()) + d->itemList.erase(it); + } +} + +/*! + Removes all the query string pairs whose key is equal to \a key + from the URL. + + \sa removeQueryItem() +*/ +void QUrlQuery::removeAllQueryItems(const QString &key) +{ + if (d.constData()) { + QString encodedKey = d->recodeFromUser(key); + Map::iterator it = d->itemList.begin(); + while (it != d->itemList.end()) { + if (it->first == encodedKey) + it = d->itemList.erase(it); + else + ++it; + } + } +} + +/*! + \fn QChar QUrlQuery::defaultQueryValueDelimiter() + Returns the default character for separating keys from values in the query, + an equal sign ("="). + + \sa setQueryDelimiters(), queryValueDelimiter(), defaultQueryPairDelimiter() +*/ + +/*! + \fn QChar QUrlQuery::defaultQueryPairDelimiter() + Returns the default character for separating keys-value pairs from each + other, an ampersand ("&"). + + \sa setQueryDelimiters(), queryPairDelimiter(), defaultQueryValueDelimiter() +*/ + +QT_END_NAMESPACE diff --git a/src/corelib/io/qurlquery.h b/src/corelib/io/qurlquery.h new file mode 100644 index 0000000000..e2b28f78e7 --- /dev/null +++ b/src/corelib/io/qurlquery.h @@ -0,0 +1,117 @@ +/**************************************************************************** +** +** Copyright (C) 2012 Intel Corporation +** Contact: http://www.qt-project.org/ +** +** This file is part of the QtCore module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** GNU Lesser General Public License Usage +** This file may be used under the terms of the GNU Lesser General Public +** License version 2.1 as published by the Free Software Foundation and +** appearing in the file LICENSE.LGPL included in the packaging of this +** file. Please review the following information to ensure the GNU Lesser +** General Public License version 2.1 requirements will be met: +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU General +** Public License version 3.0 as published by the Free Software Foundation +** and appearing in the file LICENSE.GPL included in the packaging of this +** file. Please review the following information to ensure the GNU General +** Public License version 3.0 requirements will be met: +** http://www.gnu.org/copyleft/gpl.html. +** +** Other Usage +** Alternatively, this file may be used in accordance with the terms and +** conditions contained in a signed written agreement between you and Nokia. +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QURLQUERY_H +#define QURLQUERY_H + +#include <QtCore/qpair.h> +#include <QtCore/qshareddata.h> +#include <QtCore/qurl.h> + +QT_BEGIN_HEADER + +QT_BEGIN_NAMESPACE + +class QUrlQueryPrivate; +class Q_CORE_EXPORT QUrlQuery +{ +public: + QUrlQuery(); + explicit QUrlQuery(const QUrl &url); + explicit QUrlQuery(const QString &queryString); + QUrlQuery(const QUrlQuery &other); + QUrlQuery &operator=(const QUrlQuery &other); +#ifdef Q_COMPILER_RVALUE_REFS + QUrlQuery &operator=(QUrlQuery &&other) + { qSwap(d, other.d); return *this; } +#endif + ~QUrlQuery(); + + bool operator==(const QUrlQuery &other) const; + bool operator!=(const QUrlQuery &other) const + { return !(*this == other); } + + void swap(QUrlQuery &other) { qSwap(d, other.d); } + + bool isEmpty() const; + bool isDetached() const; + void clear(); + + QString query(QUrl::ComponentFormattingOptions encoding = QUrl::PrettyDecoded) const; + void setQuery(const QString &queryString); + QString toString(QUrl::ComponentFormattingOptions encoding = QUrl::PrettyDecoded) const + { return query(encoding); } + + void setQueryDelimiters(QChar valueDelimiter, QChar pairDelimiter); + QChar queryValueDelimiter() const; + QChar queryPairDelimiter() const; + + void setQueryItems(const QList<QPair<QString, QString> > &query); + QList<QPair<QString, QString> > queryItems(QUrl::ComponentFormattingOptions encoding = QUrl::PrettyDecoded) const; + + bool hasQueryItem(const QString &key) const; + void addQueryItem(const QString &key, const QString &value); + void removeQueryItem(const QString &key); + QString queryItemValue(const QString &key, QUrl::ComponentFormattingOptions encoding = QUrl::PrettyDecoded) const; + QStringList allQueryItemValues(const QString &key, QUrl::ComponentFormattingOptions encoding = QUrl::PrettyDecoded) const; + void removeAllQueryItems(const QString &key); + + static QChar defaultQueryValueDelimiter() + { return QChar(ushort('=')); } + static QChar defaultQueryPairDelimiter() + { return QChar(ushort('&')); } + +private: + friend class QUrl; + QSharedDataPointer<QUrlQueryPrivate> d; +public: + typedef QSharedDataPointer<QUrlQueryPrivate> DataPtr; + inline DataPtr &data_ptr() { return d; } +}; + +Q_DECLARE_TYPEINFO(QUrlQuery, Q_MOVABLE_TYPE); +Q_DECLARE_SHARED(QUrlQuery) + +QT_END_NAMESPACE + +QT_END_HEADER + +#endif // QURLQUERY_H diff --git a/tests/auto/corelib/io/qurlquery/qurlquery.pro b/tests/auto/corelib/io/qurlquery/qurlquery.pro new file mode 100644 index 0000000000..d344e48337 --- /dev/null +++ b/tests/auto/corelib/io/qurlquery/qurlquery.pro @@ -0,0 +1,5 @@ +QT = core core-private testlib +TARGET = tst_qurlquery +CONFIG += parallel_test testcase +SOURCES += tst_qurlquery.cpp +DEFINES += SRCDIR=\\\"$$PWD/\\\" diff --git a/tests/auto/corelib/io/qurlquery/tst_qurlquery.cpp b/tests/auto/corelib/io/qurlquery/tst_qurlquery.cpp new file mode 100644 index 0000000000..4ce621c4ba --- /dev/null +++ b/tests/auto/corelib/io/qurlquery/tst_qurlquery.cpp @@ -0,0 +1,695 @@ +/**************************************************************************** +** +** Copyright (C) 2012 Intel Corporation. +** Contact: http://www.qt-project.org/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** GNU Lesser General Public License Usage +** This file may be used under the terms of the GNU Lesser General Public +** License version 2.1 as published by the Free Software Foundation and +** appearing in the file LICENSE.LGPL included in the packaging of this +** file. Please review the following information to ensure the GNU Lesser +** General Public License version 2.1 requirements will be met: +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU General +** Public License version 3.0 as published by the Free Software Foundation +** and appearing in the file LICENSE.GPL included in the packaging of this +** file. Please review the following information to ensure the GNU General +** Public License version 3.0 requirements will be met: +** http://www.gnu.org/copyleft/gpl.html. +** +** Other Usage +** Alternatively, this file may be used in accordance with the terms and +** conditions contained in a signed written agreement between you and Nokia. +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QtCore/QUrlQuery> +#include <QtTest/QtTest> + +typedef QList<QPair<QString, QString> > QueryItems; +Q_DECLARE_METATYPE(QueryItems) +Q_DECLARE_METATYPE(QUrl::ComponentFormattingOptions) + +class tst_QUrlQuery : public QObject +{ + Q_OBJECT + +public: + tst_QUrlQuery() + { + qRegisterMetaType<QueryItems>(); + } + +private Q_SLOTS: + void constructing(); + void addRemove(); + void multiAddRemove(); + void multiplyAddSamePair(); + void setQueryItems_data(); + void setQueryItems(); + void basicParsing_data(); + void basicParsing(); + void reconstructQuery_data(); + void reconstructQuery(); + void encodedSetQueryItems_data(); + void encodedSetQueryItems(); + void encodedParsing_data(); + void encodedParsing(); + void differentDelimiters(); +}; + +static QString prettyElement(const QString &key, const QString &value) +{ + QString result; + if (key.isNull()) + result += "null -> "; + else + result += '"' % key % "\" -> "; + if (value.isNull()) + result += "null"; + else + result += '"' % value % '"'; + return result; +} + +static QString prettyPair(QList<QPair<QString, QString> >::const_iterator it) +{ + return prettyElement(it->first, it->second); +} + +template <typename T> +static QByteArray prettyList(const T &items) +{ + QString result = "("; + bool first = true; + typename T::const_iterator it = items.constBegin(); + for ( ; it != items.constEnd(); ++it) { + if (!first) + result += ", "; + first = false; + result += prettyPair(it); + } + result += ")"; + return result.toLocal8Bit(); +} + +static bool compare(const QList<QPair<QString, QString> > &actual, const QueryItems &expected, + const char *actualStr, const char *expectedStr, const char *file, int line) +{ + return QTest::compare_helper(actual == expected, "Compared values are not the same", + qstrdup(prettyList(actual)), qstrdup(prettyList(expected).data()), + actualStr, expectedStr, file, line); +} + +#define COMPARE_ITEMS(actual, expected) \ + do { \ + if (!compare(actual, expected, #actual, #expected, __FILE__, __LINE__)) \ + return; \ + } while (0) + +inline QueryItems operator+(QueryItems items, const QPair<QString, QString> &pair) +{ + // items is already a copy + items.append(pair); + return items; +} + +inline QueryItems operator+(const QPair<QString, QString> &pair, QueryItems items) +{ + // items is already a copy + items.prepend(pair); + return items; +} + +inline QPair<QString, QString> qItem(const QString &first, const QString &second) +{ + return qMakePair(first, second); +} + +inline QPair<QString, QString> qItem(const char *first, const QString &second) +{ + return qMakePair(QString::fromUtf8(first), second); +} + +inline QPair<QString, QString> qItem(const char *first, const char *second) +{ + return qMakePair(QString::fromUtf8(first), QString::fromUtf8(second)); +} + +inline QPair<QString, QString> qItem(const QString &first, const char *second) +{ + return qMakePair(first, QString::fromUtf8(second)); +} + +static QUrlQuery emptyQuery() +{ + return QUrlQuery(); +} + +void tst_QUrlQuery::constructing() +{ + QUrlQuery empty; + QVERIFY(empty.isEmpty()); + QCOMPARE(empty.queryPairDelimiter(), QUrlQuery::defaultQueryPairDelimiter()); + QCOMPARE(empty.queryValueDelimiter(), QUrlQuery::defaultQueryValueDelimiter()); + // undefined whether it is detached, but don't crash + QVERIFY(empty.isDetached() || !empty.isDetached()); + + empty.clear(); + QVERIFY(empty.isEmpty()); + + { + QUrlQuery copy(empty); + QVERIFY(copy.isEmpty()); + QVERIFY(!copy.isDetached()); + QVERIFY(copy == empty); + QVERIFY(!(copy != empty)); + + copy = empty; + QVERIFY(copy == empty); + + copy = QUrlQuery(); + QVERIFY(copy == empty); + } + { + QUrlQuery copy(emptyQuery()); + QVERIFY(copy == empty); + } + + QVERIFY(!empty.hasQueryItem("a")); + QVERIFY(empty.queryItemValue("a").isEmpty()); + QVERIFY(empty.allQueryItemValues("a").isEmpty()); + + QVERIFY(!empty.hasQueryItem("")); + QVERIFY(empty.queryItemValue("").isEmpty()); + QVERIFY(empty.allQueryItemValues("").isEmpty()); + + QVERIFY(!empty.hasQueryItem(QString())); + QVERIFY(empty.queryItemValue(QString()).isEmpty()); + QVERIFY(empty.allQueryItemValues(QString()).isEmpty()); + + QVERIFY(empty.queryItems().isEmpty()); + + QUrlQuery other; + other.addQueryItem("a", "b"); + QVERIFY(!other.isEmpty()); + QVERIFY(other.isDetached()); + QVERIFY(other != empty); + QVERIFY(!(other == empty)); + + QUrlQuery copy(other); + QVERIFY(copy == other); + + copy.clear(); + QVERIFY(copy.isEmpty()); + QVERIFY(copy != other); + + copy = other; + QVERIFY(!copy.isEmpty()); + QVERIFY(copy == other); + + copy = QUrlQuery(); + QVERIFY(copy.isEmpty()); + + empty.setQueryDelimiters('(', ')'); + QCOMPARE(empty.queryValueDelimiter(), QChar(QLatin1Char('('))); + QCOMPARE(empty.queryPairDelimiter(), QChar(QLatin1Char(')'))); +} + +void tst_QUrlQuery::addRemove() +{ + QUrlQuery query; + + { + // one item + query.addQueryItem("a", "b"); + QVERIFY(!query.isEmpty()); + QVERIFY(query.hasQueryItem("a")); + QCOMPARE(query.queryItemValue("a"), QString("b")); + QCOMPARE(query.allQueryItemValues("a"), QStringList() << "b"); + + QList<QPair<QString, QString> > allItems = query.queryItems(); + QCOMPARE(allItems.count(), 1); + QCOMPARE(allItems.at(0).first, QString("a")); + QCOMPARE(allItems.at(0).second, QString("b")); + } + + QUrlQuery original = query; + + { + // two items + query.addQueryItem("c", "d"); + QVERIFY(query.hasQueryItem("a")); + QCOMPARE(query.queryItemValue("a"), QString("b")); + QCOMPARE(query.allQueryItemValues("a"), QStringList() << "b"); + QVERIFY(query.hasQueryItem("c")); + QCOMPARE(query.queryItemValue("c"), QString("d")); + QCOMPARE(query.allQueryItemValues("c"), QStringList() << "d"); + + QList<QPair<QString, QString> > allItems = query.queryItems(); + QCOMPARE(allItems.count(), 2); + QVERIFY(allItems.contains(qItem("a", "b"))); + QVERIFY(allItems.contains(qItem("c", "d"))); + + QVERIFY(query != original); + QVERIFY(!(query == original)); + } + + { + // remove an item that isn't there + QUrlQuery copy = query; + query.removeQueryItem("e"); + QCOMPARE(query, copy); + } + + { + // remove an item + query.removeQueryItem("c"); + QVERIFY(query.hasQueryItem("a")); + QCOMPARE(query.queryItemValue("a"), QString("b")); + QCOMPARE(query.allQueryItemValues("a"), QStringList() << "b"); + + QList<QPair<QString, QString> > allItems = query.queryItems(); + QCOMPARE(allItems.count(), 1); + QCOMPARE(allItems.at(0).first, QString("a")); + QCOMPARE(allItems.at(0).second, QString("b")); + + QVERIFY(query == original); + QVERIFY(!(query != original)); + } + + { + // add an item with en empty value + QString emptyButNotNull(0, Qt::Uninitialized); + QVERIFY(emptyButNotNull.isEmpty()); + QVERIFY(!emptyButNotNull.isNull()); + + query.addQueryItem("e", ""); + QVERIFY(query.hasQueryItem("a")); + QCOMPARE(query.queryItemValue("a"), QString("b")); + QCOMPARE(query.allQueryItemValues("a"), QStringList() << "b"); + QVERIFY(query.hasQueryItem("e")); + QCOMPARE(query.queryItemValue("e"), emptyButNotNull); + QCOMPARE(query.allQueryItemValues("e"), QStringList() << emptyButNotNull); + + QList<QPair<QString, QString> > allItems = query.queryItems(); + QCOMPARE(allItems.count(), 2); + QVERIFY(allItems.contains(qItem("a", "b"))); + QVERIFY(allItems.contains(qItem("e", emptyButNotNull))); + + QVERIFY(query != original); + QVERIFY(!(query == original)); + } + + { + // remove the items + query.removeQueryItem("a"); + query.removeQueryItem("e"); + QVERIFY(query.isEmpty()); + } +} + +void tst_QUrlQuery::multiAddRemove() +{ + QUrlQuery query; + + { + // one item, two values + query.addQueryItem("a", "b"); + query.addQueryItem("a", "c"); + QVERIFY(!query.isEmpty()); + QVERIFY(query.hasQueryItem("a")); + + // returns the first one + QVERIFY(query.queryItemValue("a") == "b"); + + // order is the order we set them in + QVERIFY(query.allQueryItemValues("a") == QStringList() << "b" << "c"); + } + + { + // add another item, two values + query.addQueryItem("A", "B"); + query.addQueryItem("A", "C"); + QVERIFY(query.hasQueryItem("A")); + QVERIFY(query.hasQueryItem("a")); + + QVERIFY(query.queryItemValue("a") == "b"); + QVERIFY(query.allQueryItemValues("a") == QStringList() << "b" << "c"); + QVERIFY(query.queryItemValue("A") == "B"); + QVERIFY(query.allQueryItemValues("A") == QStringList() << "B" << "C"); + } + + { + // remove one of the original items + query.removeQueryItem("a"); + QVERIFY(query.hasQueryItem("a")); + + // it must have removed the first one + QVERIFY(query.queryItemValue("a") == "c"); + } + + { + // remove the items we added later + query.removeAllQueryItems("A"); + QVERIFY(!query.isEmpty()); + QVERIFY(!query.hasQueryItem("A")); + } + + { + // add one element to the current, then remove them + query.addQueryItem("a", "d"); + query.removeAllQueryItems("a"); + QVERIFY(!query.hasQueryItem("a")); + QVERIFY(query.isEmpty()); + } +} + +void tst_QUrlQuery::multiplyAddSamePair() +{ + QUrlQuery query; + query.addQueryItem("a", "a"); + query.addQueryItem("a", "a"); + QCOMPARE(query.allQueryItemValues("a"), QStringList() << "a" << "a"); + + query.addQueryItem("a", "a"); + QCOMPARE(query.allQueryItemValues("a"), QStringList() << "a" << "a" << "a"); + + query.removeQueryItem("a"); + QCOMPARE(query.allQueryItemValues("a"), QStringList() << "a" << "a"); +} + +void tst_QUrlQuery::setQueryItems_data() +{ + QTest::addColumn<QueryItems>("items"); + QString emptyButNotNull(0, Qt::Uninitialized); + + QTest::newRow("empty") << QueryItems(); + QTest::newRow("1-novalue") << (QueryItems() << qItem("a", QString())); + QTest::newRow("1-emptyvalue") << (QueryItems() << qItem("a", emptyButNotNull)); + + QueryItems list; + list << qItem("a", "b"); + QTest::newRow("1-value") << list; + QTest::newRow("1-multi") << (list + qItem("a", "c")); + QTest::newRow("1-duplicated") << (list + qItem("a", "b")); + + list << qItem("c", "d"); + QTest::newRow("2") << list; + + list << qItem("c", "e"); + QTest::newRow("2-multi") << list; +} + +void tst_QUrlQuery::setQueryItems() +{ + QFETCH(QueryItems, items); + QUrlQuery query; + + QueryItems::const_iterator it = items.constBegin(); + for ( ; it != items.constEnd(); ++it) + query.addQueryItem(it->first, it->second); + COMPARE_ITEMS(query.queryItems(), items); + + query.clear(); + + query.setQueryItems(items); + COMPARE_ITEMS(query.queryItems(), items); +} + +void tst_QUrlQuery::basicParsing_data() +{ + QTest::addColumn<QString>("queryString"); + QTest::addColumn<QueryItems>("items"); + QString emptyButNotNull(0, Qt::Uninitialized); + + QTest::newRow("null") << QString() << QueryItems(); + QTest::newRow("empty") << "" << QueryItems(); + + QTest::newRow("1-novalue") << "a" << (QueryItems() << qItem("a", QString())); + QTest::newRow("1-emptyvalue") << "a=" << (QueryItems() << qItem("a", emptyButNotNull)); + QTest::newRow("1-value") << "a=b" << (QueryItems() << qItem("a", "b")); + + // some longer keys + QTest::newRow("1-longkey-novalue") << "thisisalongkey" << (QueryItems() << qItem("thisisalongkey", QString())); + QTest::newRow("1-longkey-emptyvalue") << "thisisalongkey=" << (QueryItems() << qItem("thisisalongkey", emptyButNotNull)); + QTest::newRow("1-longkey-value") << "thisisalongkey=b" << (QueryItems() << qItem("thisisalongkey", "b")); + + // longer values + QTest::newRow("1-longvalue-value") << "a=thisisalongreasonablyvalue" + << (QueryItems() << qItem("a", "thisisalongreasonablyvalue")); + QTest::newRow("1-longboth-value") << "thisisalongkey=thisisalongreasonablyvalue" + << (QueryItems() << qItem("thisisalongkey", "thisisalongreasonablyvalue")); + + // two or more entries + QueryItems baselist; + baselist << qItem("a", "b") << qItem("c", "d"); + QTest::newRow("2-ab-cd") << "a=b&c=d" << baselist; + QTest::newRow("2-cd-ab") << "c=d&a=b" << (QueryItems() << qItem("c", "d") << qItem("a", "b")); + + // the same entry multiply defined + QTest::newRow("2-a-a") << "a&a" << (QueryItems() << qItem("a", QString()) << qItem("a", QString())); + QTest::newRow("2-ab-a") << "a=b&a" << (QueryItems() << qItem("a", "b") << qItem("a", QString())); + QTest::newRow("2-ab-ab") << "a=b&a=b" << (QueryItems() << qItem("a", "b") << qItem("a", "b")); + QTest::newRow("2-ab-ac") << "a=b&a=c" << (QueryItems() << qItem("a", "b") << qItem("a", "c")); + + QPair<QString, QString> novalue = qItem("somekey", QString()); + QueryItems list2 = baselist + novalue; + QTest::newRow("3-novalue-ab-cd") << "somekey&a=b&c=d" << (novalue + baselist); + QTest::newRow("3-ab-novalue-cd") << "a=b&somekey&c=d" << (QueryItems() << qItem("a", "b") << novalue << qItem("c", "d")); + QTest::newRow("3-ab-cd-novalue") << "a=b&c=d&somekey" << list2; + + list2 << qItem("otherkeynovalue", QString()); + QTest::newRow("4-ab-cd-novalue-novalue") << "a=b&c=d&somekey&otherkeynovalue" << list2; + + QPair<QString, QString> emptyvalue = qItem("somekey", emptyButNotNull); + list2 = baselist + emptyvalue; + QTest::newRow("3-emptyvalue-ab-cd") << "somekey=&a=b&c=d" << (emptyvalue + baselist); + QTest::newRow("3-ab-emptyvalue-cd") << "a=b&somekey=&c=d" << (QueryItems() << qItem("a", "b") << emptyvalue << qItem("c", "d")); + QTest::newRow("3-ab-cd-emptyvalue") << "a=b&c=d&somekey=" << list2; +} + +void tst_QUrlQuery::basicParsing() +{ + QFETCH(QString, queryString); + QFETCH(QueryItems, items); + + QUrlQuery query(queryString); + QCOMPARE(query.isEmpty(), items.isEmpty()); + COMPARE_ITEMS(query.queryItems(), items); +} + +void tst_QUrlQuery::reconstructQuery_data() +{ + QTest::addColumn<QString>("queryString"); + QTest::addColumn<QueryItems>("items"); + QString emptyButNotNull(0, Qt::Uninitialized); + + QTest::newRow("null") << QString() << QueryItems(); + QTest::newRow("empty") << "" << QueryItems(); + + QTest::newRow("1-novalue") << "a" << (QueryItems() << qItem("a", QString())); + QTest::newRow("1-emptyvalue") << "a=" << (QueryItems() << qItem("a", emptyButNotNull)); + QTest::newRow("1-value") << "a=b" << (QueryItems() << qItem("a", "b")); + + // some longer keys + QTest::newRow("1-longkey-novalue") << "thisisalongkey" << (QueryItems() << qItem("thisisalongkey", QString())); + QTest::newRow("1-longkey-emptyvalue") << "thisisalongkey=" << (QueryItems() << qItem("thisisalongkey", emptyButNotNull)); + QTest::newRow("1-longkey-value") << "thisisalongkey=b" << (QueryItems() << qItem("thisisalongkey", "b")); + + // longer values + QTest::newRow("1-longvalue-value") << "a=thisisalongreasonablyvalue" + << (QueryItems() << qItem("a", "thisisalongreasonablyvalue")); + QTest::newRow("1-longboth-value") << "thisisalongkey=thisisalongreasonablyvalue" + << (QueryItems() << qItem("thisisalongkey", "thisisalongreasonablyvalue")); + + // two or more entries + QueryItems baselist; + baselist << qItem("a", "b") << qItem("c", "d"); + QTest::newRow("2-ab-cd") << "a=b&c=d" << baselist; + + // the same entry multiply defined + QTest::newRow("2-a-a") << "a&a" << (QueryItems() << qItem("a", QString()) << qItem("a", QString())); + QTest::newRow("2-ab-ab") << "a=b&a=b" << (QueryItems() << qItem("a", "b") << qItem("a", "b")); + QTest::newRow("2-ab-ac") << "a=b&a=c" << (QueryItems() << qItem("a", "b") << qItem("a", "c")); + QTest::newRow("2-ac-ab") << "a=c&a=b" << (QueryItems() << qItem("a", "c") << qItem("a", "b")); + QTest::newRow("2-ab-cd") << "a=b&c=d" << (QueryItems() << qItem("a", "b") << qItem("c", "d")); + QTest::newRow("2-cd-ab") << "c=d&a=b" << (QueryItems() << qItem("c", "d") << qItem("a", "b")); + + QueryItems list2 = baselist + qItem("somekey", QString()); + QTest::newRow("3-ab-cd-novalue") << "a=b&c=d&somekey" << list2; + + list2 << qItem("otherkeynovalue", QString()); + QTest::newRow("4-ab-cd-novalue-novalue") << "a=b&c=d&somekey&otherkeynovalue" << list2; + + list2 = baselist + qItem("somekey", emptyButNotNull); + QTest::newRow("3-ab-cd-emptyvalue") << "a=b&c=d&somekey=" << list2; +} + +void tst_QUrlQuery::reconstructQuery() +{ + QFETCH(QString, queryString); + QFETCH(QueryItems, items); + + QUrlQuery query; + + // add the items + for (QueryItems::ConstIterator it = items.constBegin(); it != items.constEnd(); ++it) { + query.addQueryItem(it->first, it->second); + } + QCOMPARE(query.query(), queryString); +} + +void tst_QUrlQuery::encodedSetQueryItems_data() +{ + QTest::addColumn<QString>("queryString"); + QTest::addColumn<QString>("key"); + QTest::addColumn<QString>("value"); + QTest::addColumn<QUrl::ComponentFormattingOptions>("encoding"); + QTest::addColumn<QString>("expectedQuery"); + QTest::addColumn<QString>("expectedKey"); + QTest::addColumn<QString>("expectedValue"); + typedef QUrl::ComponentFormattingOptions F; + + QTest::newRow("nul") << "f%00=bar%00" << "f%00" << "bar%00" << F(QUrl::PrettyDecoded) + << "f%00=bar%00" << "f%00" << "bar%00"; + QTest::newRow("non-decodable-1") << "foo%01%7f=b%1ar" << "foo%01%7f" << "b%1ar" << F(QUrl::PrettyDecoded) + << "foo%01%7F=b%1Ar" << "foo%01%7F" << "b%1Ar"; + QTest::newRow("non-decodable-2") << "foo\x01\x7f=b\x1ar" << "foo\x01\x7f" << "b\x1Ar" << F(QUrl::PrettyDecoded) + << "foo%01%7F=b%1Ar" << "foo%01%7F" << "b%1Ar"; + + QTest::newRow("space") << "%20=%20" << "%20" << "%20" << F(QUrl::PrettyDecoded) + << " = " << " " << " "; + QTest::newRow("encode-space") << " = " << " " << " " << F(QUrl::FullyEncoded) + << "%20=%20" << "%20" << "%20"; + + QTest::newRow("non-delimiters") << "%3C%5C%3E=%7B%7C%7D%5E%60" << "%3C%5C%3E" << "%7B%7C%7D%5E%60" << F(QUrl::PrettyDecoded) + << "<\\>={|}^`" << "<\\>" << "{|}^`"; + QTest::newRow("encode-non-delimiters") << "<\\>={|}^`" << "<\\>" << "{|}^`" << F(QUrl::FullyEncoded) + << "%3C%5C%3E=%7B%7C%7D%5E%60" << "%3C%5C%3E" << "%7B%7C%7D%5E%60"; + + QTest::newRow("equals") << "%3D=%3D" << "%3D" << "%3D" << F(QUrl::PrettyDecoded) + << "%3D=%3D" << "=" << "="; + QTest::newRow("equals-2") << "%3D==" << "=" << "=" << F(QUrl::PrettyDecoded) + << "%3D=%3D" << "=" << "="; + QTest::newRow("ampersand") << "%26=%26" << "%26" << "%26" << F(QUrl::PrettyDecoded) + << "%26=%26" << "&" << "&"; + QTest::newRow("hash") << "#=#" << "%23" << "%23" << F(QUrl::PrettyDecoded) + << "%23=%23" << "#" << "#"; + QTest::newRow("decode-hash") << "%23=%23" << "%23" << "%23" << F(QUrl::DecodeAllDelimiters) + << "#=#" << "#" << "#"; + + QTest::newRow("percent") << "%25=%25" << "%25" << "%25" << F(QUrl::PrettyDecoded) + << "%25=%25" << "%25" << "%25"; + QTest::newRow("bad-percent-1") << "%=%" << "%" << "%" << F(QUrl::PrettyDecoded) + << "%25=%25" << "%25" << "%25"; + QTest::newRow("bad-percent-2") << "%2=%2" << "%2" << "%2" << F(QUrl::PrettyDecoded) + << "%252=%252" << "%252" << "%252"; + + QTest::newRow("plus") << "+=+" << "+" << "+" << F(QUrl::PrettyDecoded) + << "+=+" << "+" << "+"; + QTest::newRow("2b") << "%2b=%2b" << "%2b" << "%2b" << F(QUrl::PrettyDecoded) + << "%2B=%2B" << "%2B" << "%2B"; + // plus signs must not be touched + QTest::newRow("encode-plus") << "+=+" << "+" << "+" << F(QUrl::FullyEncoded) + << "+=+" << "+" << "+"; + QTest::newRow("decode-2b") << "%2b=%2b" << "%2b" << "%2b" << F(QUrl::DecodeAllDelimiters) + << "%2B=%2B" << "%2B" << "%2B"; + + + QTest::newRow("unicode") << "q=R%C3%a9sum%c3%A9" << "q" << "R%C3%a9sum%c3%A9" << F(QUrl::PrettyDecoded) + << QString::fromUtf8("q=R\xc3\xa9sum\xc3\xa9") << "q" << QString::fromUtf8("R\xc3\xa9sum\xc3\xa9"); + QTest::newRow("encode-unicode") << QString::fromUtf8("q=R\xc3\xa9sum\xc3\xa9") << "q" << QString::fromUtf8("R\xc3\xa9sum\xc3\xa9") + << F(QUrl::FullyEncoded) + << "q=R%C3%A9sum%C3%A9" << "q" << "R%C3%A9sum%C3%A9"; +} + +void tst_QUrlQuery::encodedSetQueryItems() +{ + QFETCH(QString, key); + QFETCH(QString, value); + QFETCH(QString, expectedQuery); + QFETCH(QString, expectedKey); + QFETCH(QString, expectedValue); + QFETCH(QUrl::ComponentFormattingOptions, encoding); + QUrlQuery query; + + query.addQueryItem(key, value); + COMPARE_ITEMS(query.queryItems(encoding), QueryItems() << qItem(expectedKey, expectedValue)); + QCOMPARE(query.query(encoding), expectedQuery); +} + +void tst_QUrlQuery::encodedParsing_data() +{ + encodedSetQueryItems_data(); +} + +void tst_QUrlQuery::encodedParsing() +{ + QFETCH(QString, queryString); + QFETCH(QString, expectedQuery); + QFETCH(QString, expectedKey); + QFETCH(QString, expectedValue); + QFETCH(QUrl::ComponentFormattingOptions, encoding); + + QUrlQuery query(queryString); + COMPARE_ITEMS(query.queryItems(encoding), QueryItems() << qItem(expectedKey, expectedValue)); + QCOMPARE(query.query(encoding), expectedQuery); +} + +void tst_QUrlQuery::differentDelimiters() +{ + QUrlQuery query; + query.setQueryDelimiters('(', ')'); + + { + // parse: + query.setQuery("foo(bar)hello(world)"); + + QueryItems expected; + expected << qItem("foo", "bar") << qItem("hello", "world"); + COMPARE_ITEMS(query.queryItems(), expected); + COMPARE_ITEMS(query.queryItems(QUrl::FullyEncoded), expected); + COMPARE_ITEMS(query.queryItems(QUrl::DecodeAllDelimiters), expected); + } + + { + // reconstruct: + // note the final ')' is missing because there are no further items + QCOMPARE(query.query(), QString("foo(bar)hello(world")); + } + + { + // set items containing the new delimiters and the old ones + query.clear(); + query.addQueryItem("z(=)", "y(&)"); + QCOMPARE(query.query(), QString("z%28=%29(y%28&%29")); + + QUrlQuery copy = query; + QCOMPARE(query.query(), QString("z%28=%29(y%28&%29")); + + copy.setQueryDelimiters(QUrlQuery::defaultQueryValueDelimiter(), + QUrlQuery::defaultQueryPairDelimiter()); + QCOMPARE(copy.query(), QString("z(%3D)=y(%26)")); + } +} + +QTEST_APPLESS_MAIN(tst_QUrlQuery) + +#include "tst_qurlquery.moc" |