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

Add ERA SSB ticket decoder

This is the low-level format behind Thalys tickets, and will allow us to
extract more information from those once the Thalys extracts is ported to
use this.

Also add a simple tool to dump the low-level content of ticket barcodes,
to be extended especially for the other binary formats.
parent 0efe9f75
......@@ -7,3 +7,4 @@ if (TARGET Qt5::Network AND NOT CMAKE_CROSSCOMPILING AND OSM_PLANET_DIR AND OsmT
endif()
add_subdirectory(lib)
add_subdirectory(cli)
add_subdirectory(tools)
......@@ -18,6 +18,8 @@ set(kitinerary_lib_srcs
datatypes/rentalcar.cpp
datatypes/visit.cpp
era/ssbticket.cpp
generic/genericextractor.cpp
generic/genericicalextractor.cpp
generic/genericpdfextractor.cpp
......
/*
SPDX-FileCopyrightText: 2021 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "ssbticket.h"
#include <QDebug>
#include <cstring>
using namespace KItinerary;
enum {
SSB_DATA_SIZE = 114,
SSB_CHAR_WIDTH = 6,
SSB_VERSION = 3,
};
SSBTicket::SSBTicket() = default;
SSBTicket::SSBTicket(const QByteArray &data)
{
if (maybeSSB(data)) {
m_data = data;
} else {
qWarning() << "Trying to construct an SSB ticket from invalid data!";
}
}
SSBTicket::~SSBTicket() = default;
int SSBTicket::readNumber(int start, int length) const
{
if (start < 0 || length < 1 || start / 8 >= m_data.size() || (start + length) / 8 >= m_data.size() || length > 31) {
qWarning() << "invalid SSB read:" << start << length;
return {};
}
uint64_t num = 0;
for (int i = 0; i < 8; ++i) {
num <<= 8;
num |= (uint8_t)*(m_data.constData() + (start / 8) + i);
}
num <<= start % 8;
num >>= 64 - length;
return num;
}
QString SSBTicket::readString(int start, int length) const
{
QString res;
res.resize(length);
for (int i = 0; i < length; ++i) {
res[i] = (char)(readNumber(start + SSB_CHAR_WIDTH * i, SSB_CHAR_WIDTH) + 32);
}
return res;
}
bool SSBTicket::maybeSSB(const QByteArray& data)
{
if (data.size() != SSB_DATA_SIZE) {
return false;
}
return (data.at(0) >> 4) == SSB_VERSION;
}
QDate SSBTicket::issueDate(const QDate &contextDate)
{
if (m_data.isEmpty() || ticketTypeCode() > SSBTicket::RPT) {
return {};
}
int year = contextDate.year();
if (year % 10 != yearOfIssue()) {
year += (10 + yearOfIssue() - year % 10) % 10;
}
QDate d(year, 1, 1);
d = d.addDays(issuingDay() - 1);
return d;
}
QDate SSBTicket::type1DepartureDay(const QDate& contextDate)
{
if (ticketTypeCode() != SSBTicket::IRT_RES_BOA) {
return {};
}
const auto d = issueDate(contextDate);
return d.addDays(type1DepartureDate());
}
/*
SPDX-FileCopyrightText: 2021 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KITINERARY_SSBTICKET_H
#define KITINERARY_SSBTICKET_H
#include "kitinerary_export.h"
#include <QDateTime>
#include <QMetaType>
namespace KItinerary {
#define SSB_NUM_PROPERTY(Name, Start, Len) \
public: \
inline int Name() const { return readNumber(Start, Len); } \
Q_PROPERTY(int Name READ Name)
#define SSB_STR_PROPERTY(Name, Start, Len) \
public: \
inline QString Name() const { return readString(Start, Len); } \
Q_PROPERTY(QString Name READ Name)
/** ERA SSB ticket barcode.
* @see ERA TAP TSI TD B.12 Digital Security Elements For Rail Passenger Ticketing - §7 SSB - Small Structured Barcode
*/
class KITINERARY_EXPORT SSBTicket {
Q_GADGET
// low-level raw value access
// header
SSB_NUM_PROPERTY(version, 0, 4)
SSB_NUM_PROPERTY(issuerCode, 4, 14)
SSB_NUM_PROPERTY(id, 18, 4)
SSB_NUM_PROPERTY(ticketTypeCode, 22, 5)
// open data common part for type 1-4
SSB_NUM_PROPERTY(numberOfAdultPassengers, 27, 7)
SSB_NUM_PROPERTY(numberOfChildPassengers, 34, 7)
SSB_NUM_PROPERTY(specimen, 41, 1)
SSB_NUM_PROPERTY(classOfTravel, 42, 6)
SSB_STR_PROPERTY(tcn, 48, 14)
SSB_NUM_PROPERTY(yearOfIssue, 132, 4) // last digit only
SSB_NUM_PROPERTY(issuingDay, 136, 9)
// open data type 1 (IRT, RES, BOA) variant
SSB_NUM_PROPERTY(type1SubTicketType, 145, 2)
SSB_NUM_PROPERTY(type1StationCodeNumericOrAlpha, 147, 1)
SSB_NUM_PROPERTY(type1StationCodeListType, 148, 4)
SSB_NUM_PROPERTY(type1DepartureStationNum, 152, 28)
SSB_STR_PROPERTY(type1DepartureStationAlpha, 148, 5)
SSB_NUM_PROPERTY(type1ArrivalStationNum, 180, 28)
SSB_STR_PROPERTY(type1ArrivalStationAlpha, 178, 5)
SSB_NUM_PROPERTY(type1DepartureDate, 208, 9)
SSB_NUM_PROPERTY(type1DepartureTime, 217, 11)
SSB_STR_PROPERTY(type1TrainNumber, 228, 5)
SSB_NUM_PROPERTY(type1CoachNumber, 258, 10)
SSB_STR_PROPERTY(type1SeatNumbe, 268, 3)
SSB_NUM_PROPERTY(type1OverbookingIndicator, 286, 1)
SSB_NUM_PROPERTY(type1InformationMessages, 287, 14)
SSB_STR_PROPERTY(type1OpenText, 301, 27)
// open data type 2 (NRT) variant
SSB_NUM_PROPERTY(type2ReturnJourneyFlag, 145, 1)
SSB_NUM_PROPERTY(type2FirstDayOfValidity, 146, 9)
SSB_NUM_PROPERTY(type2LastDayOfValidity, 155, 9)
SSB_NUM_PROPERTY(type2StationCodeNumericOrAlpha, 164, 1)
SSB_NUM_PROPERTY(type2StationCodeListType, 165, 4)
SSB_NUM_PROPERTY(type2DepartureStationNum, 169, 28)
SSB_STR_PROPERTY(type2DepartureStationAlpha, 165, 5)
SSB_NUM_PROPERTY(type2ArrivalStationNum, 197, 28)
SSB_STR_PROPERTY(type2ArrivalStationAlpha, 195, 5)
SSB_NUM_PROPERTY(type2InformationMessages, 225, 14)
SSB_STR_PROPERTY(type2OpenText, 239, 37)
// open data type 3 (GRT) variant
SSB_NUM_PROPERTY(type3ReturnJourneyFlag, 145, 1)
SSB_NUM_PROPERTY(type3FirstDayOfValidity, 146, 9)
SSB_NUM_PROPERTY(type3LastDayOfValidity, 155, 9)
SSB_NUM_PROPERTY(type3StationCodeNumericOrAlpha, 164, 1)
SSB_NUM_PROPERTY(type3StationCodeListType, 165, 4)
SSB_NUM_PROPERTY(type3DepartureStationNum, 169, 28)
SSB_STR_PROPERTY(type3DepartureStationAlpha, 165, 5)
SSB_NUM_PROPERTY(type3ArrivalStationNum, 197, 28)
SSB_STR_PROPERTY(type3ArrivalStationAlpha, 195, 5)
SSB_STR_PROPERTY(type3NameOfGroupLeader, 225, 12)
SSB_NUM_PROPERTY(type3CountermarkNumber, 297, 8)
SSB_NUM_PROPERTY(type3InformationMessages, 305, 14)
SSB_STR_PROPERTY(type3OpenText, 305, 24)
// open data type 4 (RPT) variant
SSB_NUM_PROPERTY(type4RPTSubTicketType, 145, 2)
SSB_NUM_PROPERTY(type4FirstDayOfValidity, 147, 9)
SSB_NUM_PROPERTY(type4LastDayOfValidity, 156, 9)
SSB_NUM_PROPERTY(type4NumberOfDaysOfTravelAllowed, 165, 7)
SSB_NUM_PROPERTY(type4CountryCode1, 172, 7)
SSB_NUM_PROPERTY(type4CountryCode2, 179, 7)
SSB_NUM_PROPERTY(type4CountryCode3, 186, 7)
SSB_NUM_PROPERTY(type4CountryCode4, 193, 7)
SSB_NUM_PROPERTY(type4SecondPage, 200, 1)
SSB_NUM_PROPERTY(type4InformationMessages, 201, 14)
SSB_STR_PROPERTY(type4OpenText, 215, 40)
enum TicketType {
IRT_RES_BOA = 1,
NRT = 2,
GRT = 3,
RPT = 4
};
Q_ENUM(TicketType)
public:
SSBTicket();
explicit SSBTicket(const QByteArray &data);
~SSBTicket();
/** Date of issue. */
Q_INVOKABLE QDate issueDate(const QDate &contextDate = QDate::currentDate());
/** Departure day for type 1 (IRT/RES/BOA) tickets. */
Q_INVOKABLE QDate type1DepartureDay(const QDate &contextDate = QDate::currentDate());
/** Returns @c true if @p data might be an ERA SSB ticket. */
static bool maybeSSB(const QByteArray &data);
private:
// start and length in bits
int readNumber(int start, int length) const;
QString readString(int start, int length) const;
QByteArray m_data;
};
#undef SSB_NUM_PROPERTY
#undef SSB_STR_PROPERTY
}
Q_DECLARE_METATYPE(KItinerary::SSBTicket)
#endif // KITINERARY_SSBTICKET_H
add_executable(ticket-barcode-dump ticket-barcode-dump.cpp)
target_include_directories(ticket-barcode-dump PRIVATE ${CMAKE_BINARY_DIR})
target_link_libraries(ticket-barcode-dump KPimItinerary)
/*
SPDX-FileCopyrightText: 2021 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "../lib/era/ssbticket.h"
#include <kitinerary_version.h>
#include <KItinerary/IataBcbpParser>
#include <KItinerary/Uic9183Parser>
#include <KItinerary/VdvTicket>
#include <KItinerary/VdvTicketParser>
#include <QCommandLineParser>
#include <QCoreApplication>
#include <QDebug>
#include <QFile>
#include <QMetaProperty>
#include <iostream>
#include <cstring>
using namespace KItinerary;
void dumpSsbTicket(const QByteArray &data)
{
SSBTicket ticket(data);
const auto typePrefix = QByteArray("type" + QByteArray::number(ticket.ticketTypeCode()));
for (auto i = 0; i < SSBTicket::staticMetaObject.propertyCount(); ++i) {
const auto prop = SSBTicket::staticMetaObject.property(i);
if (std::strncmp(prop.name(), "type", 4) == 0 && std::strncmp(prop.name(), typePrefix.constData(), 5) != 0) {
continue;
}
const auto value = prop.readOnGadget(&ticket);
switch (value.type()) {
case QVariant::Int:
std::cout << prop.name() << ": " << value.toInt() << std::endl;
break;
default:
std::cout << prop.name() << ": " << qPrintable(value.toString()) << std::endl;
break;
}
}
if (ticket.ticketTypeCode() == SSBTicket::IRT_RES_BOA) {
std::cout << std::endl;
std::cout << "Issuing day: " << qPrintable(ticket.issueDate().toString(Qt::ISODate)) << std::endl;
std::cout << "Departure day: " << qPrintable(ticket.type1DepartureDay().toString(Qt::ISODate)) << std::endl;
}
}
int main(int argc, char **argv)
{
QCoreApplication::setApplicationName(QStringLiteral("ticket-barcode-dump"));
QCoreApplication::setApplicationVersion(QStringLiteral(KITINERARY_VERSION_STRING));
QCoreApplication::setOrganizationDomain(QStringLiteral("kde.org"));
QCoreApplication::setOrganizationName(QStringLiteral("KDE"));
QCoreApplication app(argc, argv);
QCommandLineParser parser;
parser.setApplicationDescription(QStringLiteral("Decode ticket barcode content."));
parser.addHelpOption();
parser.addVersionOption();
parser.addPositionalArgument(QStringLiteral("input"), QStringLiteral("File to read data from, omit for using stdin."));
parser.process(app);
// TODO stdin support
if (parser.positionalArguments().isEmpty()) {
parser.showHelp(1);
}
QFile file(parser.positionalArguments().at(0));
if (!file.open(QFile::ReadOnly)) {
std::cerr << qPrintable(file.errorString()) << std::endl;
return 1;
}
const auto data = file.readAll();
if (IataBcbpParser::maybeIataBcbp(QString::fromLatin1(data))) {
std::cout << "IATA Barcoded Boarding Pass" << std::endl;
// TODO
} else if (SSBTicket::maybeSSB(data)) {
std::cout << "ERA SSB Ticket" << std::endl;
dumpSsbTicket(data);
} else if (Uic9183Parser::maybeUic9183(data)) {
std::cout << "UIC 918.3 Container" << std::endl;
// TODO
} else if (VdvTicketParser::maybeVdvTicket(data)) {
std::cout << "VDV Ticket" << std::endl;
// TODO
} else {
std::cout << "Unknown content" << std::endl;
return 1;
}
return 0;
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment