diff --git a/autotests/bcbpdata/missing-eticket-indicator.json b/autotests/bcbpdata/missing-eticket-indicator.json new file mode 100644 index 0000000..6d87d7a --- /dev/null +++ b/autotests/bcbpdata/missing-eticket-indicator.json @@ -0,0 +1,29 @@ +[ + { + "@context": "http://schema.org", + "@type": "FlightReservation", + "airplaneSeat": "12C", + "reservationFor": { + "@type": "Flight", + "airline": { + "@type": "Airline", + "iataCode": "EW" + }, + "arrivalAirport": { + "@type": "Airport", + "iataCode": "TXL" + }, + "departureAirport": { + "@type": "Airport", + "iataCode": "BRU" + }, + "departureDay": "2019-02-04", + "flightNumber": "8103" + }, + "reservationNumber": "XXX007", + "underName": { + "@type": "Person", + "name": "DOE/JOHN" + } + } +] diff --git a/autotests/bcbpparsertest.cpp b/autotests/bcbpparsertest.cpp index 36db406..b0c8bb7 100644 --- a/autotests/bcbpparsertest.cpp +++ b/autotests/bcbpparsertest.cpp @@ -1,93 +1,96 @@ /* Copyright (C) 2018 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 "iatabcbpparser.h" #include #include #include #include #include #include #include #include #include using namespace KItinerary; class BcbpParserTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase() { qRegisterMetaType(); qRegisterMetaType(); } void testParserValid_data() { QTest::addColumn("message"); QTest::addColumn("refFile"); // example data from IATA resolution 792 version 5 Attachment B (with security sections shortended or omitted) QTest::newRow("single leg, mandatory only") << QStringLiteral("M1DESMARAIS/LUC EABC123 YULFRAAC 0834 326J001A0025 100") << QStringLiteral("iata-resolution792-example1.json"); QTest::newRow("single leg, all fields") << QStringLiteral("M1DESMARAIS/LUC EAB12C3 YULFRAAC 0834 326J003A0027 167>5321WW1325BAC 0014123456002001412346700100141234789012A0141234567890 1AC AC 1234567890123 4PCYLX58Z^108ABCDEFGH") << QStringLiteral("iata-resolution792-example2.json"); QTest::newRow("single leg, partial") << QStringLiteral("M1GRANDMAIRE/MELANIE EABC123 GVACDGAF 0123 339C002F0025 130>5002A0571234567890 AF AF 1234567890123456 Y^18ABCDEFGH") << QStringLiteral("iata-resolution792-example3.json"); QTest::newRow("multi leg, all fields") << QStringLiteral("M2DESMARAIS/LUC EAB12C3 YULFRAAC 0834 326J003A0027 167>5321WW1325BAC 0014123456002001412346700100141234789012A0141234567890 1AC AC 1234567890123 4PCYLX58ZDEF456 FRAGVALH 3664 327C012C0002 12E2A0140987654321 1AC AC 1234567890123 3PCNWQ^108ABCDEFGH") << QStringLiteral("iata-resolution792-example4.json"); QTest::newRow("multi leg, partial") << QStringLiteral("M2GRANDMAIRE/MELANIE EABC123 GVACDGAF 0123 339C002F0025 130>5002A0571234567890 AF AF 1234567890123456 YDEF456 CDGDTWNW 0049 339F001A0002 12C2A012098765432101 2PC ^18ABCDEFGH") << QStringLiteral("iata-resolution792-example5.json"); + + // EW misses the 'E' eticket marker (BCBP item 253) + QTest::newRow("missing eticket indicator") << QStringLiteral("M1DOE/JOHN XXX007 BRUTXLEW 8103 035Y012C0030 147>1181W 8033BEW 0000000000000291040000000000 0 LH 123456789012345 ") << QStringLiteral("missing-eticket-indicator.json"); } void testParserValid() { QFETCH(QString, message); QFETCH(QString, refFile); QFile f(QStringLiteral(SOURCE_DIR "/bcbpdata/") + refFile); QVERIFY(f.open(QFile::ReadOnly)); const auto refArray = QJsonDocument::fromJson(f.readAll()).array(); QVERIFY(!refArray.isEmpty()); const auto res = IataBcbpParser::parse(message, QDate(2018, 4, 2)); const auto resJson = JsonLdDocument::toJson(res); if (refArray != resJson) { qWarning().noquote() << QJsonDocument(resJson).toJson(); } QCOMPARE(resJson, refArray); } void testParserInvalid_data() { QTest::addColumn("message"); QTest::newRow("empty") << QString(); QTest::newRow("too short") << QStringLiteral("M1DESMARAIS/LUC "); QTest::newRow("wrong leg count") << QStringLiteral("M2DESMARAIS/LUC EABC123 YULFRAAC 0834 326J001A0025 100"); } void testParserInvalid() { QFETCH(QString, message); const auto res = IataBcbpParser::parse(message); QVERIFY(res.isEmpty()); } }; QTEST_APPLESS_MAIN(BcbpParserTest) #include "bcbpparsertest.moc" diff --git a/src/iatabcbpparser.cpp b/src/iatabcbpparser.cpp index 7a753dc..a1dff8a 100644 --- a/src/iatabcbpparser.cpp +++ b/src/iatabcbpparser.cpp @@ -1,166 +1,165 @@ /* Copyright (C) 2018 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 "iatabcbpparser.h" #include "logging.h" #include #include #include #include #include #include #include using namespace KItinerary; namespace KItinerary { enum Constants { UniqueMandatorySize = 23, RepeastedMandatorySize = 37, FormatCode = 'M', - ElectronicTicketIndicator = 'E', BeginOfVersionNumber = '>' }; static QStringRef stripLeadingZeros(const QStringRef &s) { const auto it = std::find_if(s.begin(), s.end(), [](const QChar &c) { return c != QLatin1Char('0'); }); const auto d = std::distance(s.begin(), it); return s.mid(d); } static int readHexValue(const QStringRef &s, int width) { return s.mid(0, width).toInt(nullptr, 16); } static int parseRepeatedMandatorySection(const QStringRef& msg, const QDate &issueDate, FlightReservation& res) { res.setReservationNumber(msg.mid(0, 7).trimmed().toString()); Flight flight; Airport airport; airport.setIataCode(msg.mid(7, 3).toString()); flight.setDepartureAirport(airport); airport.setIataCode(msg.mid(10, 3).toString()); flight.setArrivalAirport(airport); Airline airline; airline.setIataCode(msg.mid(13, 3).trimmed().toString()); flight.setAirline(airline); flight.setFlightNumber(stripLeadingZeros(msg.mid(16, 5).trimmed()).toString()); // 3x Date of flight, as days since Jan 1st if (issueDate.isValid()) { const auto days = msg.mid(21, 3).toInt() - 1; QDate date(issueDate.year(), 1, 1); date = date.addDays(days); if (date >= issueDate) { flight.setDepartureDay(date); } else { flight.setDepartureDay(QDate(issueDate.year() + 1, 1, 1).addDays(days)); } } res.setReservationFor(QVariant::fromValue(flight)); // 1x Compartment code res.setAirplaneSeat(stripLeadingZeros(msg.mid(25, 4)).toString()); // 5x Checkin sequence number // 1x Passenger status // field size of conditional section + airline use section return readHexValue(msg.mid(35), 2); } } QVector IataBcbpParser::parse(const QString& message, const QDate &issueDate) { if (message.size() < (UniqueMandatorySize + RepeastedMandatorySize)) { qCWarning(Log) << "IATA BCBP code too short"; return {}; } - if (message.at(0) != QLatin1Char(FormatCode) || !message.at(1).isDigit() || message.at(22) != QLatin1Char(ElectronicTicketIndicator)) { + if (message.at(0) != QLatin1Char(FormatCode) || !message.at(1).isDigit()) { qCWarning(Log) << "IATA BCBP code invalid unique mandatory section format"; return {}; } // parse unique mandatory section const auto legCount = message.at(1).toLatin1() - '0'; QVector result; result.reserve(legCount); FlightReservation res1; Person person; const auto fullName = message.midRef(2, 20).trimmed(); // TODO split in family and given name person.setName(fullName.toString()); res1.setUnderName(QVariant::fromValue(person)); const auto varSize = parseRepeatedMandatorySection(message.midRef(UniqueMandatorySize), issueDate, res1); int index = UniqueMandatorySize + RepeastedMandatorySize; if (message.size() < (index + varSize)) { qCWarning(Log) << "IATA BCBP code too short for conditional section in first leg" << varSize << message.size(); return {}; } if (varSize > 0) { // parse unique conditional section if (message.at(index) != QLatin1Char(BeginOfVersionNumber)) { qCWarning(Log) << "IATA BCBP unique conditional section has invalid format"; return {}; } // 1x version number // 2x field size // baggage tags, information about boarding pass source, not really interesting for us // skip repeated conditional section, containing mainly bonus program data // skip for airline use section index += varSize; } result.push_back(QVariant::fromValue(res1)); // all following legs only contain repeated sections, copy content from the unique ones from the first leg for (int i = 1; i < legCount; ++i) { if (message.size() < (index + RepeastedMandatorySize)) { qCWarning(Log) << "IATA BCBP repeated mandatory section too short" << i; return {}; } FlightReservation res = res1; const auto varSize = parseRepeatedMandatorySection(message.midRef(index), issueDate, res); index += RepeastedMandatorySize; if (message.size() < (index + varSize)) { qCWarning(Log) << "IATA BCBP repeated conditional section too short" << i; return {}; } // skip repeated conditional section // skip for airline use section index += varSize; result.push_back(QVariant::fromValue(res)); } // optional security section at the end, not interesting for us return result; }