Commit f33666d6 authored by Volker Krause's avatar Volker Krause
Browse files

Make VDV ticket content structs introspectable

This allows us to print almost all content of a VDV ticket in the barcode
dumping tool, as well as the removal of a lot of diagnostic code from the
actual VDV ticket parser.

The validity area structs still need work to properly decode those, but I
yet have to find the official documents describing those.  We should have
enough information to decode 0080VU blocks in UIC 918.3 tickets now though.
parent 0796cb7c
Pipeline #51627 passed with stage
in 10 minutes and 51 seconds
......@@ -64,6 +64,7 @@ set(kitinerary_lib_srcs
vdv/iso9796_2decoder.cpp
vdv/vdvcertificate.cpp
vdv/vdvticket.cpp
vdv/vdvticketcontent.cpp
vdv/vdvticketparser.cpp
vdv/certs/vdv-certs.qrc
......
......@@ -7,6 +7,8 @@
#ifndef KITINERARY_BER_ELEMENT_P_H
#define KITINERARY_BER_ELEMENT_P_H
#include "kitinerary_export.h"
#include <QByteArray>
#include <cstdint>
......@@ -23,7 +25,7 @@ namespace BER {
* Implicitly this is also kinda implementing a QByteArrayRef, as this works without copying
* the underlying data.
*/
class Element
class KITINERARY_EXPORT Element
{
public:
Element();
......
......@@ -53,6 +53,11 @@ struct VdvBcdDate
}
inline operator QDate() const { return value(); }
inline bool operator==(const QDate &other) const { return value() == other; }
inline bool operator!=(const QDate &other) const { return value() != other; }
// dummy assignment operator for compatibility with the Q_PROPERTY system
inline VdvBcdDate& operator=(const QDate&) { return *this; }
};
/** Big-endian numeric value. */
......@@ -73,6 +78,9 @@ struct VdvNumber
}
inline constexpr operator uint32_t() const { return value(); }
// dummy assignment operator for compatibility with the Q_PROPERTY system
inline VdvNumber<N>& operator=(uint32_t) { return *this; }
};
/** Date/time representation encoded in 4 byte. */
......@@ -95,6 +103,11 @@ struct VdvDateTimeCompact
}
inline operator QDateTime() const { return value(); }
inline bool operator==(const QDateTime &other) const { return value() == other; }
inline bool operator!=(const QDateTime &other) const { return value() != other; }
// dummy assignment operator for compatibility with the Q_PROPERTY system
inline VdvDateTimeCompact& operator=(const QDateTime&) { return *this; }
};
#pragma pack(pop)
......
......@@ -8,6 +8,8 @@
#include "vdvdata_p.h"
#include "iso9796_2decoder_p.h"
#include "../tlv/berelement_p.h"
#include <QDate>
#include <QDebug>
#include <QFile>
......
......@@ -7,11 +7,8 @@
#ifndef KITINERARY_VDVDATA_P_H
#define KITINERARY_VDVDATA_P_H
#include "../tlv/berelement_p.h"
#include "vdvbasictypes.h"
#include <cstdint>
namespace KItinerary {
enum {
......@@ -20,8 +17,6 @@ enum {
TagCaReference = 0x42,
TagTicketProductData = 0x85,
TagTicketProductTransactionData = 0x8A,
TagTicketBasicData = 0xDA,
TagTicketTravelerData = 0xDB,
TagCertificate = 0x7F21,
TagCertificateSignature = 0x5F37,
TagCertificateSignatureRemainder = 0x5F38,
......@@ -82,80 +77,6 @@ struct VdvCertificateKey {
}
};
/** Ticket data header. */
struct VdvTicketHeader
{
VdvNumber<4> ticketId;
VdvNumber<2> kvpOrgId;
VdvNumber<2> productId;
VdvNumber<2> pvOrgId;
VdvDateTimeCompact beginDt;
VdvDateTimeCompact endDt;
};
/** Product specific data - basic information. */
struct VdvTicketBasicData
{
VdvNumber<1> paymentType;
VdvNumber<1> travelerType; // 1 adult, 2 child, 65 bike
VdvNumber<1> includedTravelerType1; // 0 none, 1 adult, 2 child, 251 family child
VdvNumber<1> includedTravelerCount1;
VdvNumber<1> includedTravelerType2;
VdvNumber<1> includedTravelerCount2;
VdvNumber<1> categroy;
VdvNumber<1> serviceClass; // 1 first class, 2 second class, 3 first class upgrade
VdvNumber<3> price; // 24 bit big endian, price in Euro cent
VdvNumber<2> vat; // VAT rate in 0,01% steps
VdvNumber<1> priceCategory;
VdvNumber<3> productNumber;
};
/** Product specific data - traveler information. */
struct VdvTicketTravelerData
{
VdvNumber<1> gender;
VdvBcdDate birthDate;
char nameBegin;
inline const char* name() const
{
return &nameBegin;
}
inline int nameSize(int elementSize) const
{
return elementSize - sizeof(VdvTicketTravelerData) + 1;
}
};
/** Ticket transaction data block. */
struct VdvTicketTransactionData
{
VdvNumber<2> kvpOrgId;
VdvNumber<1> terminalTypeCode;
VdvNumber<2> terminalNumber;
VdvNumber<2> terminalOrganizationNumber;
VdvDateTimeCompact dt;
VdvNumber<1> locationTypeCode;
VdvNumber<3> locationNumber;
VdvNumber<2> locationOrganizationNumber;
};
/** Ticket issuer data block. */
struct VdvTicketIssueData
{
VdvNumber<4> samSeq1;
VdvNumber<1> version;
VdvNumber<4> samSeq2;
VdvNumber<3> samId;
};
/** Ticket trailer, after padding. */
struct VdvTicketTrailer
{
const char identifier[3];
VdvNumber<2> version;
};
#pragma pack(pop)
}
......
......@@ -5,9 +5,12 @@
*/
#include "vdvticket.h"
#include "vdvticketcontent.h"
#include "vdvdata_p.h"
#include "logging.h"
#include "../tlv/berelement_p.h"
#include <QDebug>
using namespace KItinerary;
......@@ -19,7 +22,7 @@ public:
QByteArray m_data;
BER::Element productElement(uint32_t type) const;
template <typename T> const T* productData(uint32_t type) const;
template <typename T> const T* productData(uint32_t type = T::Tag) const;
};
}
......@@ -64,10 +67,7 @@ VdvTicket::VdvTicket(const QByteArray &data)
return;
}
offset += productBlock.size();
const auto transactionBlock = reinterpret_cast<const VdvTicketTransactionData*>(data.constData() + offset);
qDebug() << "transaction block:" << transactionBlock->kvpOrgId;
offset += sizeof(VdvTicketTransactionData);
offset += sizeof(VdvTicketCommonTransactionData);
const auto prodTransactionBlock = BER::TypedElement<TagTicketProductTransactionData>(data, offset);
if (!prodTransactionBlock.isValid()) {
......@@ -75,9 +75,6 @@ VdvTicket::VdvTicket(const QByteArray &data)
return;
}
offset += prodTransactionBlock.size();
const auto issueData = reinterpret_cast<const VdvTicketIssueData*>(data.constData() + offset);
qDebug() << issueData->version << issueData->samId;
offset += sizeof(VdvTicketIssueData);
// 0 padding to reach at least 111 bytes
......@@ -89,21 +86,6 @@ VdvTicket::VdvTicket(const QByteArray &data)
return;
}
d->m_data = data;
#if 1
const auto hdr = reinterpret_cast<const VdvTicketHeader*>(data.constData());
qDebug() << hdr->productId << hdr->pvOrgId;
// iterate over TLV content
auto tlv = productBlock.first();
while (tlv.isValid()) {
qDebug() << "tag:" << tlv.type() << "size:" << tlv.contentSize() << "content:" << QByteArray((const char*)tlv.contentData(), tlv.contentSize()).toHex();
tlv = tlv.next();
}
const auto basicData = d->productData<VdvTicketBasicData>(TagTicketBasicData);
if (basicData) {
qDebug() << "traveler type:" << basicData->travelerType;
}
#endif
}
VdvTicket::VdvTicket(const VdvTicket&) = default;
......@@ -112,37 +94,25 @@ VdvTicket& VdvTicket::operator=(const VdvTicket&) = default;
QDateTime VdvTicket::beginDateTime() const
{
if (d->m_data.isEmpty()) {
return {};
}
const auto hdr = reinterpret_cast<const VdvTicketHeader*>(d->m_data.constData());
return hdr->beginDt;
const auto hdr = header();
return hdr ? hdr->validityBegin : QDateTime();
}
QDateTime KItinerary::VdvTicket::endDateTime() const
{
if (d->m_data.isEmpty()) {
return {};
}
const auto hdr = reinterpret_cast<const VdvTicketHeader*>(d->m_data.constData());
return hdr->endDt;
const auto hdr = header();
return hdr ? hdr->validityEnd : QDateTime();
}
int VdvTicket::issuerId() const
{
if (d->m_data.isEmpty()) {
return 0;
}
const auto hdr = reinterpret_cast<const VdvTicketHeader*>(d->m_data.constData());
return hdr->kvpOrgId;
const auto hdr = header();
return hdr ? hdr->kvpOrgId : 0;
}
VdvTicket::ServiceClass VdvTicket::serviceClass() const
{
const auto tlv = d->productData<VdvTicketBasicData>(TagTicketBasicData);
const auto tlv = d->productData<VdvTicketBasicData>();
if (!tlv) {
return UnknownClass;
}
......@@ -162,12 +132,11 @@ VdvTicket::ServiceClass VdvTicket::serviceClass() const
Person VdvTicket::person() const
{
const auto elem = d->productElement(TagTicketTravelerData);
const auto elem = d->productElement(VdvTicketTravelerData::Tag);
const auto tlv = elem.isValid() ? elem.contentAt<VdvTicketTravelerData>() : nullptr;
if (!tlv) {
return {};
}
qDebug() << "traveler:" << tlv->gender << tlv->birthDate << QByteArray(tlv->name(), tlv->nameSize(elem.contentSize()));
const auto len = strnlen(tlv->name(), tlv->nameSize(elem.contentSize())); // name field can contain null bytes
if (len == 0) {
......@@ -202,10 +171,46 @@ Person VdvTicket::person() const
QString VdvTicket::ticketNumber() const
{
if (d->m_data.isEmpty()) {
return {};
}
const auto hdr = header();
return hdr ? QString::number(hdr->ticketId) : QString();
}
const auto hdr = reinterpret_cast<const VdvTicketHeader*>(d->m_data.constData());
return QString::number(hdr->ticketId);
const VdvTicketHeader* VdvTicket::header() const
{
return d->m_data.isEmpty() ? nullptr : reinterpret_cast<const VdvTicketHeader*>(d->m_data.constData());
}
BER::Element VdvTicket::productData() const
{
const auto productElement = BER::Element(d->m_data, sizeof(VdvTicketHeader));
return (productElement.isValid() && productElement.type() == TagTicketProductData) ? productElement : BER::Element();
}
const VdvTicketCommonTransactionData* VdvTicket::commonTransactionData() const
{
return d->m_data.isEmpty() ? nullptr :
reinterpret_cast<const VdvTicketCommonTransactionData*>(d->m_data.constData() + sizeof(VdvTicketHeader) + productData().size());
}
BER::Element VdvTicket::productSpecificTransactionData() const
{
const auto offset = sizeof(VdvTicketHeader) + productData().size() + sizeof(VdvTicketCommonTransactionData);
const auto elem = BER::Element(d->m_data, offset);
return (elem.isValid() && elem.type() == TagTicketProductTransactionData) ? elem : BER::Element();
}
const VdvTicketIssueData* VdvTicket::issueData() const
{
const auto offset = sizeof(VdvTicketHeader) + productData().size()
+ sizeof(VdvTicketCommonTransactionData) + productSpecificTransactionData().size();
return d->m_data.isEmpty() ? nullptr : reinterpret_cast<const VdvTicketIssueData*>(d->m_data.constData() + offset);
}
const VdvTicketTrailer* VdvTicket::trailer() const
{
auto offset = sizeof(VdvTicketHeader) + productData().size()
+ sizeof(VdvTicketCommonTransactionData) + productSpecificTransactionData().size()
+ sizeof(VdvTicketIssueData);
offset += std::max<int>(111 - offset - sizeof(VdvTicketTrailer), 0); // padding to 111 bytes
return d->m_data.isEmpty() ? nullptr : reinterpret_cast<const VdvTicketTrailer*>(d->m_data.constData() + offset);
}
......@@ -17,7 +17,12 @@
namespace KItinerary {
class VdvTicketHeader;
class VdvTicketPrivate;
class VdvTicketCommonTransactionData;
class VdvTicketIssueData;
struct VdvTicketTrailer;
namespace BER { class Element; }
/** Ticket information from a VDV barcode.
* For use by tooling or custom extractor scripts.
......@@ -62,6 +67,14 @@ public:
Person person() const;
QString ticketNumber() const;
// low-level content access
const VdvTicketHeader* header() const;
BER::Element productData() const;
const VdvTicketCommonTransactionData* commonTransactionData() const;
BER::Element productSpecificTransactionData() const;
const VdvTicketIssueData* issueData() const;
const VdvTicketTrailer* trailer() const;
private:
QExplicitlySharedDataPointer<VdvTicketPrivate> d;
};
......
/*
SPDX-FileCopyrightText: 2021 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "vdvticketcontent.h"
#include "moc_vdvticketcontent.cpp"
/*
SPDX-FileCopyrightText: 2019-2021 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KITINERARY_VDVTICKETCONTENT_H
#define KITINERARY_VDVTICKETCONTENT_H
#include "kitinerary_export.h"
#include "vdvbasictypes.h"
#include <qobjectdefs.h>
namespace KItinerary {
#define VDV_NUM_PROPERTY(Name, Size) \
public: \
VdvNumber<Size> Name; \
Q_PROPERTY(uint Name MEMBER Name)
#define VDV_DATETIME_PROPERTY(Name) \
public: \
VdvDateTimeCompact Name; \
Q_PROPERTY(QDateTime Name MEMBER Name)
#define VDV_DATE_PROPERTY(Name) \
public: \
VdvBcdDate Name; \
Q_PROPERTY(QDate Name MEMBER Name)
#pragma pack(push)
#pragma pack(1)
/** Ticket data header. */
class KITINERARY_EXPORT VdvTicketHeader
{
Q_GADGET
VDV_NUM_PROPERTY(ticketId, 4)
VDV_NUM_PROPERTY(kvpOrgId, 2)
VDV_NUM_PROPERTY(productId, 2)
VDV_NUM_PROPERTY(pvOrgId, 2)
VDV_DATETIME_PROPERTY(validityBegin)
VDV_DATETIME_PROPERTY(validityEnd)
};
/** Product specific data - basic information. */
class KITINERARY_EXPORT VdvTicketBasicData
{
Q_GADGET
VDV_NUM_PROPERTY(paymentType, 1)
VDV_NUM_PROPERTY(travelerType, 1) // 1 adult, 2 child, 65 bike
VDV_NUM_PROPERTY(includedTravelerType1, 1) // 0 none, 1 adult, 2 child, 251 family child
VDV_NUM_PROPERTY(includedTravelerCount1, 1)
VDV_NUM_PROPERTY(includedTravelerType2, 1)
VDV_NUM_PROPERTY(includedTravelerCount2, 1)
VDV_NUM_PROPERTY(categroy, 1)
VDV_NUM_PROPERTY(serviceClass, 1) // 1 first class, 2 second class, 3 first class upgrade
VDV_NUM_PROPERTY(price, 3) // 24 bit big endian, price in Euro cent
VDV_NUM_PROPERTY(vat, 2) // VAT rate in 0,01% steps
VDV_NUM_PROPERTY(priceCategory, 1)
VDV_NUM_PROPERTY(productNumber, 3)
public:
enum { Tag = 0xDA };
};
/** Product specific data - traveler information. */
class KITINERARY_EXPORT VdvTicketTravelerData
{
Q_GADGET
VDV_NUM_PROPERTY(gender, 1)
VDV_DATE_PROPERTY(birthDate)
public:
char nameBegin;
inline const char* name() const
{
return &nameBegin;
}
inline int nameSize(int elementSize) const
{
return elementSize - sizeof(VdvTicketTravelerData) + 1;
}
enum { Tag = 0xDB };
};
/** Ticket validity area data block. */
class KITINERARY_EXPORT VdvTicketValidityAreaData
{
Q_GADGET
VDV_NUM_PROPERTY(type, 1)
VDV_NUM_PROPERTY(orgId, 2)
public:
enum { Tag = 0xDC };
};
class KITINERARY_EXPORT VdvTicketValidityAreaDataType31 : public VdvTicketValidityAreaData
{
Q_GADGET
VDV_NUM_PROPERTY(startId, 3)
VDV_NUM_PROPERTY(destinationId, 3)
VDV_NUM_PROPERTY(wayTextId, 2)
VDV_NUM_PROPERTY(ticketRelation, 4)
VDV_NUM_PROPERTY(pointCloudId, 4)
public:
enum { Type = 0x31 };
};
/** Ticket transaction data block. */
class KITINERARY_EXPORT VdvTicketCommonTransactionData
{
Q_GADGET
VDV_NUM_PROPERTY(kvpOrgId, 2)
VDV_NUM_PROPERTY(terminalTypeCode, 1)
VDV_NUM_PROPERTY(terminalNumber, 2)
VDV_NUM_PROPERTY(terminalOrganizationNumber, 2)
VDV_DATETIME_PROPERTY(transactionDateTime)
VDV_NUM_PROPERTY(locationTypeCode, 1)
VDV_NUM_PROPERTY(locationNumber, 3)
VDV_NUM_PROPERTY(locationOrganizationNumber, 2)
};
/** Ticket issuer data block. */
class KITINERARY_EXPORT VdvTicketIssueData
{
Q_GADGET
VDV_NUM_PROPERTY(samSeq1, 4)
VDV_NUM_PROPERTY(version, 1)
VDV_NUM_PROPERTY(samSeq2, 4)
VDV_NUM_PROPERTY(samId, 3)
};
/** Ticket trailer, after padding. */
struct VdvTicketTrailer
{
const char identifier[3];
VdvNumber<2> version;
};
#pragma pack(pop)
#undef VDV_NUM_PROPERTY
#undef VDV_DATETIME_PROPERTY
#undef VDV_DATE_PROPERTY
}
#endif // KITINERARY_VDVTICKETCONTENT_H
......@@ -10,6 +10,8 @@
#include "iso9796_2decoder_p.h"
#include "logging.h"
#include "../tlv/berelement_p.h"
#include <QByteArray>
#include <QDebug>
......
......@@ -6,6 +6,8 @@
#include "../lib/era/ssbticket.h"
#include "../lib/uic9183/uic9183head.h"
#include "../lib/vdv/vdvticketcontent.h"
#include "../lib/tlv/berelement_p.h"
#include <kitinerary_version.h>
......@@ -29,7 +31,7 @@
using namespace KItinerary;
void dumpSsbTicket(const QByteArray &data)
static void dumpSsbTicket(const QByteArray &data)
{
SSBTicket ticket(data);
......@@ -58,7 +60,7 @@ void dumpSsbTicket(const QByteArray &data)
}
}
void dumpRawData(const char *data, std::size_t size)
static void dumpRawData(const char *data, std::size_t size)
{
bool isText = true;
for (std::size_t i = 0; i < size && isText; ++i) {
......@@ -72,7 +74,17 @@ void dumpRawData(const char *data, std::size_t size)
}
}
void dumpUic9183(const QByteArray &data)
template <typename T>
static void dumpGadget(const T *gadget, const char* indent)
{
for (auto i = 0; i < T::staticMetaObject.propertyCount(); ++i) {
const auto prop = T::staticMetaObject.property(i);
const auto value = prop.readOnGadget(gadget);
std::cout << indent << prop.name() << ": " << qPrintable(value.toString()) << std::endl;
}
}
static void dumpUic9183(const QByteArray &data)
{
Uic9183Parser parser;
parser.parse(data);
......@@ -86,11 +98,7 @@ void dumpUic9183(const QByteArray &data)
if (block.isA<Uic9183Head>()) {
Uic9183Head head(block);
for (auto i = 0; i < Uic9183Head::staticMetaObject.propertyCount(); ++i) {
const auto prop = Uic9183Head::staticMetaObject.property(i);
const auto value = prop.readOnGadget(&head);
std::cout << " " << prop.name() << ": " << qPrintable(value.toString()) << std::endl;
}
dumpGadget(&head, " ");
} else if (block.isA<Uic9183TicketLayout>()) {
Uic9183TicketLayout tlay(block);
std::cout << " Layout standard: " << qPrintable(tlay.type()) << std::endl;
......@@ -117,13 +125,73 @@ void dumpUic9183(const QByteArray &data)
}
}
void dumpVdv(const QByteArray &data)
static void dumpVdv(const QByteArray &data)
{
VdvTicketParser parser;
if (!parser.parse(data)) {
std::cerr << "failed to parse VDV ticket" << std::endl;
return;
}
const auto ticket = parser.ticket();
std::cout << " Header:" << std::endl;
dumpGadget(ticket.header(), " ");
std::cout << " Product data:" << std::endl;
for (auto block = ticket.productData().first(); block.isValid(); block = block.next()) {
std::cout << " Tag: 0x" << std::hex << block.type() << std::