KPkPass

pass.cpp
1/*
2 SPDX-FileCopyrightText: 2017-2018 Volker Krause <vkrause@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "pass.h"
8#include "barcode.h"
9#include "boardingpass.h"
10#include "location.h"
11#include "logging.h"
12#include "pass_p.h"
13
14#include <KZip>
15
16#include <QBuffer>
17#include <QColor>
18#include <QFile>
19#include <QJsonArray>
20#include <QJsonDocument>
21#include <QJsonObject>
22#include <QLocale>
23#include <QRegularExpression>
24#include <QStringDecoder>
25#include <QUrl>
26
27#include <cctype>
28
29using namespace KPkPass;
30
31static const char *const passTypes[] = {"boardingPass", "coupon", "eventTicket", "generic", "storeCard"};
32static const auto passTypesCount = sizeof(passTypes) / sizeof(passTypes[0]);
33
34QJsonObject PassPrivate::passData() const
35{
36 return passObj.value(QLatin1StringView(passTypes[passType])).toObject();
37}
38
39QString PassPrivate::message(const QString &key) const
40{
41 const auto it = messages.constFind(key);
42 if (it != messages.constEnd()) {
43 return it.value();
44 }
45 return key;
46}
47
48void PassPrivate::parse()
49{
50 // find the first matching message catalog
51 const auto langs = QLocale().uiLanguages();
52 for (auto lang : langs) {
53 auto idx = lang.indexOf(QLatin1Char('-'));
54 if (idx > 0) {
55 lang = lang.left(idx);
56 }
57 lang += QLatin1StringView(".lproj");
58 if (parseMessages(lang)) {
59 return;
60 }
61 }
62
63 // fallback to Englis if we didn't find anything better
64 parseMessages(QStringLiteral("en.lproj"));
65}
66
67static int indexOfUnquoted(const QString &catalog, QLatin1Char c, int start)
68{
69 for (int i = start; i < catalog.size(); ++i) {
70 const QChar catalogChar = catalog.at(i);
71 if (catalogChar == c) {
72 return i;
73 }
74 if (catalogChar == QLatin1Char('\\')) {
75 ++i;
76 }
77 }
78
79 return -1;
80}
81
82static QString unquote(QStringView str)
83{
84 QString res;
85 res.reserve(str.size());
86 for (int i = 0; i < str.size(); ++i) {
87 const auto c1 = str.at(i);
88 if (c1 == QLatin1Char('\\') && i < str.size() - 1) {
89 const auto c2 = str.at(i + 1);
90 if (c2 == QLatin1Char('r')) {
91 res.push_back(QLatin1Char('\r'));
92 } else if (c2 == QLatin1Char('n')) {
93 res.push_back(QLatin1Char('\n'));
94 } else if (c2 == QLatin1Char('\\')) {
95 res.push_back(c2);
96 } else {
97 res.push_back(c1);
98 res.push_back(c2);
99 }
100 ++i;
101 } else {
102 res.push_back(c1);
103 }
104 }
105 return res;
106}
107
108bool PassPrivate::parseMessages(const QString &lang)
109{
110 auto entry = zip->directory()->entry(lang);
111 if (!entry || !entry->isDirectory()) {
112 return false;
113 }
114
115 auto dir = static_cast<const KArchiveDirectory *>(entry);
116 auto file = dir->file(QStringLiteral("pass.strings"));
117 if (!file) {
118 return false;
119 }
120
121 std::unique_ptr<QIODevice> dev(file->createDevice());
122 const auto rawData = dev->readAll();
123 if (rawData.size() < 4) {
124 return false;
125 }
126 // this should be UTF-16BE, but that doesn't stop Eurowings from using UTF-8,
127 // so do a primitive auto-detection here. UTF-16's first byte would either be the BOM
128 // or \0.
129 QString catalog;
130 if (std::ispunct((unsigned char)rawData.at(0))) {
131 catalog = QString::fromUtf8(rawData);
132 } else {
134 catalog = codec(rawData);
135 }
136
137 int idx = 0;
138 while (idx < catalog.size()) {
139 // key
140 const auto keyBegin = indexOfUnquoted(catalog, QLatin1Char('"'), idx) + 1;
141 if (keyBegin < 1) {
142 break;
143 }
144 const auto keyEnd = indexOfUnquoted(catalog, QLatin1Char('"'), keyBegin);
145 if (keyEnd <= keyBegin) {
146 break;
147 }
148
149 // value
150 const auto valueBegin = indexOfUnquoted(catalog, QLatin1Char('"'), keyEnd + 2) + 1; // there's at least also the '='
151 if (valueBegin <= keyEnd) {
152 break;
153 }
154 const auto valueEnd = indexOfUnquoted(catalog, QLatin1Char('"'), valueBegin);
155 if (valueEnd < valueBegin) {
156 break;
157 }
158
159 const auto key = catalog.mid(keyBegin, keyEnd - keyBegin);
160 const auto value = unquote(QStringView(catalog).mid(valueBegin, valueEnd - valueBegin));
161 messages.insert(key, value);
162 idx = valueEnd + 1; // there's at least the linebreak and/or a ';'
163 }
164
165 return !messages.isEmpty();
166}
167
168QList<Field> PassPrivate::fields(QLatin1StringView fieldType, const Pass *q) const
169{
170 const auto a = passData().value(fieldType).toArray();
171 QList<Field> f;
172 f.reserve(a.size());
173 for (const auto &v : a) {
174 f.push_back(Field{v.toObject(), q});
175 }
176 return f;
177}
178
179Pass *PassPrivate::fromData(std::unique_ptr<QIODevice> device, QObject *parent)
180{
181 std::unique_ptr<KZip> zip(new KZip(device.get()));
182 if (!zip->open(QIODevice::ReadOnly)) {
183 return nullptr;
184 }
185
186 // extract pass.json
187 auto file = zip->directory()->file(QStringLiteral("pass.json"));
188 if (!file) {
189 return nullptr;
190 }
191 std::unique_ptr<QIODevice> dev(file->createDevice());
193 const auto data = dev->readAll();
194 auto passObj = QJsonDocument::fromJson(data, &error).object();
195 if (error.error != QJsonParseError::NoError) {
196 qCWarning(Log) << "Error parsing pass.json:" << error.errorString() << error.offset;
197
198 // try to fix some known JSON syntax errors
199 auto s = QString::fromUtf8(data);
200 s.replace(QRegularExpression(QStringLiteral(R"(\}[\s\n]*,[\s\n]*\})")), QStringLiteral("}}"));
201 s.replace(QRegularExpression(QStringLiteral(R"(\][\s\n]*,[\s\n]*\})")), QStringLiteral("]}"));
202 passObj = QJsonDocument::fromJson(s.toUtf8(), &error).object();
203 if (error.error != QJsonParseError::NoError) {
204 qCWarning(Log) << "JSON syntax workarounds didn't help either:" << error.errorString() << error.offset;
205 return nullptr;
206 }
207 }
208 if (passObj.value(QLatin1StringView("formatVersion")).toInt() > 1) {
209 qCWarning(Log) << "pass.json has unsupported format version!";
210 return nullptr;
211 }
212
213 // determine pass type
214 int passTypeIdx = -1;
215 for (unsigned int i = 0; i < passTypesCount; ++i) {
216 if (passObj.contains(QLatin1StringView(passTypes[i]))) {
217 passTypeIdx = static_cast<int>(i);
218 break;
219 }
220 }
221 if (passTypeIdx < 0) {
222 qCWarning(Log) << "pkpass file has no pass data structure!";
223 return nullptr;
224 }
225
226 Pass *pass = nullptr;
227 switch (passTypeIdx) {
228 case Pass::BoardingPass:
229 pass = new KPkPass::BoardingPass(parent);
230 break;
231 default:
232 pass = new Pass(static_cast<Pass::Type>(passTypeIdx), parent);
233 break;
234 }
235
236 pass->d->buffer = std::move(device);
237 pass->d->zip = std::move(zip);
238 pass->d->passObj = passObj;
239 pass->d->parse();
240 return pass;
241}
242
243Pass::Pass(Type passType, QObject *parent)
244 : QObject(parent)
245 , d(new PassPrivate)
246{
247 d->passType = passType;
248}
249
250Pass::~Pass() = default;
251
252Pass::Type Pass::type() const
253{
254 return d->passType;
255}
256
257QString Pass::description() const
258{
259 return d->passObj.value(QLatin1StringView("description")).toString();
260}
261
262QString Pass::organizationName() const
263{
264 return d->passObj.value(QLatin1StringView("organizationName")).toString();
265}
266
267QString Pass::passTypeIdentifier() const
268{
269 return d->passObj.value(QLatin1StringView("passTypeIdentifier")).toString();
270}
271
272QString Pass::serialNumber() const
273{
274 return d->passObj.value(QLatin1StringView("serialNumber")).toString();
275}
276
277QDateTime Pass::expirationDate() const
278{
279 return QDateTime::fromString(d->passObj.value(QLatin1StringView("expirationDate")).toString(), Qt::ISODate);
280}
281
282bool Pass::isVoided() const
283{
284 return d->passObj.value(QLatin1StringView("voided")).toString() == QLatin1StringView("true");
285}
286
287QList<Location> Pass::locations() const
288{
289 QList<Location> locs;
290 const auto a = d->passObj.value(QLatin1StringView("locations")).toArray();
291 locs.reserve(a.size());
292 for (const auto &loc : a) {
293 locs.push_back(Location(loc.toObject()));
294 }
295
296 return locs;
297}
298
300{
301 return d->passObj.value(QLatin1StringView("maxDistance")).toInt(500);
302}
303
304QDateTime Pass::relevantDate() const
305{
306 return QDateTime::fromString(d->passObj.value(QLatin1StringView("relevantDate")).toString(), Qt::ISODate);
307}
308
309static QColor parseColor(const QString &s)
310{
312 const auto l = QStringView(s).mid(4, s.length() - 5).split(QLatin1Char(','));
313 if (l.size() != 3)
314 return {};
315 return QColor(l[0].trimmed().toInt(), l[1].trimmed().toInt(), l[2].trimmed().toInt());
316 }
317 return QColor(s);
318}
319
320QColor Pass::backgroundColor() const
321{
322 return parseColor(d->passObj.value(QLatin1StringView("backgroundColor")).toString());
323}
324
325QColor Pass::foregroundColor() const
326{
327 return parseColor(d->passObj.value(QLatin1StringView("foregroundColor")).toString());
328}
329
330QString Pass::groupingIdentifier() const
331{
332 return d->passObj.value(QLatin1StringView("groupingIdentifier")).toString();
333}
334
335QColor Pass::labelColor() const
336{
337 const auto c = parseColor(d->passObj.value(QLatin1StringView("labelColor")).toString());
338 if (c.isValid()) {
339 return c;
340 }
341 return foregroundColor();
342}
343
344QString Pass::logoText() const
345{
346 return d->message(d->passObj.value(QLatin1StringView("logoText")).toString());
347}
348
349bool Pass::hasImage(const QString &baseName) const
350{
351 const auto entries = d->zip->directory()->entries();
352 for (const auto &entry : entries) {
353 if (entry.startsWith(baseName)
354 && (QStringView(entry).mid(baseName.size()).startsWith(QLatin1Char('@')) || QStringView(entry).mid(baseName.size()).startsWith(QLatin1Char('.')))
355 && entry.endsWith(QLatin1StringView(".png"))) {
356 return true;
357 }
358 }
359 return false;
360}
361
362bool Pass::hasIcon() const
363{
364 return hasImage(QStringLiteral("icon"));
365}
366
367bool Pass::hasLogo() const
368{
369 return hasImage(QStringLiteral("logo"));
370}
371
372bool Pass::hasStrip() const
373{
374 return hasImage(QStringLiteral("strip"));
375}
376
377bool Pass::hasBackground() const
378{
379 return hasImage(QStringLiteral("background"));
380}
381
382bool Pass::hasFooter() const
383{
384 return hasImage(QStringLiteral("footer"));
385}
386
387bool Pass::hasThumbnail() const
388{
389 return hasImage(QStringLiteral("thumbnail"));
390}
391
392QImage Pass::image(const QString &baseName, unsigned int devicePixelRatio) const
393{
394 const KArchiveFile *file = nullptr;
395 QImage img;
396
397 auto dpr = devicePixelRatio;
398 for (; dpr > 0; --dpr) {
399 const auto it = d->m_images.find(ImageCacheKey{baseName, dpr});
400 if (it != d->m_images.end()) {
401 img = (*it).second;
402 break;
403 }
404 if (dpr > 1) {
405 file = d->zip->directory()->file(baseName + QLatin1Char('@') + QString::number(dpr) + QLatin1StringView("x.png"));
406 } else {
407 file = d->zip->directory()->file(baseName + QLatin1StringView(".png"));
408 }
409 if (file)
410 break;
411 }
412 if (!img.isNull()) { // cache hit
413 return img;
414 }
415
416 if (!file) {
417 return {};
418 }
419
420 std::unique_ptr<QIODevice> dev(file->createDevice());
421 img = QImage::fromData(dev->readAll());
422 img.setDevicePixelRatio(dpr);
423 d->m_images[ImageCacheKey{baseName, dpr}] = img;
424 if (dpr != devicePixelRatio) {
425 d->m_images[ImageCacheKey{baseName, devicePixelRatio}] = img;
426 }
427 return img;
428}
429
430QImage Pass::icon(unsigned int devicePixelRatio) const
431{
432 return image(QStringLiteral("icon"), devicePixelRatio);
433}
434
435QImage Pass::logo(unsigned int devicePixelRatio) const
436{
437 return image(QStringLiteral("logo"), devicePixelRatio);
438}
439
440QImage Pass::strip(unsigned int devicePixelRatio) const
441{
442 return image(QStringLiteral("strip"), devicePixelRatio);
443}
444
445QImage Pass::background(unsigned int devicePixelRatio) const
446{
447 return image(QStringLiteral("background"), devicePixelRatio);
448}
449
450QImage Pass::footer(unsigned int devicePixelRatio) const
451{
452 return image(QStringLiteral("footer"), devicePixelRatio);
453}
454
455QImage Pass::thumbnail(unsigned int devicePixelRatio) const
456{
457 return image(QStringLiteral("thumbnail"), devicePixelRatio);
458}
459
460QString Pass::authenticationToken() const
461{
462 return d->passObj.value(QLatin1StringView("authenticationToken")).toString();
463}
464
465QUrl Pass::webServiceUrl() const
466{
467 return QUrl(d->passObj.value(QLatin1StringView("webServiceURL")).toString());
468}
469
471{
472 QUrl url(webServiceUrl());
473 if (!url.isValid()) {
474 return {};
475 }
476 url.setPath(url.path() + QLatin1StringView("/v1/passes/") + passTypeIdentifier() + QLatin1Char('/') + serialNumber());
477 return url;
478}
479
480QList<Barcode> Pass::barcodes() const
481{
482 QList<Barcode> codes;
483
484 // barcodes array
485 const auto a = d->passObj.value(QLatin1StringView("barcodes")).toArray();
486 codes.reserve(a.size());
487 for (const auto &bc : a)
488 codes.push_back(Barcode(bc.toObject(), this));
489
490 // just a single barcode
491 if (codes.isEmpty()) {
492 const auto bc = d->passObj.value(QLatin1StringView("barcode")).toObject();
493 if (!bc.isEmpty())
494 codes.push_back(Barcode(bc, this));
495 }
496
497 return codes;
498}
499
500static const char *const fieldNames[] = {"auxiliaryFields", "backFields", "headerFields", "primaryFields", "secondaryFields"};
501static const auto fieldNameCount = sizeof(fieldNames) / sizeof(fieldNames[0]);
502
503QList<Field> Pass::auxiliaryFields() const
504{
505 return d->fields(QLatin1StringView(fieldNames[0]), this);
506}
507
508QList<Field> Pass::backFields() const
509{
510 return d->fields(QLatin1StringView(fieldNames[1]), this);
511}
512
513QList<Field> Pass::headerFields() const
514{
515 return d->fields(QLatin1StringView(fieldNames[2]), this);
516}
517
518QList<Field> Pass::primaryFields() const
519{
520 return d->fields(QLatin1StringView(fieldNames[3]), this);
521}
522
523QList<Field> Pass::secondaryFields() const
524{
525 return d->fields(QLatin1StringView(fieldNames[4]), this);
526}
527
528Field Pass::field(const QString &key) const
529{
530 for (unsigned int i = 0; i < fieldNameCount; ++i) {
531 const auto fs = d->fields(QLatin1StringView(fieldNames[i]), this);
532 for (const auto &f : fs) {
533 if (f.key() == key) {
534 return f;
535 }
536 }
537 }
538 return {};
539}
540
542{
543 QList<Field> fs;
544 for (unsigned int i = 0; i < fieldNameCount; ++i) {
545 fs += d->fields(QLatin1StringView(fieldNames[i]), this);
546 }
547 return fs;
548}
549
550Pass *Pass::fromData(const QByteArray &data, QObject *parent)
551{
552 std::unique_ptr<QBuffer> buffer(new QBuffer);
553 buffer->setData(data);
554 buffer->open(QBuffer::ReadOnly);
555 return PassPrivate::fromData(std::move(buffer), parent);
556}
557
558Pass *Pass::fromFile(const QString &fileName, QObject *parent)
559{
560 std::unique_ptr<QFile> file(new QFile(fileName));
561 if (file->open(QFile::ReadOnly)) {
562 return PassPrivate::fromData(std::move(file), parent);
563 }
564 qCWarning(Log) << "Failed to open" << fileName << ":" << file->errorString();
565 return nullptr;
566}
567
568QVariantMap Pass::fieldsVariantMap() const
569{
570 QVariantMap m;
571 const auto elems = fields();
572 std::for_each(elems.begin(), elems.end(), [&m](const Field &f) {
573 m.insert(f.key(), QVariant::fromValue(f));
574 });
575 return m;
576}
577
579{
580 const auto prevPos = d->buffer->pos();
581 d->buffer->seek(0);
582 const auto data = d->buffer->readAll();
583 d->buffer->seek(prevPos);
584 return data;
585}
586
587#include "moc_pass.cpp"
virtual QIODevice * createDevice() const
A pass barcode element.
Definition barcode.h:27
Field element in a KPkPass::Pass.
Definition field.h:29
A pass location element.
Definition location.h:25
Base class for a pkpass file.
Definition pass.h:35
QImage image(const QString &baseName, unsigned int devicePixelRatio=1) const
Returns an image asset of this pass.
Definition pass.cpp:392
int maximumDistance() const
Distance in meters to any of the pass locations before this pass becomes relevant.
Definition pass.cpp:299
QUrl passUpdateUrl() const
Pass update URL.
Definition pass.cpp:470
Q_INVOKABLE QImage icon(unsigned int devicePixelRatio=1) const
Returns the pass icon.
Definition pass.cpp:430
Q_INVOKABLE QImage logo(unsigned int devicePixelRatio=1) const
Returns the pass logo.
Definition pass.cpp:435
bool hasImage(const QString &baseName) const
Returns true if an image asset with the given base name exists.
Definition pass.cpp:349
static Pass * fromData(const QByteArray &data, QObject *parent=nullptr)
Create a appropriate sub-class based on the pkpass file type.
Definition pass.cpp:550
static Pass * fromFile(const QString &fileName, QObject *parent=nullptr)
Create a appropriate sub-class based on the pkpass file type.
Definition pass.cpp:558
Type
Type of the pass.
Definition pass.h:76
Q_INVOKABLE QImage thumbnail(unsigned int devicePixelRatio=1) const
Returns the thumbnail image if present.
Definition pass.cpp:455
QList< Field > fields() const
Returns all fields found in this pass.
Definition pass.cpp:541
Q_INVOKABLE QImage strip(unsigned int devicePixelRatio=1) const
Returns the strip image if present.
Definition pass.cpp:440
Q_INVOKABLE QImage background(unsigned int devicePixelRatio=1) const
Returns the background image if present.
Definition pass.cpp:445
QByteArray rawData() const
The raw data of this pass.
Definition pass.cpp:578
Q_INVOKABLE QImage footer(unsigned int devicePixelRatio=1) const
Returns the footer image if present.
Definition pass.cpp:450
Q_SCRIPTABLE Q_NOREPLY void start()
char * toString(const EngineQuery &query)
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
KIOCORE_EXPORT QString dir(const QString &fileClass)
QDateTime fromString(QStringView string, QStringView format, QCalendar cal)
QImage fromData(QByteArrayView data, const char *format)
bool isNull() const const
void setDevicePixelRatio(qreal scaleFactor)
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
QJsonObject object() const const
QJsonValue value(QLatin1StringView key) const const
QJsonObject toObject() const const
bool isEmpty() const const
void push_back(parameter_type value)
void reserve(qsizetype size)
T value(qsizetype i) const const
QStringList uiLanguages() const const
QObject * parent() const const
const QChar at(qsizetype position) const const
QString fromUtf8(QByteArrayView str)
bool isEmpty() const const
qsizetype length() const const
QString mid(qsizetype position, qsizetype n) const const
QString number(double n, char format, int precision)
void push_back(QChar ch)
void reserve(qsizetype size)
qsizetype size() const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QStringView mid(qsizetype start, qsizetype length) const const
QChar at(qsizetype n) const const
qsizetype size() const const
QList< QStringView > split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool startsWith(QChar ch) const const
CaseInsensitive
bool isValid() const const
QString path(ComponentFormattingOptions options) const const
void setPath(const QString &path, ParsingMode mode)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:07:40 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.