diff --git a/autotests/bcbpdata/easyjet.json b/autotests/bcbpdata/easyjet.json new file mode 100644 index 0000000..707ff0a --- /dev/null +++ b/autotests/bcbpdata/easyjet.json @@ -0,0 +1,30 @@ +[ + { + "@context": "http://schema.org", + "@type": "FlightReservation", + "airplaneSeat": "3C", + "reservationFor": { + "@type": "Flight", + "airline": { + "@type": "Airline", + "iataCode": "EZY" + }, + "arrivalAirport": { + "@type": "Airport", + "iataCode": "LGW" + }, + "departureAirport": { + "@type": "Airport", + "iataCode": "MRS" + }, + "departureDay": "2018-04-09", + "flightNumber": "8724" + }, + "reservationNumber": "ABCDEFG", + "underName": { + "@type": "Person", + "familyName": "DOE", + "givenName": "JOHN" + } + } +] diff --git a/autotests/bcbpparsertest.cpp b/autotests/bcbpparsertest.cpp index 00adf8b..a2521e0 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 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"); // boarding pass issue date (BCBP item 22) QTest::newRow("issue date") << QStringLiteral("M1DOE/JOHN EXXX007 TXLBRUSN 2588 034Y023D0999 35D>5181WM7034BSN 2A08200000000000 SN LH 123456789012345 *30600000K0902 ") << QStringLiteral("issue-date.json"); + + // EasyJet being easy on the standard interpretation + QTest::newRow("easyjet") << QStringLiteral("M1DOE/JOHN EABCDEFGMRSLGWEZY8724 99 3C 506 10Axxxxxxxxxx") << QStringLiteral("easyjet.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 831ef24..642e79a 100644 --- a/src/iatabcbpparser.cpp +++ b/src/iatabcbpparser.cpp @@ -1,219 +1,218 @@ /* 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', 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, 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 // we don't know the year here, so use 1970, will be filled up or discarded by the caller const auto days = msg.mid(21, 3).toInt() - 1; flight.setDepartureDay(QDate(1970, 1, 1).addDays(days)); res.setReservationFor(flight); // 1x Compartment code - res.setAirplaneSeat(stripLeadingZeros(msg.mid(25, 4)).toString()); + res.setAirplaneSeat(stripLeadingZeros(msg.mid(25, 4)).trimmed().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 &externalIssueDate) { if (message.size() < (UniqueMandatorySize + RepeastedMandatorySize)) { qCWarning(Log) << "IATA BCBP code too short"; return {}; } 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(); const auto idx = fullName.indexOf(QLatin1Char('/')); if (idx > 0 && idx < fullName.size() - 1) { person.setFamilyName(fullName.left(idx).toString()); person.setGivenName(fullName.mid(idx + 1).toString()); } else { person.setName(fullName.toString()); } res1.setUnderName(person); } const auto varSize = parseRepeatedMandatorySection(message.midRef(UniqueMandatorySize), 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 {}; } auto issueDate = externalIssueDate; 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 of unique conditional section - const auto uniqCondSize = readHexValue(message.midRef(index + 2), 2); - if (uniqCondSize + 4 > varSize) { - qCWarning(Log) << "IATA BCBP unique conditional section has invalid size" << varSize << uniqCondSize; - return {}; - } - - // 1x passenger description - // 1x source of checking - // 1x source of boarding pass issuance - - // 4x date of issue of boarding pass - // this only contains the last digit of the year (sic), but we assume it to be in the past - // so this still gives us a 10 year range of correctly determined times - if (uniqCondSize >= 11 && externalIssueDate.isValid()) { - const auto year = message.at(index + 7).toLatin1() - '0'; - const auto days = message.midRef(index + 8, 3).toInt() - 1; - if (year < 0 || year > 9 || days < 0 || days > 365) { - qCWarning(Log) << "IATA BCBP invalid boarding pass issue date format" << message.midRef(index + 7, 8); + // parse unique conditional section, if there is one, otherwise we skip all of this assuming "for airline use" + if (message.at(index) == QLatin1Char(BeginOfVersionNumber)) { + // 1x version number + // 2x field size of unique conditional section + const auto uniqCondSize = readHexValue(message.midRef(index + 2), 2); + if (uniqCondSize + 4 > varSize) { + qCWarning(Log) << "IATA BCBP unique conditional section has invalid size" << varSize << uniqCondSize; return {}; } - auto currentYear = externalIssueDate.year() - externalIssueDate.year() % 10 + year; - if (currentYear > externalIssueDate.year()) { - currentYear -= 10; + // 1x passenger description + // 1x source of checking + // 1x source of boarding pass issuance + + // 4x date of issue of boarding pass + // this only contains the last digit of the year (sic), but we assume it to be in the past + // so this still gives us a 10 year range of correctly determined times + if (uniqCondSize >= 11 && externalIssueDate.isValid()) { + const auto year = message.at(index + 7).toLatin1() - '0'; + const auto days = message.midRef(index + 8, 3).toInt() - 1; + if (year < 0 || year > 9 || days < 0 || days > 365) { + qCWarning(Log) << "IATA BCBP invalid boarding pass issue date format" << message.midRef(index + 7, 8); + return {}; + } + + auto currentYear = externalIssueDate.year() - externalIssueDate.year() % 10 + year; + if (currentYear > externalIssueDate.year()) { + currentYear -= 10; + } + issueDate = QDate(currentYear, 1, 1).addDays(days); } - issueDate = QDate(currentYear, 1, 1).addDays(days); - } - // 1x document type - // 3x airline code of boarding pass issuer - // 3x 13x baggage tag numbers + // 1x document type + // 3x airline code of boarding pass issuer + // 3x 13x baggage tag numbers + + // skip repeated conditional section, containing mainly bonus program data + } - // skip repeated conditional section, containing mainly bonus program data // skip for airline use section index += varSize; } result.push_back(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), 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(res); } // optional security section at the end, not interesting for us // complete departure dates with the now (hopefully known issue date) for (auto it = result.begin(); it != result.end(); ++it) { auto res = (*it).value(); auto flight = res.reservationFor().value(); if (issueDate.isValid()) { const auto days = flight.departureDay().dayOfYear() - 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)); } } else { flight.setDepartureDay(QDate()); } res.setReservationFor(flight); *it = res; } return result; }