Commit 101ace26 authored by Volker Krause's avatar Volker Krause
Browse files

Add new IATA BCBP types

This will replace the existing IATA BCBP parser methods and separate basic
data types and extractor logic like we do for all other ticket types now.
This will allow to have IATA BCBPs in the document node tree and thus
filter on them and expose them to JS scripts.
parent fc2ae06b
......@@ -5,6 +5,9 @@
*/
#include "iatabcbpparser.h"
#include "../lib/iata/iatabcbp.h"
#include <KItinerary/JsonLdDocument>
#include <KItinerary/Organization>
#include <KItinerary/Place>
......@@ -69,6 +72,9 @@ private Q_SLOTS:
qWarning().noquote() << QJsonDocument(resJson).toJson();
}
QCOMPARE(resJson, refArray);
IataBcbp bcbp(message);
QVERIFY(bcbp.isValid());
}
void testParserInvalid_data()
......@@ -90,6 +96,9 @@ private Q_SLOTS:
qDebug().noquote() << QJsonDocument(JsonLdDocument::toJson(res)).toJson();
}
QVERIFY(res.isEmpty());
IataBcbp bcbp(message);
QVERIFY(!bcbp.isValid());
}
};
......
......@@ -40,6 +40,9 @@ set(kitinerary_lib_srcs
extractors/iatabcbpextractor.cpp
iata/iatabcbp.cpp
iata/iatabcbpsections.cpp
jsapi/barcode.cpp
jsapi/bitarray.cpp
jsapi/context.cpp
......
/*
SPDX-FileCopyrightText: 2021 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "iatabcbp.h"
#include "iatabcbpconstants_p.h"
#include "logging.h"
#include <QScopeGuard>
#include <cctype>
using namespace KItinerary;
using namespace KItinerary::IataBcbpConstants;
IataBcbp::IataBcbp() = default;
IataBcbp::IataBcbp(const QString& data)
{
if (data.size() < MinimumViableSize || data[0] != QLatin1Char('M') || !data[1].isDigit()) {
return;
}
const auto trimmed = QStringView(data).trimmed(); // tolerance against e.g. trailing newlines
if (std::any_of(trimmed.begin(), trimmed.end(), [](QChar c) { return c.row() != 0 || !c.isPrint(); })) {
return;
}
m_data = data;
auto resetOnInvalid = qScopeGuard([this] { m_data.clear(); });
if (!uniqueMandatorySection().isValid()) {
return;
}
if (hasUniqueConditionalSection() && !uniqueConditionalSection().isValid()) {
return;
}
const auto legCount = uniqueMandatorySection().numberOfLegs();
int offset = UniqueMandatorySize;
for (int i = 0; i < legCount; ++i) {
if (offset > m_data.size()) {
return;
}
auto rms = IataBcbpRepeatedMandatorySection(QStringView(m_data).mid(offset));
if (!rms.isValid()) {
return;
}
offset += rms.variableFieldSize() + RepeatedMandatorySize;
}
resetOnInvalid.dismiss();
}
IataBcbp::~IataBcbp() = default;
bool IataBcbp::isValid() const
{
return !m_data.isEmpty();
}
IataBcbpUniqueMandatorySection IataBcbp::uniqueMandatorySection() const
{
return IataBcbpUniqueMandatorySection(QStringView(m_data).left(UniqueMandatorySize));
}
bool IataBcbp::hasUniqueConditionalSection() const
{
return (m_data.size() > (UniqueMandatorySize + RepeatedMandatorySize)) && (m_data.at(UniqueMandatorySize + RepeatedMandatorySize) == QLatin1Char('>'));
}
IataBcbpUniqueConditionalSection IataBcbp::uniqueConditionalSection() const
{
return IataBcbpUniqueConditionalSection(QStringView(m_data).mid(UniqueMandatorySize + RepeatedMandatorySize));
}
IataBcbpRepeatedMandatorySection IataBcbp::repeatedMandatorySection(int leg) const
{
int offset = UniqueMandatorySize;
for (auto i = 0; i < leg; ++i) {
offset += RepeatedMandatorySize + IataBcbpRepeatedMandatorySection(QStringView(m_data).mid(offset)).variableFieldSize();
}
return IataBcbpRepeatedMandatorySection(QStringView(m_data).mid(offset, RepeatedMandatorySize));
}
IataBcbpRepeatedConditionalSection IataBcbp::repeatedConditionalSection(int leg) const
{
int offset = UniqueMandatorySize;
if (leg == 0 && hasUniqueConditionalSection()) {
offset += uniqueConditionalSection().fieldSize() + MinimumUniqueConditionalSize;
}
for (auto i = 0; i < leg; ++i) {
offset += RepeatedMandatorySize + IataBcbpRepeatedMandatorySection(QStringView(m_data).mid(offset)).variableFieldSize();
}
return IataBcbpRepeatedConditionalSection(QStringView(m_data).mid(offset + RepeatedMandatorySize));
}
QString IataBcbp::airlineUseSection(int leg) const
{
int offset = UniqueMandatorySize;
for (auto i = 0; i < leg; ++i) {
offset += RepeatedMandatorySize + IataBcbpRepeatedMandatorySection(QStringView(m_data).mid(offset)).variableFieldSize();
}
auto length = IataBcbpRepeatedMandatorySection(QStringView(m_data).mid(offset)).variableFieldSize();
if (leg == 0 && hasUniqueConditionalSection()) {
const auto s = uniqueConditionalSection().fieldSize() + MinimumUniqueConditionalSize;
offset += uniqueConditionalSection().fieldSize() + MinimumUniqueConditionalSize;
length -= s;
}
if (leg == 0 && !hasUniqueConditionalSection()) { // Easyjet special case that has a airline use section right after the mandatory block
return m_data.mid(offset + RepeatedMandatorySize, length);
}
const auto offset2 = IataBcbpRepeatedConditionalSection(QStringView(m_data).mid(offset + RepeatedMandatorySize)).conditionalFieldSize() + 2 + RepeatedMandatorySize;
return m_data.mid(offset + offset2, length - offset2 + RepeatedMandatorySize);
}
bool IataBcbp::hasSecuritySection() const
{
int offset = UniqueMandatorySize;
for (auto i = 0; i < uniqueMandatorySection().numberOfLegs(); ++i) {
offset += RepeatedMandatorySize + IataBcbpRepeatedMandatorySection(QStringView(m_data).mid(offset)).variableFieldSize();
}
return offset < m_data.size() && m_data[offset] == QLatin1Char('^');
}
IataBcbpSecuritySection IataBcbp::securitySection() const
{
int offset = UniqueMandatorySize;
for (auto i = 0; i < uniqueMandatorySection().numberOfLegs(); ++i) {
offset += RepeatedMandatorySize + IataBcbpRepeatedMandatorySection(QStringView(m_data).mid(offset)).variableFieldSize();
}
return IataBcbpSecuritySection(m_data.mid(offset));
}
QString IataBcbp::rawData() const
{
return m_data;
}
bool IataBcbp::maybeIataBcbp(const QByteArray &data)
{
return data.size() >= MinimumViableSize && data[0] == 'M' && std::isdigit(data[1]);
}
bool IataBcbp::maybeIataBcbp(const QString &data)
{
return data.size() >= MinimumViableSize && data[0] == QLatin1Char('M') && data[1].isDigit();
}
/*
SPDX-FileCopyrightText: 2021 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KITINERARY_IATABCBP_H
#define KITINERARY_IATABCBP_H
#include "kitinerary_export.h"
#include "iatabcbpsections.h"
#include <QMetaType>
#include <QString>
namespace KItinerary {
/**
* A IATA BarCoded Boarding Pass (BCBP)
*/
class KITINERARY_EXPORT IataBcbp
{
Q_GADGET
Q_PROPERTY(KItinerary::IataBcbpUniqueMandatorySection uniqueMandatorySection READ uniqueMandatorySection)
Q_PROPERTY(KItinerary::IataBcbpUniqueConditionalSection uniqueConditionalSection READ uniqueConditionalSection)
Q_PROPERTY(KItinerary::IataBcbpSecuritySection securitySection READ securitySection)
Q_PROPERTY(QString rawData READ rawData STORED false)
public:
IataBcbp();
explicit IataBcbp(const QString &data);
~IataBcbp();
bool isValid() const;
IataBcbpUniqueMandatorySection uniqueMandatorySection() const;
bool hasUniqueConditionalSection() const;
IataBcbpUniqueConditionalSection uniqueConditionalSection() const;
/** Mandatory section of @p leg. */
Q_INVOKABLE KItinerary::IataBcbpRepeatedMandatorySection repeatedMandatorySection(int leg) const;
/** Conditional (optional) section of @p leg. */
Q_INVOKABLE KItinerary::IataBcbpRepeatedConditionalSection repeatedConditionalSection(int leg) const;
/** Airline use (non-standard/vendor specific) section of @p leg. */
Q_INVOKABLE QString airlineUseSection(int leg) const;
bool hasSecuritySection() const;
IataBcbpSecuritySection securitySection() const;
/** Raw data, for generating barcodes out of this again. */
QString rawData() const;
/** Fast checks whether this might be an IATA BCBP. */
static bool maybeIataBcbp(const QByteArray &data);
static bool maybeIataBcbp(const QString &data);
private:
QString m_data;
};
}
Q_DECLARE_METATYPE(KItinerary::IataBcbp)
#endif // KITINERARY_IATABCBP_H
/*
SPDX-FileCopyrightText: 2021 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KITINERARY_IATABCBPCONSTANTS_H
#define KITINERARY_IATABCBPCONSTANTS_H
namespace KItinerary {
namespace IataBcbpConstants {
enum {
UniqueMandatorySize = 23,
RepeatedMandatorySize = 37,
RepeatedMandatoryMinimalViableSize = 24, // pre-checkin data, technically incomplete, but we can make use of this nevertheless
MinimumViableSize = UniqueMandatorySize + RepeatedMandatoryMinimalViableSize,
MinimumUniqueConditionalSize = 4,
MinimumSecuritySectionSize = 4,
};
}
}
#endif
/*
SPDX-FileCopyrightText: 2021 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "iatabcbpsections.h"
#include "iatabcbpconstants_p.h"
#include "logging.h"
using namespace KItinerary;
using namespace KItinerary::IataBcbpConstants;
QString IataBcbpSectionBase::readString(int offset, int length) const
{
if (m_data.size() >= offset + length) {
return m_data.mid(offset, length).trimmed().toString();
}
return {};
}
int IataBcbpSectionBase::readNumericValue(int offset, int length, int base) const
{
if (m_data.size() >= offset + length) {
return m_data.mid(offset, length).toInt(nullptr, base);
}
return 0;
}
IataBcbpUniqueMandatorySection::IataBcbpUniqueMandatorySection(QStringView data)
{
m_data = data;
}
bool IataBcbpUniqueMandatorySection::isValid() const
{
const auto legCount = numberOfLegs();
return legCount >= 1 && legCount <= 4;
}
IataBcbpUniqueConditionalSection::IataBcbpUniqueConditionalSection(QStringView data)
{
if (data.size() < MinimumUniqueConditionalSize) {
return;
}
m_data = data;
m_data = data.left(MinimumUniqueConditionalSize + fieldSize());
}
bool IataBcbpUniqueConditionalSection::isValid() const
{
if (m_data.size() >= 11) {
// issue date
if (std::any_of(m_data.begin() + 8, m_data.begin() + 11, [](auto c) { return !c.isDigit() && c != QLatin1Char(' '); }) || dayOfIssue() > 366) {
return false;
}
}
return true;
}
QDate IataBcbpUniqueConditionalSection::dateOfIssue(const QDate &contextDate) const
{
const auto day = dayOfIssue() - 1;
if (m_data.size() < 11 || day < 0) {
return {};
}
const auto year = contextDate.year() - contextDate.year() % 10 + yearOfIssue();
const auto d = QDate(year, 1, 1).addDays(day);
// TODO shouldn't this rather be d > contextDate?
if (year > contextDate.year()) {
return QDate(year - 10, 1, 1).addDays(day);
}
return d;
}
IataBcbpRepeatedMandatorySection::IataBcbpRepeatedMandatorySection(QStringView data)
{
m_data = data;
}
static bool isValidAirportCode(QStringView s)
{
return std::all_of(s.begin(), s.end(), [](const QChar c) { return c.isLetter() && c.isUpper(); });
}
bool IataBcbpRepeatedMandatorySection::isValid() const
{
if (m_data.size() < RepeatedMandatoryMinimalViableSize) {
return false;
}
return isValidAirportCode(m_data.mid(7, 3))
&& isValidAirportCode(m_data.mid(10, 3))
&& std::all_of(m_data.begin() + 21, m_data.begin() + 24, [](auto c) { return c.isDigit() || c == QLatin1Char(' '); })
&& dayOfFlight() <= 366;
}
QDate IataBcbpRepeatedMandatorySection::dateOfFlight(const QDate& contextDate) const
{
const auto day = dayOfFlight() - 1;
if (day < 0) {
return {}; // no set
}
const auto d = QDate(contextDate.year(), 1, 1).addDays(day);
if (d < contextDate) {
return QDate(d.year() + 1, 1, 1).addDays(day);
}
return d;
}
IataBcbpRepeatedConditionalSection::IataBcbpRepeatedConditionalSection(QStringView data)
{
if (data.size() < 2) {
return;
}
m_data = data;
m_data = data.left(conditionalFieldSize() + 2);
}
IataBcbpSecuritySection::IataBcbpSecuritySection(QStringView data)
{
if (data.size() < MinimumSecuritySectionSize) {
return;
}
m_data = data;
m_data = data.left(size() + MinimumSecuritySectionSize);
}
#include "moc_iatabcbpsections.cpp"
/*
SPDX-FileCopyrightText: 2021 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KITINERARY_IATABCBPSECTIONS_H
#define KITINERARY_IATABCBPSECTIONS_H
#include "kitinerary_export.h"
#include <QDate>
#include <QMetaType>
namespace KItinerary {
/** @internal Base class for IATA BCBP sections. */
class KITINERARY_EXPORT IataBcbpSectionBase
{
protected:
QString readString(int offset, int length) const;
int readNumericValue(int offset, int length, int base) const;
QStringView m_data;
};
#define IATA_STR_PROPERTY(Name, Start, Length) \
public: \
inline QString Name() const { return readString(Start, Length); } \
Q_PROPERTY(QString Name READ Name)
#define IATA_NUM_PROPERTY(Name, Start, Length) \
public: \
inline int Name() const { return readNumericValue(Start, Length, 10); } \
Q_PROPERTY(int Name READ Name)
#define IATA_HEX_PROPERTY(Name, Start, Length) \
public: \
inline int Name() const { return readNumericValue(Start, Length, 16); } \
Q_PROPERTY(int Name READ Name)
/** Unique mandatory section of an IATA BCBP. */
class KITINERARY_EXPORT IataBcbpUniqueMandatorySection : protected IataBcbpSectionBase
{
Q_GADGET
IATA_STR_PROPERTY(formatCode, 0, 1)
IATA_NUM_PROPERTY(numberOfLegs, 1, 1)
IATA_STR_PROPERTY(passengerName, 2, 20)
IATA_STR_PROPERTY(electronicTicketIndicator, 22, 1)
public:
IataBcbpUniqueMandatorySection() = default;
explicit IataBcbpUniqueMandatorySection(QStringView data);
bool isValid() const;
};
/** Unique conditional (optional) section of an IATA BCBP. */
class KITINERARY_EXPORT IataBcbpUniqueConditionalSection : protected IataBcbpSectionBase
{
Q_GADGET
IATA_NUM_PROPERTY(version, 1, 1)
IATA_HEX_PROPERTY(fieldSize, 2, 2)
IATA_STR_PROPERTY(passengerDescription, 4, 1)
IATA_STR_PROPERTY(sourceOfCheckin, 5, 1)
IATA_STR_PROPERTY(sourceOfBoardingPassIssuance, 6, 1)
IATA_NUM_PROPERTY(yearOfIssue, 7, 1)
IATA_NUM_PROPERTY(dayOfIssue, 8, 3)
IATA_STR_PROPERTY(documentType, 11, 1)
IATA_STR_PROPERTY(airlineDesignatorOfBoardingPassIssuer, 12, 3)
IATA_STR_PROPERTY(baggageTagLicensePlateNumber1, 15, 13)
IATA_STR_PROPERTY(baggageTagLicensePlateNumber2, 28, 13)
IATA_STR_PROPERTY(baggageTagLicensePlateNumber3, 41, 13)
public:
IataBcbpUniqueConditionalSection() = default;
explicit IataBcbpUniqueConditionalSection(QStringView data);
bool isValid() const;
Q_INVOKABLE QDate dateOfIssue(const QDate &contextDate = QDate::currentDate()) const;
};
/** Repeated mandatory sections of an IATA BCBP, occurs once per leg. */
class KITINERARY_EXPORT IataBcbpRepeatedMandatorySection : protected IataBcbpSectionBase
{
Q_GADGET
IATA_STR_PROPERTY(operatingCarrierPNRCode, 0, 7)
IATA_STR_PROPERTY(fromCityAirportCode, 7, 3)
IATA_STR_PROPERTY(toCityAirportCode, 10, 3)
IATA_STR_PROPERTY(operatingCarrierDesignator, 13, 3)
IATA_STR_PROPERTY(flightNumber, 16, 5)
IATA_NUM_PROPERTY(dayOfFlight, 21, 3)
IATA_STR_PROPERTY(compartmentCode, 24, 1)
IATA_STR_PROPERTY(seatNumber, 25, 4)
IATA_STR_PROPERTY(checkinSequenceNumber, 29, 5)
IATA_STR_PROPERTY(passengerStatus, 34, 1)
IATA_HEX_PROPERTY(variableFieldSize, 35, 2)
public:
IataBcbpRepeatedMandatorySection() = default;
explicit IataBcbpRepeatedMandatorySection(QStringView data);
bool isValid() const;
/** Date of the flight.
* @param contextDate A date before the flight to determine
* the full year which is not specified in the pass itself.
*/
Q_INVOKABLE QDate dateOfFlight(const QDate &contextDate = QDate::currentDate()) const;
};
/** Conditional (optional) sections of an IATA BCBP, occurs once per leg. */
class KITINERARY_EXPORT IataBcbpRepeatedConditionalSection : protected IataBcbpSectionBase
{
Q_GADGET
IATA_HEX_PROPERTY(conditionalFieldSize, 0, 2)
IATA_STR_PROPERTY(airlineNumericCode, 2, 3)
IATA_STR_PROPERTY(documentNumber, 5, 10)
IATA_STR_PROPERTY(selecteeIndicator, 15, 1)
IATA_STR_PROPERTY(internationalDocumentVerification, 16, 1)
IATA_STR_PROPERTY(marketingCarrierDesignator, 17, 3)
IATA_STR_PROPERTY(frequentFlyerAirlineDesignator, 20, 3)
IATA_STR_PROPERTY(frequenFlyerNumber, 23, 16)
IATA_STR_PROPERTY(idAdIndicator, 39, 1)
IATA_STR_PROPERTY(freeBaggageAllowance, 40, 3)
IATA_STR_PROPERTY(fastTrack, 43, 1)
public:
IataBcbpRepeatedConditionalSection() = default;
explicit IataBcbpRepeatedConditionalSection(QStringView data);
};
/** Security section of an IATA BCBP. */
class KITINERARY_EXPORT IataBcbpSecuritySection : protected IataBcbpSectionBase
{
Q_GADGET
IATA_STR_PROPERTY(type, 1, 1)
IATA_HEX_PROPERTY(size, 2, 2)
IATA_STR_PROPERTY(securityData, 4, size())
public:
IataBcbpSecuritySection() = default;
explicit IataBcbpSecuritySection(QStringView data);
};
#undef IATA_STR_PROPERTY
#undef IATA_HEX_PROPERTY
}
Q_DECLARE_METATYPE(KItinerary::IataBcbpUniqueMandatorySection)
Q_DECLARE_METATYPE(KItinerary::IataBcbpUniqueConditionalSection)
Q_DECLARE_METATYPE(KItinerary::IataBcbpRepeatedMandatorySection)
Q_DECLARE_METATYPE(KItinerary::IataBcbpRepeatedConditionalSection)
#endif // KITINERARY_IATABCBPSECTIONS_H
......@@ -7,6 +7,7 @@
#include "../lib/era/ssbv1ticket.h"
#include "../lib/era/ssbv2ticket.h"
#include "../lib/era/ssbv3ticket.h"
#include "../lib/iata/iatabcbp.h"
#include "../lib/uic9183/uic9183head.h"
#include "../lib/uic9183/uic9183header.h"
#include "../lib/uic9183/vendor0080vublockdata.h"
......@@ -15,7 +16,6 @@
#include <kitinerary_version.h>
#include <KItinerary/IataBcbpParser>
#include <KItinerary/Uic9183Block>
#include <KItinerary/Uic9183Parser>
#include <KItinerary/Uic9183TicketLayout>
......@@ -233,6 +233,38 @@ static void dumpVdv(const QByteArray &data)
std::cout << " version: " << ticket.trailer()->version << std::endl;
}
void dumpIataBcbp(const QString &data, const QDate &contextDate)
{
IataBcbp ticket(data);
if (!ticket.isValid()) {
std::cout << "invalid format" << std::endl;
return;
}
const auto ums = ticket.uniqueMandatorySection();
dumpGadget(&ums);
const auto ucs = ticket.uniqueConditionalSection();
dumpGadget(&ucs);
const auto issueDate = ucs.dateOfIssue(contextDate);
std::cout << "Date of issue: " << qPrintable(issueDate.toString(Qt::ISODate)) << std::endl;
for (auto i = 0; i < ums.numberOfLegs(); ++i) {
std::cout << "Leg " << (i + 1) << std::endl;
const auto rms = ticket.repeatedMandatorySection(i);
dumpGadget(&rms, " ");
const auto rcs = ticket.repeatedConditionalSection(i);
dumpGadget(&rcs, " ");
std::cout << " Airline use section: " << qPrintable(ticket.airlineUseSection(i)) << std::endl;
std::cout << " Date of flight: " << qPrintable(rms.dateOfFlight(issueDate.isValid() ? issueDate : contextDate).toString(Qt::ISODate)) << std::endl;
}