File indexing completed on 2024-05-12 05:21:34
0001 /* 0002 SPDX-FileCopyrightText: 2017-2018 Volker Krause <vkrause@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "pass.h" 0008 #include "barcode.h" 0009 #include "boardingpass.h" 0010 #include "location.h" 0011 #include "logging.h" 0012 #include "pass_p.h" 0013 0014 #include <KZip> 0015 0016 #include <QBuffer> 0017 #include <QColor> 0018 #include <QFile> 0019 #include <QJsonArray> 0020 #include <QJsonDocument> 0021 #include <QJsonObject> 0022 #include <QLocale> 0023 #include <QRegularExpression> 0024 #include <QStringDecoder> 0025 #include <QUrl> 0026 0027 #include <cctype> 0028 0029 using namespace KPkPass; 0030 0031 static const char *const passTypes[] = {"boardingPass", "coupon", "eventTicket", "generic", "storeCard"}; 0032 static const auto passTypesCount = sizeof(passTypes) / sizeof(passTypes[0]); 0033 0034 QJsonObject PassPrivate::passData() const 0035 { 0036 return passObj.value(QLatin1StringView(passTypes[passType])).toObject(); 0037 } 0038 0039 QString PassPrivate::message(const QString &key) const 0040 { 0041 const auto it = messages.constFind(key); 0042 if (it != messages.constEnd()) { 0043 return it.value(); 0044 } 0045 return key; 0046 } 0047 0048 void PassPrivate::parse() 0049 { 0050 // find the first matching message catalog 0051 const auto langs = QLocale().uiLanguages(); 0052 for (auto lang : langs) { 0053 auto idx = lang.indexOf(QLatin1Char('-')); 0054 if (idx > 0) { 0055 lang = lang.left(idx); 0056 } 0057 lang += QLatin1StringView(".lproj"); 0058 if (parseMessages(lang)) { 0059 return; 0060 } 0061 } 0062 0063 // fallback to Englis if we didn't find anything better 0064 parseMessages(QStringLiteral("en.lproj")); 0065 } 0066 0067 static int indexOfUnquoted(const QString &catalog, QLatin1Char c, int start) 0068 { 0069 for (int i = start; i < catalog.size(); ++i) { 0070 const QChar catalogChar = catalog.at(i); 0071 if (catalogChar == c) { 0072 return i; 0073 } 0074 if (catalogChar == QLatin1Char('\\')) { 0075 ++i; 0076 } 0077 } 0078 0079 return -1; 0080 } 0081 0082 static QString unquote(QStringView str) 0083 { 0084 QString res; 0085 res.reserve(str.size()); 0086 for (int i = 0; i < str.size(); ++i) { 0087 const auto c1 = str.at(i); 0088 if (c1 == QLatin1Char('\\') && i < str.size() - 1) { 0089 const auto c2 = str.at(i + 1); 0090 if (c2 == QLatin1Char('r')) { 0091 res.push_back(QLatin1Char('\r')); 0092 } else if (c2 == QLatin1Char('n')) { 0093 res.push_back(QLatin1Char('\n')); 0094 } else if (c2 == QLatin1Char('\\')) { 0095 res.push_back(c2); 0096 } else { 0097 res.push_back(c1); 0098 res.push_back(c2); 0099 } 0100 ++i; 0101 } else { 0102 res.push_back(c1); 0103 } 0104 } 0105 return res; 0106 } 0107 0108 bool PassPrivate::parseMessages(const QString &lang) 0109 { 0110 auto entry = zip->directory()->entry(lang); 0111 if (!entry || !entry->isDirectory()) { 0112 return false; 0113 } 0114 0115 auto dir = static_cast<const KArchiveDirectory *>(entry); 0116 auto file = dir->file(QStringLiteral("pass.strings")); 0117 if (!file) { 0118 return false; 0119 } 0120 0121 std::unique_ptr<QIODevice> dev(file->createDevice()); 0122 const auto rawData = dev->readAll(); 0123 if (rawData.size() < 4) { 0124 return false; 0125 } 0126 // this should be UTF-16BE, but that doesn't stop Eurowings from using UTF-8, 0127 // so do a primitive auto-detection here. UTF-16's first byte would either be the BOM 0128 // or \0. 0129 QString catalog; 0130 if (std::ispunct((unsigned char)rawData.at(0))) { 0131 catalog = QString::fromUtf8(rawData); 0132 } else { 0133 auto codec = QStringDecoder(QStringDecoder::Utf16BE); 0134 catalog = codec(rawData); 0135 } 0136 0137 int idx = 0; 0138 while (idx < catalog.size()) { 0139 // key 0140 const auto keyBegin = indexOfUnquoted(catalog, QLatin1Char('"'), idx) + 1; 0141 if (keyBegin < 1) { 0142 break; 0143 } 0144 const auto keyEnd = indexOfUnquoted(catalog, QLatin1Char('"'), keyBegin); 0145 if (keyEnd <= keyBegin) { 0146 break; 0147 } 0148 0149 // value 0150 const auto valueBegin = indexOfUnquoted(catalog, QLatin1Char('"'), keyEnd + 2) + 1; // there's at least also the '=' 0151 if (valueBegin <= keyEnd) { 0152 break; 0153 } 0154 const auto valueEnd = indexOfUnquoted(catalog, QLatin1Char('"'), valueBegin); 0155 if (valueEnd < valueBegin) { 0156 break; 0157 } 0158 0159 const auto key = catalog.mid(keyBegin, keyEnd - keyBegin); 0160 const auto value = unquote(QStringView(catalog).mid(valueBegin, valueEnd - valueBegin)); 0161 messages.insert(key, value); 0162 idx = valueEnd + 1; // there's at least the linebreak and/or a ';' 0163 } 0164 0165 return !messages.isEmpty(); 0166 } 0167 0168 QList<Field> PassPrivate::fields(QLatin1StringView fieldType, const Pass *q) const 0169 { 0170 const auto a = passData().value(fieldType).toArray(); 0171 QList<Field> f; 0172 f.reserve(a.size()); 0173 for (const auto &v : a) { 0174 f.push_back(Field{v.toObject(), q}); 0175 } 0176 return f; 0177 } 0178 0179 Pass *PassPrivate::fromData(std::unique_ptr<QIODevice> device, QObject *parent) 0180 { 0181 std::unique_ptr<KZip> zip(new KZip(device.get())); 0182 if (!zip->open(QIODevice::ReadOnly)) { 0183 return nullptr; 0184 } 0185 0186 // extract pass.json 0187 auto file = zip->directory()->file(QStringLiteral("pass.json")); 0188 if (!file) { 0189 return nullptr; 0190 } 0191 std::unique_ptr<QIODevice> dev(file->createDevice()); 0192 QJsonParseError error; 0193 const auto data = dev->readAll(); 0194 auto passObj = QJsonDocument::fromJson(data, &error).object(); 0195 if (error.error != QJsonParseError::NoError) { 0196 qCWarning(Log) << "Error parsing pass.json:" << error.errorString() << error.offset; 0197 0198 // try to fix some known JSON syntax errors 0199 auto s = QString::fromUtf8(data); 0200 s.replace(QRegularExpression(QStringLiteral(R"(\}[\s\n]*,[\s\n]*\})")), QStringLiteral("}}")); 0201 s.replace(QRegularExpression(QStringLiteral(R"(\][\s\n]*,[\s\n]*\})")), QStringLiteral("]}")); 0202 passObj = QJsonDocument::fromJson(s.toUtf8(), &error).object(); 0203 if (error.error != QJsonParseError::NoError) { 0204 qCWarning(Log) << "JSON syntax workarounds didn't help either:" << error.errorString() << error.offset; 0205 return nullptr; 0206 } 0207 } 0208 if (passObj.value(QLatin1StringView("formatVersion")).toInt() > 1) { 0209 qCWarning(Log) << "pass.json has unsupported format version!"; 0210 return nullptr; 0211 } 0212 0213 // determine pass type 0214 int passTypeIdx = -1; 0215 for (unsigned int i = 0; i < passTypesCount; ++i) { 0216 if (passObj.contains(QLatin1StringView(passTypes[i]))) { 0217 passTypeIdx = static_cast<int>(i); 0218 break; 0219 } 0220 } 0221 if (passTypeIdx < 0) { 0222 qCWarning(Log) << "pkpass file has no pass data structure!"; 0223 return nullptr; 0224 } 0225 0226 Pass *pass = nullptr; 0227 switch (passTypeIdx) { 0228 case Pass::BoardingPass: 0229 pass = new KPkPass::BoardingPass(parent); 0230 break; 0231 default: 0232 pass = new Pass(static_cast<Pass::Type>(passTypeIdx), parent); 0233 break; 0234 } 0235 0236 pass->d->buffer = std::move(device); 0237 pass->d->zip = std::move(zip); 0238 pass->d->passObj = passObj; 0239 pass->d->parse(); 0240 return pass; 0241 } 0242 0243 Pass::Pass(Type passType, QObject *parent) 0244 : QObject(parent) 0245 , d(new PassPrivate) 0246 { 0247 d->passType = passType; 0248 } 0249 0250 Pass::~Pass() = default; 0251 0252 Pass::Type Pass::type() const 0253 { 0254 return d->passType; 0255 } 0256 0257 QString Pass::description() const 0258 { 0259 return d->passObj.value(QLatin1StringView("description")).toString(); 0260 } 0261 0262 QString Pass::organizationName() const 0263 { 0264 return d->passObj.value(QLatin1StringView("organizationName")).toString(); 0265 } 0266 0267 QString Pass::passTypeIdentifier() const 0268 { 0269 return d->passObj.value(QLatin1StringView("passTypeIdentifier")).toString(); 0270 } 0271 0272 QString Pass::serialNumber() const 0273 { 0274 return d->passObj.value(QLatin1StringView("serialNumber")).toString(); 0275 } 0276 0277 QDateTime Pass::expirationDate() const 0278 { 0279 return QDateTime::fromString(d->passObj.value(QLatin1StringView("expirationDate")).toString(), Qt::ISODate); 0280 } 0281 0282 bool Pass::isVoided() const 0283 { 0284 return d->passObj.value(QLatin1StringView("voided")).toString() == QLatin1StringView("true"); 0285 } 0286 0287 QList<Location> Pass::locations() const 0288 { 0289 QList<Location> locs; 0290 const auto a = d->passObj.value(QLatin1StringView("locations")).toArray(); 0291 locs.reserve(a.size()); 0292 for (const auto &loc : a) { 0293 locs.push_back(Location(loc.toObject())); 0294 } 0295 0296 return locs; 0297 } 0298 0299 int Pass::maximumDistance() const 0300 { 0301 return d->passObj.value(QLatin1StringView("maxDistance")).toInt(500); 0302 } 0303 0304 QDateTime Pass::relevantDate() const 0305 { 0306 return QDateTime::fromString(d->passObj.value(QLatin1StringView("relevantDate")).toString(), Qt::ISODate); 0307 } 0308 0309 static QColor parseColor(const QString &s) 0310 { 0311 if (s.startsWith(QLatin1StringView("rgb("), Qt::CaseInsensitive)) { 0312 const auto l = QStringView(s).mid(4, s.length() - 5).split(QLatin1Char(',')); 0313 if (l.size() != 3) 0314 return {}; 0315 return QColor(l[0].trimmed().toInt(), l[1].trimmed().toInt(), l[2].trimmed().toInt()); 0316 } 0317 return QColor(s); 0318 } 0319 0320 QColor Pass::backgroundColor() const 0321 { 0322 return parseColor(d->passObj.value(QLatin1StringView("backgroundColor")).toString()); 0323 } 0324 0325 QColor Pass::foregroundColor() const 0326 { 0327 return parseColor(d->passObj.value(QLatin1StringView("foregroundColor")).toString()); 0328 } 0329 0330 QString Pass::groupingIdentifier() const 0331 { 0332 return d->passObj.value(QLatin1StringView("groupingIdentifier")).toString(); 0333 } 0334 0335 QColor Pass::labelColor() const 0336 { 0337 const auto c = parseColor(d->passObj.value(QLatin1StringView("labelColor")).toString()); 0338 if (c.isValid()) { 0339 return c; 0340 } 0341 return foregroundColor(); 0342 } 0343 0344 QString Pass::logoText() const 0345 { 0346 return d->message(d->passObj.value(QLatin1StringView("logoText")).toString()); 0347 } 0348 0349 bool Pass::hasImage(const QString &baseName) const 0350 { 0351 const auto entries = d->zip->directory()->entries(); 0352 for (const auto &entry : entries) { 0353 if (entry.startsWith(baseName) 0354 && (QStringView(entry).mid(baseName.size()).startsWith(QLatin1Char('@')) || QStringView(entry).mid(baseName.size()).startsWith(QLatin1Char('.'))) 0355 && entry.endsWith(QLatin1StringView(".png"))) { 0356 return true; 0357 } 0358 } 0359 return false; 0360 } 0361 0362 bool Pass::hasIcon() const 0363 { 0364 return hasImage(QStringLiteral("icon")); 0365 } 0366 0367 bool Pass::hasLogo() const 0368 { 0369 return hasImage(QStringLiteral("logo")); 0370 } 0371 0372 bool Pass::hasStrip() const 0373 { 0374 return hasImage(QStringLiteral("strip")); 0375 } 0376 0377 bool Pass::hasBackground() const 0378 { 0379 return hasImage(QStringLiteral("background")); 0380 } 0381 0382 bool Pass::hasFooter() const 0383 { 0384 return hasImage(QStringLiteral("footer")); 0385 } 0386 0387 bool Pass::hasThumbnail() const 0388 { 0389 return hasImage(QStringLiteral("thumbnail")); 0390 } 0391 0392 QImage Pass::image(const QString &baseName, unsigned int devicePixelRatio) const 0393 { 0394 const KArchiveFile *file = nullptr; 0395 QImage img; 0396 0397 auto dpr = devicePixelRatio; 0398 for (; dpr > 0; --dpr) { 0399 const auto it = d->m_images.find(ImageCacheKey{baseName, dpr}); 0400 if (it != d->m_images.end()) { 0401 img = (*it).second; 0402 break; 0403 } 0404 if (dpr > 1) { 0405 file = d->zip->directory()->file(baseName + QLatin1Char('@') + QString::number(dpr) + QLatin1StringView("x.png")); 0406 } else { 0407 file = d->zip->directory()->file(baseName + QLatin1StringView(".png")); 0408 } 0409 if (file) 0410 break; 0411 } 0412 if (!img.isNull()) { // cache hit 0413 return img; 0414 } 0415 0416 if (!file) { 0417 return {}; 0418 } 0419 0420 std::unique_ptr<QIODevice> dev(file->createDevice()); 0421 img = QImage::fromData(dev->readAll()); 0422 img.setDevicePixelRatio(dpr); 0423 d->m_images[ImageCacheKey{baseName, dpr}] = img; 0424 if (dpr != devicePixelRatio) { 0425 d->m_images[ImageCacheKey{baseName, devicePixelRatio}] = img; 0426 } 0427 return img; 0428 } 0429 0430 QImage Pass::icon(unsigned int devicePixelRatio) const 0431 { 0432 return image(QStringLiteral("icon"), devicePixelRatio); 0433 } 0434 0435 QImage Pass::logo(unsigned int devicePixelRatio) const 0436 { 0437 return image(QStringLiteral("logo"), devicePixelRatio); 0438 } 0439 0440 QImage Pass::strip(unsigned int devicePixelRatio) const 0441 { 0442 return image(QStringLiteral("strip"), devicePixelRatio); 0443 } 0444 0445 QImage Pass::background(unsigned int devicePixelRatio) const 0446 { 0447 return image(QStringLiteral("background"), devicePixelRatio); 0448 } 0449 0450 QImage Pass::footer(unsigned int devicePixelRatio) const 0451 { 0452 return image(QStringLiteral("footer"), devicePixelRatio); 0453 } 0454 0455 QImage Pass::thumbnail(unsigned int devicePixelRatio) const 0456 { 0457 return image(QStringLiteral("thumbnail"), devicePixelRatio); 0458 } 0459 0460 QString Pass::authenticationToken() const 0461 { 0462 return d->passObj.value(QLatin1StringView("authenticationToken")).toString(); 0463 } 0464 0465 QUrl Pass::webServiceUrl() const 0466 { 0467 return QUrl(d->passObj.value(QLatin1StringView("webServiceURL")).toString()); 0468 } 0469 0470 QUrl Pass::passUpdateUrl() const 0471 { 0472 QUrl url(webServiceUrl()); 0473 if (!url.isValid()) { 0474 return {}; 0475 } 0476 url.setPath(url.path() + QLatin1StringView("/v1/passes/") + passTypeIdentifier() + QLatin1Char('/') + serialNumber()); 0477 return url; 0478 } 0479 0480 QList<Barcode> Pass::barcodes() const 0481 { 0482 QList<Barcode> codes; 0483 0484 // barcodes array 0485 const auto a = d->passObj.value(QLatin1StringView("barcodes")).toArray(); 0486 codes.reserve(a.size()); 0487 for (const auto &bc : a) 0488 codes.push_back(Barcode(bc.toObject(), this)); 0489 0490 // just a single barcode 0491 if (codes.isEmpty()) { 0492 const auto bc = d->passObj.value(QLatin1StringView("barcode")).toObject(); 0493 if (!bc.isEmpty()) 0494 codes.push_back(Barcode(bc, this)); 0495 } 0496 0497 return codes; 0498 } 0499 0500 static const char *const fieldNames[] = {"auxiliaryFields", "backFields", "headerFields", "primaryFields", "secondaryFields"}; 0501 static const auto fieldNameCount = sizeof(fieldNames) / sizeof(fieldNames[0]); 0502 0503 QList<Field> Pass::auxiliaryFields() const 0504 { 0505 return d->fields(QLatin1StringView(fieldNames[0]), this); 0506 } 0507 0508 QList<Field> Pass::backFields() const 0509 { 0510 return d->fields(QLatin1StringView(fieldNames[1]), this); 0511 } 0512 0513 QList<Field> Pass::headerFields() const 0514 { 0515 return d->fields(QLatin1StringView(fieldNames[2]), this); 0516 } 0517 0518 QList<Field> Pass::primaryFields() const 0519 { 0520 return d->fields(QLatin1StringView(fieldNames[3]), this); 0521 } 0522 0523 QList<Field> Pass::secondaryFields() const 0524 { 0525 return d->fields(QLatin1StringView(fieldNames[4]), this); 0526 } 0527 0528 Field Pass::field(const QString &key) const 0529 { 0530 for (unsigned int i = 0; i < fieldNameCount; ++i) { 0531 const auto fs = d->fields(QLatin1StringView(fieldNames[i]), this); 0532 for (const auto &f : fs) { 0533 if (f.key() == key) { 0534 return f; 0535 } 0536 } 0537 } 0538 return {}; 0539 } 0540 0541 QList<Field> Pass::fields() const 0542 { 0543 QList<Field> fs; 0544 for (unsigned int i = 0; i < fieldNameCount; ++i) { 0545 fs += d->fields(QLatin1StringView(fieldNames[i]), this); 0546 } 0547 return fs; 0548 } 0549 0550 Pass *Pass::fromData(const QByteArray &data, QObject *parent) 0551 { 0552 std::unique_ptr<QBuffer> buffer(new QBuffer); 0553 buffer->setData(data); 0554 buffer->open(QBuffer::ReadOnly); 0555 return PassPrivate::fromData(std::move(buffer), parent); 0556 } 0557 0558 Pass *Pass::fromFile(const QString &fileName, QObject *parent) 0559 { 0560 std::unique_ptr<QFile> file(new QFile(fileName)); 0561 if (file->open(QFile::ReadOnly)) { 0562 return PassPrivate::fromData(std::move(file), parent); 0563 } 0564 qCWarning(Log) << "Failed to open" << fileName << ":" << file->errorString(); 0565 return nullptr; 0566 } 0567 0568 QVariantMap Pass::fieldsVariantMap() const 0569 { 0570 QVariantMap m; 0571 const auto elems = fields(); 0572 std::for_each(elems.begin(), elems.end(), [&m](const Field &f) { 0573 m.insert(f.key(), QVariant::fromValue(f)); 0574 }); 0575 return m; 0576 } 0577 0578 QByteArray Pass::rawData() const 0579 { 0580 const auto prevPos = d->buffer->pos(); 0581 d->buffer->seek(0); 0582 const auto data = d->buffer->readAll(); 0583 d->buffer->seek(prevPos); 0584 return data; 0585 } 0586 0587 #include "moc_pass.cpp"