| 1 | // Copyright (C) 2022 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
| 3 | // Qt-Security score:critical reason:data-parser |
| 4 | |
| 5 | #include "qlocale_p.h" |
| 6 | |
| 7 | #include "qstringbuilder.h" |
| 8 | #include "qdatetime.h" |
| 9 | #include "qstringlist.h" |
| 10 | #include "qvariant.h" |
| 11 | #include "qreadwritelock.h" |
| 12 | |
| 13 | QT_BEGIN_NAMESPACE |
| 14 | |
| 15 | using namespace Qt::StringLiterals; |
| 16 | |
| 17 | #ifndef QT_NO_SYSTEMLOCALE |
| 18 | namespace { |
| 19 | struct QSystemLocaleData |
| 20 | { |
| 21 | QSystemLocaleData() |
| 22 | : lc_numeric(QLocale::C) |
| 23 | ,lc_time(QLocale::C) |
| 24 | ,lc_monetary(QLocale::C) |
| 25 | ,lc_messages(QLocale::C) |
| 26 | { |
| 27 | initFromEnvironmentUnprotected(); |
| 28 | } |
| 29 | |
| 30 | void readEnvironment(); |
| 31 | |
| 32 | QReadWriteLock lock; |
| 33 | |
| 34 | QLocale lc_numeric; |
| 35 | QLocale lc_time; |
| 36 | QLocale lc_monetary; |
| 37 | QLocale lc_messages; |
| 38 | QByteArray lc_messages_var; |
| 39 | QByteArray lc_measurement_var; |
| 40 | QByteArray lc_collate_var; |
| 41 | QStringList uiLanguages; |
| 42 | |
| 43 | private: |
| 44 | void initFromEnvironmentUnprotected(); |
| 45 | }; |
| 46 | |
| 47 | void QSystemLocaleData::readEnvironment() |
| 48 | { |
| 49 | QWriteLocker locker(&lock); |
| 50 | initFromEnvironmentUnprotected(); |
| 51 | } |
| 52 | |
| 53 | void QSystemLocaleData::initFromEnvironmentUnprotected() |
| 54 | { |
| 55 | // See https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html#tag_08_02 |
| 56 | // for the semantics of each of these: |
| 57 | QByteArray all = qgetenv(varName: "LC_ALL" ); |
| 58 | QByteArray numeric = all.isEmpty() ? qgetenv(varName: "LC_NUMERIC" ) : all; |
| 59 | QByteArray time = all.isEmpty() ? qgetenv(varName: "LC_TIME" ) : all; |
| 60 | QByteArray monetary = all.isEmpty() ? qgetenv(varName: "LC_MONETARY" ) : all; |
| 61 | lc_messages_var = all.isEmpty() ? qgetenv(varName: "LC_MESSAGES" ) : all; |
| 62 | lc_measurement_var = all.isEmpty() ? qgetenv(varName: "LC_MEASUREMENT" ) : all; |
| 63 | lc_collate_var = all.isEmpty() ? qgetenv(varName: "LC_COLLATE" ) : all; |
| 64 | QByteArray lang = qgetenv(varName: "LANG" ); |
| 65 | if (lang.isEmpty()) |
| 66 | lang = QByteArray("C" ); |
| 67 | if (numeric.isEmpty()) |
| 68 | numeric = lang; |
| 69 | if (time.isEmpty()) |
| 70 | time = lang; |
| 71 | if (monetary.isEmpty()) |
| 72 | monetary = lang; |
| 73 | if (lc_messages_var.isEmpty()) |
| 74 | lc_messages_var = lang; |
| 75 | if (lc_measurement_var.isEmpty()) |
| 76 | lc_measurement_var = lang; |
| 77 | if (lc_collate_var.isEmpty()) |
| 78 | lc_collate_var = lang; |
| 79 | lc_numeric = QLocale(QString::fromLatin1(ba: numeric)); |
| 80 | lc_time = QLocale(QString::fromLatin1(ba: time)); |
| 81 | lc_monetary = QLocale(QString::fromLatin1(ba: monetary)); |
| 82 | lc_messages = QLocale(QString::fromLatin1(ba: lc_messages_var)); |
| 83 | } |
| 84 | |
| 85 | Q_GLOBAL_STATIC(QSystemLocaleData, qSystemLocaleData) |
| 86 | |
| 87 | } // unnamed namespace |
| 88 | |
| 89 | static bool contradicts(QStringView maybe, const QString &known) |
| 90 | { |
| 91 | if (maybe.isEmpty()) |
| 92 | return false; |
| 93 | |
| 94 | /* |
| 95 | If \a known (our current best shot at deciding which language to use) |
| 96 | provides more information (e.g. script, country) than \a maybe (a |
| 97 | candidate to replace \a known) and \a maybe agrees with \a known in what |
| 98 | it does provide, we keep \a known; this happens when \a maybe comes from |
| 99 | LANGUAGE (usually a simple language code) and LANG includes script and/or |
| 100 | country. A textual comparison won't do because, for example, bn (Bengali) |
| 101 | isn't a prefix of ben_IN, but the latter is a refinement of the former. |
| 102 | (Meanwhile, bn is a prefix of bnt, Bantu; and a prefix of ben is be, |
| 103 | Belarusian. There are many more such prefixings between two- and |
| 104 | three-letter codes.) |
| 105 | */ |
| 106 | QLocaleId knownId = QLocaleId::fromName(name: known); |
| 107 | QLocaleId maybeId = QLocaleId::fromName(name: maybe); |
| 108 | return !(maybeId.acceptLanguage(lang: knownId.language_id) && maybeId.acceptScriptTerritory(other: knownId)); |
| 109 | } |
| 110 | |
| 111 | QLocale QSystemLocale::fallbackLocale() const |
| 112 | { |
| 113 | // See man 7 locale for precedence - LC_ALL beats LC_MESSAGES beats LANG: |
| 114 | QString lang = qEnvironmentVariable(varName: "LC_ALL" ); |
| 115 | if (lang.isEmpty()) |
| 116 | lang = qEnvironmentVariable(varName: "LC_MESSAGES" ); |
| 117 | if (lang.isEmpty()) |
| 118 | lang = qEnvironmentVariable(varName: "LANG" ); |
| 119 | // if the locale is the "C" locale, then we can return the language we found here: |
| 120 | if (lang.isEmpty() || lang == "C"_L1 || lang == "POSIX"_L1 ) |
| 121 | return QLocale(lang); |
| 122 | |
| 123 | // ... otherwise, if the first part of LANGUAGE says more than or |
| 124 | // contradicts what we have, use that: |
| 125 | { |
| 126 | QString language = qEnvironmentVariable(varName: "LANGUAGE" ); |
| 127 | // We only look at the first entry: |
| 128 | if (const auto colon = language.indexOf(c: u':'); colon >= 0) |
| 129 | language.truncate(pos: colon); // unshared QString; this is cheap |
| 130 | if (contradicts(maybe: language, known: lang)) |
| 131 | return QLocale(language); |
| 132 | } |
| 133 | |
| 134 | return QLocale(lang); |
| 135 | } |
| 136 | |
| 137 | QVariant QSystemLocale::query(QueryType type, QVariant &&in) const |
| 138 | { |
| 139 | QSystemLocaleData *d = qSystemLocaleData(); |
| 140 | if (!d) |
| 141 | return QVariant(); |
| 142 | |
| 143 | if (type == LocaleChanged) { |
| 144 | d->readEnvironment(); |
| 145 | return QVariant(); |
| 146 | } |
| 147 | |
| 148 | QReadLocker locker(&d->lock); |
| 149 | |
| 150 | const QLocale &lc_numeric = d->lc_numeric; |
| 151 | const QLocale &lc_time = d->lc_time; |
| 152 | const QLocale &lc_monetary = d->lc_monetary; |
| 153 | const QLocale &lc_messages = d->lc_messages; |
| 154 | |
| 155 | switch (type) { |
| 156 | case DecimalPoint: |
| 157 | return lc_numeric.decimalPoint(); |
| 158 | case Grouping: |
| 159 | return QVariant::fromValue(value: lc_numeric.d->m_data->groupSizes()); |
| 160 | case GroupSeparator: |
| 161 | return lc_numeric.groupSeparator(); |
| 162 | case ZeroDigit: |
| 163 | return lc_numeric.zeroDigit(); |
| 164 | case NegativeSign: |
| 165 | return lc_numeric.negativeSign(); |
| 166 | case DateFormatLong: |
| 167 | return lc_time.dateFormat(format: QLocale::LongFormat); |
| 168 | case DateFormatShort: |
| 169 | return lc_time.dateFormat(format: QLocale::ShortFormat); |
| 170 | case TimeFormatLong: |
| 171 | return lc_time.timeFormat(format: QLocale::LongFormat); |
| 172 | case TimeFormatShort: |
| 173 | return lc_time.timeFormat(format: QLocale::ShortFormat); |
| 174 | case DayNameLong: |
| 175 | return lc_time.dayName(in.toInt(), format: QLocale::LongFormat); |
| 176 | case DayNameShort: |
| 177 | return lc_time.dayName(in.toInt(), format: QLocale::ShortFormat); |
| 178 | case DayNameNarrow: |
| 179 | return lc_time.dayName(in.toInt(), format: QLocale::NarrowFormat); |
| 180 | case StandaloneDayNameLong: |
| 181 | return lc_time.standaloneDayName(in.toInt(), format: QLocale::LongFormat); |
| 182 | case StandaloneDayNameShort: |
| 183 | return lc_time.standaloneDayName(in.toInt(), format: QLocale::ShortFormat); |
| 184 | case StandaloneDayNameNarrow: |
| 185 | return lc_time.standaloneDayName(in.toInt(), format: QLocale::NarrowFormat); |
| 186 | case MonthNameLong: |
| 187 | return lc_time.monthName(in.toInt(), format: QLocale::LongFormat); |
| 188 | case MonthNameShort: |
| 189 | return lc_time.monthName(in.toInt(), format: QLocale::ShortFormat); |
| 190 | case MonthNameNarrow: |
| 191 | return lc_time.monthName(in.toInt(), format: QLocale::NarrowFormat); |
| 192 | case StandaloneMonthNameLong: |
| 193 | return lc_time.standaloneMonthName(in.toInt(), format: QLocale::LongFormat); |
| 194 | case StandaloneMonthNameShort: |
| 195 | return lc_time.standaloneMonthName(in.toInt(), format: QLocale::ShortFormat); |
| 196 | case StandaloneMonthNameNarrow: |
| 197 | return lc_time.standaloneMonthName(in.toInt(), format: QLocale::NarrowFormat); |
| 198 | case DateToStringLong: |
| 199 | return lc_time.toString(date: in.toDate(), format: QLocale::LongFormat); |
| 200 | case DateToStringShort: |
| 201 | return lc_time.toString(date: in.toDate(), format: QLocale::ShortFormat); |
| 202 | case TimeToStringLong: |
| 203 | return lc_time.toString(time: in.toTime(), format: QLocale::LongFormat); |
| 204 | case TimeToStringShort: |
| 205 | return lc_time.toString(time: in.toTime(), format: QLocale::ShortFormat); |
| 206 | case DateTimeFormatLong: |
| 207 | return lc_time.dateTimeFormat(format: QLocale::LongFormat); |
| 208 | case DateTimeFormatShort: |
| 209 | return lc_time.dateTimeFormat(format: QLocale::ShortFormat); |
| 210 | case DateTimeToStringLong: |
| 211 | return lc_time.toString(dateTime: in.toDateTime(), format: QLocale::LongFormat); |
| 212 | case DateTimeToStringShort: |
| 213 | return lc_time.toString(dateTime: in.toDateTime(), format: QLocale::ShortFormat); |
| 214 | case PositiveSign: |
| 215 | return lc_numeric.positiveSign(); |
| 216 | case AMText: |
| 217 | return lc_time.amText(); |
| 218 | case PMText: |
| 219 | return lc_time.pmText(); |
| 220 | case FirstDayOfWeek: |
| 221 | return lc_time.firstDayOfWeek(); |
| 222 | case CurrencySymbol: |
| 223 | return lc_monetary.currencySymbol(QLocale::CurrencySymbolFormat(in.toUInt())); |
| 224 | case CurrencyToString: { |
| 225 | switch (in.userType()) { |
| 226 | case QMetaType::Int: |
| 227 | return lc_monetary.toCurrencyString(i: in.toInt()); |
| 228 | case QMetaType::UInt: |
| 229 | return lc_monetary.toCurrencyString(i: in.toUInt()); |
| 230 | case QMetaType::Double: |
| 231 | return lc_monetary.toCurrencyString(in.toDouble()); |
| 232 | case QMetaType::LongLong: |
| 233 | return lc_monetary.toCurrencyString(in.toLongLong()); |
| 234 | case QMetaType::ULongLong: |
| 235 | return lc_monetary.toCurrencyString(in.toULongLong()); |
| 236 | default: |
| 237 | break; |
| 238 | } |
| 239 | return QString(); |
| 240 | } |
| 241 | case MeasurementSystem: { |
| 242 | const QString meas_locale = QString::fromLatin1(ba: d->lc_measurement_var); |
| 243 | if (meas_locale.compare(other: "Metric"_L1 , cs: Qt::CaseInsensitive) == 0) |
| 244 | return QLocale::MetricSystem; |
| 245 | if (meas_locale.compare(other: "Other"_L1 , cs: Qt::CaseInsensitive) == 0) |
| 246 | return QLocale::MetricSystem; |
| 247 | return QVariant((int)QLocale(meas_locale).measurementSystem()); |
| 248 | } |
| 249 | case Collation: |
| 250 | return QString::fromLatin1(ba: d->lc_collate_var); |
| 251 | case UILanguages: { |
| 252 | if (!d->uiLanguages.isEmpty()) |
| 253 | return d->uiLanguages; |
| 254 | QString languages = QString::fromLatin1(ba: qgetenv(varName: "LANGUAGE" )); |
| 255 | QStringList lst; |
| 256 | if (languages.isEmpty()) |
| 257 | lst.append(t: QString::fromLatin1(ba: d->lc_messages_var)); |
| 258 | else |
| 259 | lst = languages.split(sep: u':'); |
| 260 | |
| 261 | for (const QString &e : std::as_const(t&: lst)) { |
| 262 | QStringView language, script, territory; |
| 263 | if (qt_splitLocaleName(name: e, lang: &language, script: &script, cntry: &territory)) { |
| 264 | QString joined = language.isEmpty() ? u"und"_s : language.toString(); |
| 265 | if (!script.isEmpty()) |
| 266 | joined += u'-' + script; |
| 267 | if (!territory.isEmpty()) |
| 268 | joined += u'-' + territory; |
| 269 | d->uiLanguages.append(t: joined); |
| 270 | } |
| 271 | } |
| 272 | return d->uiLanguages.isEmpty() ? QVariant() : QVariant(d->uiLanguages); |
| 273 | } |
| 274 | case StringToStandardQuotation: |
| 275 | return lc_messages.quoteString(str: qvariant_cast<QStringView>(v: std::move(in))); |
| 276 | case StringToAlternateQuotation: |
| 277 | return lc_messages.quoteString(str: qvariant_cast<QStringView>(v: std::move(in)), |
| 278 | style: QLocale::AlternateQuotation); |
| 279 | case ListToSeparatedString: |
| 280 | return lc_messages.createSeparatedList(strl: in.toStringList()); |
| 281 | case LocaleChanged: |
| 282 | Q_UNREACHABLE(); // handled before the switch |
| 283 | case LanguageId: |
| 284 | case TerritoryId: |
| 285 | case Weekdays: |
| 286 | case ScriptId: |
| 287 | case NativeLanguageName: |
| 288 | case NativeTerritoryName: |
| 289 | break; |
| 290 | } |
| 291 | return QVariant(); |
| 292 | } |
| 293 | #endif // QT_NO_SYSTEMLOCALE |
| 294 | |
| 295 | QT_END_NAMESPACE |
| 296 | |