diff --git a/autotests/vdvtickettest.cpp b/autotests/vdvtickettest.cpp index 4b09aaa..85ba83f 100644 --- a/autotests/vdvtickettest.cpp +++ b/autotests/vdvtickettest.cpp @@ -1,70 +1,90 @@ /* Copyright (C) 2019 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include #include #include using namespace KItinerary; class VdvTicketTest : public QObject { Q_OBJECT private Q_SLOTS: void testMaybeVdvTicket_data() { QTest::addColumn("input"); QTest::addColumn("isVdv"); QTest::newRow("empty") << QByteArray() << false; QTest::newRow("null") << QByteArray(352, 0x0) << false; QByteArray b(352, 0x0); b[0] = (char)0x9E; b[1] = (char)0x81; b[2] = (char)0x80; b[131] = (char)0x9A; b[132] = (char)0x05; b[133] = 'V'; b[134] = 'D'; b[135] = 'V'; QTest::newRow("valid min length") << b << true; } void testMaybeVdvTicket() { QFETCH(QByteArray, input); QFETCH(bool, isVdv); QCOMPARE(VdvTicketParser::maybeVdvTicket(input), isVdv); } void testTicket() { - const auto data = QByteArray::fromHex("00f569d018f111d017d43b7d68003b8268008532da110000030000000000000992000000000000db0c000000000000000000000000dc0f1117d40000000004b000000000000018f110007a18f13b7974cfd400000017d48a00000105d601000105d602474200000000005644561400"); + auto data = QByteArray::fromHex("00f569d018f111d017d43b7d68003b8268008532da110000030000000002000992000000000000db0c000000000000000000000000dc0f1117d40000000004b000000000000018f110007a18f13b7974cfd400000017d48a00000105d601000105d602474200000000005644561400"); VdvTicket ticket(data); QCOMPARE(ticket.issuerId(), 6385); QCOMPARE(ticket.beginDateTime(), QDateTime({2019, 11, 29}, {13, 0})); QCOMPARE(ticket.endDateTime(), QDateTime({2019, 12, 2}, {13, 0})); + QCOMPARE(ticket.serviceClass(), VdvTicket::SecondClass); + QCOMPARE(ticket.person(), Person()); + QCOMPARE(ticket.ticketNumber(), QStringLiteral("16083408")); + + data = QByteArray::fromHex("001a4bab1874283e184434ba000134bb18008541da110001000000000003000000000000001a4bdb1502199610144B6174696523447261676F6E00000000dc150000000000000000000000000000000000000000001874110064187434b87128ff7a126918748a0000062e9e0100062e9e007d895644561107"); + ticket = VdvTicket(data); + QCOMPARE(ticket.issuerId(), 6260); + QCOMPARE(ticket.beginDateTime(), QDateTime({2016, 5, 26}, {0, 0, 2})); + QCOMPARE(ticket.endDateTime(), QDateTime({2016, 5, 27}, {3, 0})); + QCOMPARE(ticket.serviceClass(), VdvTicket::FirstClassUpgrade); + QCOMPARE(ticket.person().familyName(), QStringLiteral("Dragon")); + QCOMPARE(ticket.person().givenName(), QStringLiteral("Katie")); + QCOMPARE(ticket.ticketNumber(), QStringLiteral("1723307")); + + data = QByteArray::fromHex("00f569d018f111d017d43b7d68003b8268008532da110000030000000000000992000000000000db0c00000000004B33654044346Edc0f1117d40000000004b000000000000018f110007a18f13b7974cfd400000017d48a00000105d601000105d602474200000000005644561400"); + ticket = VdvTicket(data); + QCOMPARE(ticket.serviceClass(), VdvTicket::UnknownClass); + QCOMPARE(ticket.person().familyName(), QStringLiteral("D")); + QCOMPARE(ticket.person().givenName(), QStringLiteral("K")); + QCOMPARE(ticket.ticketNumber(), QStringLiteral("16083408")); } }; QTEST_APPLESS_MAIN(VdvTicketTest) #include "vdvtickettest.moc" diff --git a/src/generic/genericvdvextractor.cpp b/src/generic/genericvdvextractor.cpp index 3d905af..c0adeae 100644 --- a/src/generic/genericvdvextractor.cpp +++ b/src/generic/genericvdvextractor.cpp @@ -1,65 +1,77 @@ /* Copyright (C) 2019 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "genericvdvextractor_p.h" +#include #include #include +#include + #include #include #include using namespace KItinerary; QJsonArray GenericVdvExtractor::extract(const QByteArray &data) { VdvTicketParser p; if (!p.parse(data)) { return {}; } const auto vdv = p.ticket(); QJsonObject org; org.insert(QStringLiteral("@type"), QLatin1String("Organization")); org.insert(QStringLiteral("identifier"), QString(QLatin1String("vdv:") + QString::number(vdv.issuerId()))); QJsonObject trip; trip.insert(QStringLiteral("@type"), QLatin1String("TrainTrip")); trip.insert(QStringLiteral("provider"), org); QJsonObject seat; seat.insert(QStringLiteral("@type"), QLatin1String("Seat")); -// seat.insert(QStringLiteral("seatingType"), vdv.serviceClass()); + switch (vdv.serviceClass()) { + case VdvTicket::FirstClass: + case VdvTicket::FirstClassUpgrade: + seat.insert(QStringLiteral("seatingType"), QStringLiteral("1")); + break; + case VdvTicket::SecondClass: + seat.insert(QStringLiteral("seatingType"), QStringLiteral("2")); + break; + default: + break; + } QJsonObject ticket; ticket.insert(QStringLiteral("@type"), QLatin1String("Ticket")); ticket.insert(QStringLiteral("ticketToken"), QString(QLatin1String("aztecbin:") + QString::fromLatin1(data.toBase64()))); ticket.insert(QStringLiteral("ticketedSeat"), seat); - - QJsonObject person; - person.insert(QStringLiteral("@type"), QLatin1String("Person")); -// person.insert(QStringLiteral("name"), vdv.passengerName()); + if (vdv.serviceClass() == VdvTicket::FirstClassUpgrade) { + ticket.insert(QStringLiteral("name"), i18n("Upgrade")); + } QJsonObject res; res.insert(QStringLiteral("@type"), QLatin1String("TrainReservation")); res.insert(QStringLiteral("reservationFor"), trip); -// res.insert(QStringLiteral("reservationNumber"), vdv.ticketNumber()); + res.insert(QStringLiteral("reservationNumber"), vdv.ticketNumber()); res.insert(QStringLiteral("reservedTicket"), ticket); - res.insert(QStringLiteral("underName"), person); + res.insert(QStringLiteral("underName"), JsonLdDocument::toJson(vdv.person())); return {res}; } diff --git a/src/vdv/vdvticket.cpp b/src/vdv/vdvticket.cpp index 70777fd..612a00b 100644 --- a/src/vdv/vdvticket.cpp +++ b/src/vdv/vdvticket.cpp @@ -1,142 +1,217 @@ /* Copyright (C) 2019 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "vdvticket.h" #include "vdvdata_p.h" #include "logging.h" #include using namespace KItinerary; namespace KItinerary { class VdvTicketPrivate : public QSharedData { public: QByteArray m_data; + + template const T* productData() const; }; } +template +const T* VdvTicketPrivate::productData() const +{ + if (m_data.isEmpty()) { + return nullptr; + } + + const auto productBlock = reinterpret_cast(m_data.constData() + sizeof(VdvTicketHeader)); + return productBlock->contentByTag(); +} VdvTicket::VdvTicket() : d(new VdvTicketPrivate) { } VdvTicket::VdvTicket(const QByteArray &data) : d(new VdvTicketPrivate) { if (data.size() < MinimumTicketDataSize) { qCWarning(Log) << "Ticket data too small" << data.size(); return; } static_assert(sizeof(VdvTicketHeader) < MinimumTicketDataSize, ""); - const auto hdr = reinterpret_cast(data.constData()); int offset = sizeof(VdvTicketHeader); const auto productBlock = reinterpret_cast(data.constData() + offset); if (!productBlock->isValid() || productBlock->size() + offset > data.size()) { qCWarning(Log) << "Invalid product block" << productBlock->isValid() << productBlock->size() << offset << data.size(); return; } offset += productBlock->size(); const auto transactionBlock = reinterpret_cast(data.constData() + offset); qDebug() << "transaction block:" << qFromBigEndian(transactionBlock->kvpOrgId); offset += sizeof(VdvTicketTransactionData); const auto prodTransactionBlock = reinterpret_cast(data.constData() + offset); if (!prodTransactionBlock->isValid() || prodTransactionBlock->size() + offset > data.size()) { qCWarning(Log) << "Invalid product transaction block" << prodTransactionBlock->isValid() << prodTransactionBlock->size() << offset << data.size(); return; } offset += prodTransactionBlock->size(); const auto issueData = reinterpret_cast(data.constData() + offset); qDebug() << issueData->version << QByteArray((const char*)&issueData->samId, 3).toHex(); offset += sizeof(VdvTicketIssueData); - qDebug() << "padding:" << std::max(111 - offset, 0); + // 0 padding to reach at least 111 bytes offset += std::max(111 - offset - (int)sizeof(VdvTicketTrailer), 0); const auto trailer = reinterpret_cast(data.constData() + offset); - qDebug() << QByteArray(trailer->identifier, 3) << qFromBigEndian(trailer->version); if (memcmp(trailer->identifier, "VDV", 3) != 0) { - qCWarning(Log) << "Invalid ticket trailer identifier."; + qCWarning(Log) << "Invalid ticket trailer identifier:" << QByteArray(trailer->identifier, 3) << qFromBigEndian(trailer->version); return; } d->m_data = data; - // TODO temporary - qDebug() << qFromBigEndian(hdr->ticketId) << issuerId() << qFromBigEndian(hdr->productId) << qFromBigEndian(hdr->pvOrgId); - qDebug() << "begin:" << beginDateTime(); - qDebug() << "end:" << endDateTime(); +#if 0 + const auto hdr = reinterpret_cast(data.constData()); + qDebug() << qFromBigEndian(hdr->productId) << qFromBigEndian(hdr->pvOrgId); // iterate over TLV content auto tlv = productBlock->first(); while (tlv) { qDebug() << "tag:" << tlv->tag << "size:" << tlv->contentSize() << "content:" << QByteArray((const char*)tlv->contentData(), tlv->contentSize()).toHex(); tlv = productBlock->next(tlv); } const auto basicData = productBlock->contentByTag(); if (basicData) { - qDebug() << "traveler type:" << basicData->travelerType << "class:" << basicData->serviceClass; - } - const auto travelerData = productBlock->contentByTag(); - if (travelerData) { - qDebug() << "traveler:" << travelerData->gender << QDate(travelerData->birthDate.year(), travelerData->birthDate.month(), travelerData->birthDate.day()) << QByteArray(travelerData->name(), travelerData->nameSize()); + qDebug() << "traveler type:" << basicData->travelerType; } +#endif } VdvTicket::VdvTicket(const VdvTicket&) = default; VdvTicket::~VdvTicket() = default; VdvTicket& VdvTicket::operator=(const VdvTicket&) = default; static QDateTime dtCompactToQdt(const VdvDateTimeCompact &dtc) { return QDateTime({dtc.year(), dtc.month(), dtc.day()}, {dtc.hour(), dtc.minute(), dtc.second()}); } QDateTime VdvTicket::beginDateTime() const { if (d->m_data.isEmpty()) { return {}; } const auto hdr = reinterpret_cast(d->m_data.constData()); return dtCompactToQdt(hdr->beginDt); } QDateTime KItinerary::VdvTicket::endDateTime() const { if (d->m_data.isEmpty()) { return {}; } const auto hdr = reinterpret_cast(d->m_data.constData()); return dtCompactToQdt(hdr->endDt); } int VdvTicket::issuerId() const { if (d->m_data.isEmpty()) { return 0; } const auto hdr = reinterpret_cast(d->m_data.constData()); return qFromBigEndian(hdr->kvpOrgId); } + +VdvTicket::ServiceClass VdvTicket::serviceClass() const +{ + const auto tlv = d->productData(); + if (!tlv) { + return UnknownClass; + } + switch (tlv->serviceClass) { + case 0: + return UnknownClass; + case 1: + return FirstClass; + case 2: + return SecondClass; + case 3: + return FirstClassUpgrade; + } + qCDebug(Log) << "Unknown service class:" << tlv->serviceClass; + return UnknownClass; +} + +Person VdvTicket::person() const +{ + const auto tlv = d->productData(); + if (!tlv) { + return {}; + } + qDebug() << "traveler:" << tlv->gender << QDate(tlv->birthDate.year(), tlv->birthDate.month(), tlv->birthDate.day()) << QByteArray(tlv->name(), tlv->nameSize()); + + const auto len = strnlen(tlv->name(), tlv->nameSize()); // name field can contain null bytes + if (len == 0) { + return {}; + } + + const auto name = QString::fromUtf8(tlv->name(), len); + + Person p; + const auto idxHash = name.indexOf(QLatin1Char('#')); + const auto idxAt = name.indexOf(QLatin1Char('@')); + + // encoding as first#last + if (idxHash > 0) { + p.setFamilyName(name.mid(idxHash + 1)); + p.setGivenName(name.left(idxHash)); + } + + // encoding as f1fn@l1ln + else if (idxAt > 0) { + p.setFamilyName(name.at(idxAt + 1)); + p.setGivenName(name.at(0)); + } + + // unknown encoding + else { + p.setName(name); + } + + return p; +} + +QString VdvTicket::ticketNumber() const +{ + if (d->m_data.isEmpty()) { + return {}; + } + + const auto hdr = reinterpret_cast(d->m_data.constData()); + return QString::number(qFromBigEndian(hdr->ticketId)); +} diff --git a/src/vdv/vdvticket.h b/src/vdv/vdvticket.h index e75f624..621ac93 100644 --- a/src/vdv/vdvticket.h +++ b/src/vdv/vdvticket.h @@ -1,64 +1,84 @@ /* Copyright (C) 2019 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef KITINERARY_VDVTICKET_H #define KITINERARY_VDVTICKET_H #include "kitinerary_export.h" +#include + #include #include #include namespace KItinerary { class VdvTicketPrivate; /** Ticket information from a VDV barcode. * For use by tooling or custom extractor scripts. */ class KITINERARY_EXPORT VdvTicket { Q_GADGET /** Begin of the validitiy of this ticket. */ Q_PROPERTY(QDateTime beginDateTime READ beginDateTime) /** End of the validity of this ticket. */ Q_PROPERTY(QDateTime endDateTime READ endDateTime) /** VDV organization identifier of the ticket issuer. */ Q_PROPERTY(int issuerId READ issuerId) + /** Service class for this ticket. */ + Q_PROPERTY(ServiceClass serviceClass READ serviceClass) + /** The person this ticket is valid for. */ + Q_PROPERTY(KItinerary::Person person READ person) + /** Ticket number. */ + Q_PROPERTY(QString ticketNumber READ ticketNumber) public: VdvTicket(); VdvTicket(const QByteArray &data); VdvTicket(const VdvTicket&); ~VdvTicket(); VdvTicket& operator=(const VdvTicket&); QDateTime beginDateTime() const; QDateTime endDateTime() const; int issuerId() const; + enum ServiceClass { + UnknownClass = 0, + FirstClass = 1, + SecondClass = 2, + FirstClassUpgrade = 3 + }; + Q_ENUM(ServiceClass) + ServiceClass serviceClass() const; + + Person person() const; + QString ticketNumber() const; + private: QExplicitlySharedDataPointer d; }; } Q_DECLARE_METATYPE(KItinerary::VdvTicket) #endif // KITINERARY_VDVTICKET_H