diff --git a/autotests/bcbpdata/iata-resolution792-example2.json b/autotests/bcbpdata/iata-resolution792-example2.json index ad5a308..c6f6d99 100644 --- a/autotests/bcbpdata/iata-resolution792-example2.json +++ b/autotests/bcbpdata/iata-resolution792-example2.json @@ -1,29 +1,29 @@ [ { "@context": "http://schema.org", "@type": "FlightReservation", "airplaneSeat": "3A", "reservationFor": { "@type": "Flight", "airline": { "@type": "Airline", "iataCode": "AC" }, "arrivalAirport": { "@type": "Airport", "iataCode": "FRA" }, "departureAirport": { "@type": "Airport", "iataCode": "YUL" }, - "departureDay": "2018-11-22", + "departureDay": "2011-11-22", "flightNumber": "834" }, "reservationNumber": "AB12C3", "underName": { "@type": "Person", "name": "DESMARAIS/LUC" } } ] diff --git a/autotests/bcbpdata/iata-resolution792-example4.json b/autotests/bcbpdata/iata-resolution792-example4.json index b6bd319..cc5c318 100644 --- a/autotests/bcbpdata/iata-resolution792-example4.json +++ b/autotests/bcbpdata/iata-resolution792-example4.json @@ -1,56 +1,56 @@ [ { "@context": "http://schema.org", "@type": "FlightReservation", "airplaneSeat": "3A", "reservationFor": { "@type": "Flight", "airline": { "@type": "Airline", "iataCode": "AC" }, "arrivalAirport": { "@type": "Airport", "iataCode": "FRA" }, "departureAirport": { "@type": "Airport", "iataCode": "YUL" }, - "departureDay": "2018-11-22", + "departureDay": "2011-11-22", "flightNumber": "834" }, "reservationNumber": "AB12C3", "underName": { "@type": "Person", "name": "DESMARAIS/LUC" } }, { "@context": "http://schema.org", "@type": "FlightReservation", "airplaneSeat": "12C", "reservationFor": { "@type": "Flight", "airline": { "@type": "Airline", "iataCode": "LH" }, "arrivalAirport": { "@type": "Airport", "iataCode": "GVA" }, "departureAirport": { "@type": "Airport", "iataCode": "FRA" }, - "departureDay": "2018-11-23", + "departureDay": "2011-11-23", "flightNumber": "3664" }, "reservationNumber": "DEF456", "underName": { "@type": "Person", "name": "DESMARAIS/LUC" } } ] diff --git a/autotests/bcbpdata/missing-eticket-indicator.json b/autotests/bcbpdata/issue-date.json similarity index 73% copy from autotests/bcbpdata/missing-eticket-indicator.json copy to autotests/bcbpdata/issue-date.json index 6d87d7a..a4b726a 100644 --- a/autotests/bcbpdata/missing-eticket-indicator.json +++ b/autotests/bcbpdata/issue-date.json @@ -1,29 +1,29 @@ [ { "@context": "http://schema.org", "@type": "FlightReservation", - "airplaneSeat": "12C", + "airplaneSeat": "23D", "reservationFor": { "@type": "Flight", "airline": { "@type": "Airline", - "iataCode": "EW" + "iataCode": "SN" }, "arrivalAirport": { "@type": "Airport", - "iataCode": "TXL" + "iataCode": "BRU" }, "departureAirport": { "@type": "Airport", - "iataCode": "BRU" + "iataCode": "TXL" }, - "departureDay": "2019-02-04", - "flightNumber": "8103" + "departureDay": "2017-02-03", + "flightNumber": "2588" }, "reservationNumber": "XXX007", "underName": { - "@type": "Person", - "name": "DOE/JOHN" + "@type": "Person", + "name": "DOE/JOHN" } } ] diff --git a/autotests/bcbpdata/missing-eticket-indicator.json b/autotests/bcbpdata/missing-eticket-indicator.json index 6d87d7a..b6f6648 100644 --- a/autotests/bcbpdata/missing-eticket-indicator.json +++ b/autotests/bcbpdata/missing-eticket-indicator.json @@ -1,29 +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", + "departureDay": "2018-02-04", "flightNumber": "8103" }, "reservationNumber": "XXX007", "underName": { "@type": "Person", "name": "DOE/JOHN" } } ] diff --git a/autotests/bcbpparsertest.cpp b/autotests/bcbpparsertest.cpp index b0c8bb7..17c21c2 100644 --- a/autotests/bcbpparsertest.cpp +++ b/autotests/bcbpparsertest.cpp @@ -1,96 +1,99 @@ /* 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"); + + // 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"); } 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 05cc56e..3c846c4 100644 --- a/src/iatabcbpparser.cpp +++ b/src/iatabcbpparser.cpp @@ -1,165 +1,211 @@ /* 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, const QDate &issueDate, FlightReservation& res) +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 - 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)); - } - } + // 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()); // 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) +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(); // TODO split in family and given name person.setName(fullName.toString()); res1.setUnderName(person); - const auto varSize = parseRepeatedMandatorySection(message.midRef(UniqueMandatorySize), issueDate, res1); + 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 - // baggage tags, information about boarding pass source, not really interesting for us + // 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); + return {}; + } + + auto currentYear = externalIssueDate.year() - externalIssueDate.year() % 10 + year; + if (currentYear > externalIssueDate.year()) { + currentYear -= 10; + } + issueDate = QDate(currentYear, 1, 1).addDays(days); + } + + // 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 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), issueDate, res); + 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; }