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"