KItinerary

uic9183parser.cpp
1/*
2 SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "uic9183parser.h"
8#include "logging.h"
9#include "rct2ticket.h"
10#include "uic9183block.h"
11#include "uic9183flex.h"
12#include "uic9183head.h"
13#include "uic9183header.h"
14#include "uic9183ticketlayout.h"
15#include "variantvisitor_p.h"
16#include "vendor0080block.h"
17#include "vendor1154block.h"
18
19#include "era/fcbextractor_p.h"
20#include "era/fcbticket1.h"
21#include "era/fcbticket3.h"
22#include "era/fcbutil.h"
23
24#include <QDateTime>
25#include <QDebug>
26#include <QJsonDocument>
27#include <QJsonObject>
28#include <QTimeZone>
29
30#include <zlib.h>
31
32#include <cassert>
33#include <cstring>
34
35using namespace KItinerary;
36
37namespace KItinerary {
38
39class Uic9183ParserPrivate : public QSharedData
40{
41public:
42 QByteArray m_data;
43 QByteArray m_payload;
44};
45}
46
47Uic9183Parser::Uic9183Parser()
48 : d(new Uic9183ParserPrivate)
49{
50}
51
52Uic9183Parser::Uic9183Parser(const Uic9183Parser&) = default;
53Uic9183Parser::~Uic9183Parser() = default;
54Uic9183Parser& Uic9183Parser::operator=(const Uic9183Parser&) = default;
55
57{
58 return Uic9183Block(d->m_payload, 0);
59}
60
61Uic9183Block Uic9183Parser::findBlock(const char name[6]) const
62{
63 for (auto block = firstBlock(); !block.isNull(); block = block.nextBlock()) {
64 if (block.isA(name)) {
65 return block;
66 }
67 }
68 return {};
69}
70
72{
73 if (name.size() != 6 || d->m_payload.isEmpty()) {
74 return {};
75 }
76
77#define BLOCK_FROM_NAME(Type) \
78 if (name == QLatin1StringView(Type::RecordId)) { \
79 const auto block = findBlock<Type>(); \
80 return block.isValid() ? QVariant::fromValue(block) : QVariant(); \
81 }
82
83 BLOCK_FROM_NAME(Uic9183Head)
84 BLOCK_FROM_NAME(Uic9183TicketLayout)
85 BLOCK_FROM_NAME(Uic9183Flex)
86 BLOCK_FROM_NAME(Vendor0080BLBlock)
87 BLOCK_FROM_NAME(Vendor0080VUBlock)
88 BLOCK_FROM_NAME(Vendor1154UTBlock)
89
90#undef BLOCK_FROM_NAME
91
92 return QVariant::fromValue(findBlock(name.toUtf8().constData()));
93}
94
98
99void Uic9183Parser::parse(const QByteArray &data)
100{
101 d->m_data.clear();
102 d->m_payload.clear();
103
104 Uic9183Header header(data);
105 if (!header.isValid()) {
106 return;
107 }
108
109 // nx zlib payload
110 d->m_data = data;
111 d->m_payload.resize(4096);
112 z_stream stream;
113 stream.zalloc = nullptr;
114 stream.zfree = nullptr;
115 stream.opaque = nullptr;
116 stream.avail_in = data.size() - header.compressedMessageOffset();
117 stream.next_in = reinterpret_cast<unsigned char*>(const_cast<char*>(data.data() + header.compressedMessageOffset()));
118 stream.avail_out = d->m_payload.size();
119 stream.next_out = reinterpret_cast<unsigned char*>(d->m_payload.data());
120
121 inflateInit(&stream);
122 const auto res = inflate(&stream, Z_NO_FLUSH);
123 switch (res) {
124 case Z_OK:
125 case Z_STREAM_END:
126 break; // all good
127 default:
128 qCWarning(Log) << "UIC 918.3 payload zlib decompression failed" << stream.msg;
129 return;
130 }
131 inflateEnd(&stream);
132 d->m_payload.truncate(d->m_payload.size() - stream.avail_out);
133 //qCDebug(Log) << res << d->m_payload << stream.avail_out;
134
135 // workaround for Renfe (1071) having various errors...
136 if (d->m_payload.size() > 600 && d->m_payload.startsWith("U_HEAD0100531071") && d->m_payload[54] == 'U' && d->m_payload[36] == ' ') {
137 qCWarning(Log) << "Applying Renfe workaround for broken UIC 913.8 content...";
138 d->m_payload.remove(36, 1); // off by one in U_HEAD
139 const auto idx = d->m_payload.indexOf("U_TLAY00");
140 if (idx < d->m_payload.size() + 400 && std::strncmp(d->m_payload.constData() + idx + 12, "RCT2", 4) != 0) {
141 d->m_payload.insert(idx + 7, "1"); // wrong U_TLAY version
142 d->m_payload.replace(idx + 12, 4, "RCT2"); // wrong layout type
143 d->m_payload.remove(idx + 20, 1); // garbage trailing the layout type?
144 qCDebug(Log) << d->m_payload;
145 }
146 }
147}
148
149bool Uic9183Parser::isValid() const
150{
151 return !d->m_payload.isEmpty();
152}
153
154QString Uic9183Parser::pnr() const
155{
156 if (const auto head = findBlock<Uic9183Head>(); head.isValid()) {
157 auto key = head.ticketKey().trimmed();
158 const auto issuerId = head.issuerCompanyCodeNumeric();
159
160 // try to make this match what's printed on the matching tickets...
161 if (issuerId == 80 && (key.size() == 8 || key.size() == 9) && key.at(6) == QLatin1Char('-') && key.at(7).isDigit()) {
162 return key.left(6); // DB domestic
163 }
164 if (issuerId == 80 && key.size() == 13 &&
165 key.endsWith(QLatin1StringView("0101"))) {
166 return key.left(9); // DB domestic part of an international order
167 }
168 if ((issuerId == 1088 || issuerId == 1184) && key.size() == 9 && key.at(7) == QLatin1Char('_') && key.at(8).isDigit()) {
169 return key.left(7); // SNCB and NS
170 }
171
172 return key;
173 }
174
175 if (const auto flex = findBlock<Uic9183Flex>(); flex.isValid()) {
176 if (auto pnr = FcbExtractor::pnr(flex.fcb()); !pnr.isEmpty()) {
177 return pnr;
178 }
179 }
180
181 return {};
182}
183
184QString Uic9183Parser::name() const
185{
186 // ERA FCB
187 if (const auto flex = findBlock<Uic9183Flex>(); flex.isValid()) {
188 auto name = FcbExtractor::ticketName(flex.fcb());
189 if (!name.isEmpty()) {
190 return name;
191 }
192 }
193
194 // DB vendor block
195 if (const auto b = findBlock<Vendor0080BLBlock>(); b.isValid()) {
196 const auto sblock = b.findSubBlock("001");
197 if (!sblock.isNull()) {
198 return QString::fromUtf8(sblock.content(), sblock.contentSize());
199 }
200 }
201
202 // RCT2
203 if (const auto rct2 = rct2Ticket(); rct2.isValid()) {
204 return rct2.title();
205 }
206
207 return {};
208}
209
210QString Uic9183Parser::carrierId() const
211{
212 if (const auto head = findBlock<Uic9183Head>(); head.isValid()) {
213 return head.issuerCompanyCodeString();
214 }
215 if (const auto flex = findBlock<Uic9183Flex>(); flex.isValid()) {
216 if (const auto id = FcbExtractor::issuerId(flex.fcb()); !id.isEmpty()) {
217 return id;
218 }
219 }
220 return header().signerCompanyCode();
221}
222
223Organization Uic9183Parser::issuer() const
224{
225 Organization issuer;
226 if (const auto flex = findBlock<Uic9183Flex>(); flex.isValid()) {
227 issuer = FcbExtractor::issuer(flex.fcb());
228 }
229 issuer.setIdentifier(QLatin1StringView("uic:") + carrierId());
230 return issuer;
231}
232
233QDateTime Uic9183Parser::validFrom() const
234{
235 // ERA FCB
236 if (const auto flex = findBlock<Uic9183Flex>(); flex.isValid()) {
237 const auto dt = FcbExtractor::validFrom(flex.fcb());
238 if (dt.isValid()) {
239 return dt;
240 }
241 }
242
243 // DB vendor block
244 if (const auto b = findBlock<Vendor0080BLBlock>(); b.isValid() && b.orderBlockCount() == 1) {
245 return QDateTime(b.orderBlock(0).validFrom(), {0, 0, 0});
246 }
247
248 // ÖBB vender block
249 if (const auto b = findBlock("118199"); !b.isNull()) {
250 const auto obj = QJsonDocument::fromJson(QByteArray::fromRawData(b.content(), b.contentSize())).object();
251 auto dt =
252 QDateTime::fromString(obj.value(QLatin1StringView("V")).toString(),
253 QStringLiteral("yyMMddhhmm"));
254 if (dt.isValid()) { // ÖBB VorteilsCard barcodes have an empty vendor block
255 if (dt.date().year() < 2000) {
256 dt = dt.addYears(100);
257 }
258 dt.setTimeZone(QTimeZone::utc());
259 return dt;
260 }
261 }
262
263 // CD vender block
264 if (const auto b = findBlock<Vendor1154UTBlock>(); b.isValid()) {
265 const auto subBlock = b.findSubBlock("OD");
266 qDebug() << subBlock.toString();
267 if (!subBlock.isNull()) {
268 return QDateTime::fromString(subBlock.toString(), QStringLiteral("dd.MM.yyyy hh:mm"));
269 }
270 }
271
272 // RCT2
273 if (const auto rct2 = rct2Ticket(); rct2.isValid()) {
274 const auto dt = rct2.firstDayOfValidity();
275 if (dt.month() != 1 || dt.day() != 1 || !rct2.outboundDepartureStation().isEmpty()) {
276 return QDateTime(dt, {0, 0, 0});
277 }
278 // firstDayOfValidity is just a year, and we have wildcard station names
279 const auto dep = rct2.outboundDepartureTime();
280 return dep.isValid() ? dep : QDateTime(dt, {0, 0, 0});
281 }
282
283 return {};
284}
285
286QDateTime Uic9183Parser::validUntil() const
287{
288 // ERA FCB
289 if (const auto flex = findBlock<Uic9183Flex>(); flex.isValid()) {
290 const auto dt = FcbExtractor::validUntil(flex.fcb());
291 if (dt.isValid()) {
292 return dt;
293 }
294 }
295
296 // DB vendor block
297 if (const auto b = findBlock<Vendor0080BLBlock>(); b.isValid() && b.orderBlockCount() == 1) {
298 return QDateTime(b.orderBlock(0).validTo(), {23, 59, 59});
299 }
300
301 // ÖBB vender block
302 if (const auto b = findBlock("118199"); !b.isNull()) {
303 const auto obj = QJsonDocument::fromJson(QByteArray::fromRawData(b.content(), b.contentSize())).object();
304 auto dt =
305 QDateTime::fromString(obj.value(QLatin1StringView("B")).toString(),
306 QStringLiteral("yyMMddhhmm"));
307 if (dt.isValid()) { // ÖBB VorteilsCard barcodes have an empty vendor block
308 if (dt.date().year() < 2000) {
309 dt = dt.addYears(100);
310 }
311 dt.setTimeZone(QTimeZone::utc());
312 return dt;
313 }
314 }
315
316 // CD vender block
317 if (const auto b = findBlock<Vendor1154UTBlock>(); b.isValid()) {
318 const auto subBlock = b.findSubBlock("DO");
319 if (!subBlock.isNull()) {
320 return QDateTime::fromString(subBlock.toString(), QStringLiteral("dd.MM.yyyy hh:mm"));
321 }
322 }
323
324
325 // RCT2 RPT according to ERA TAP TSI Annex B.6
326 if (const auto rct2 = rct2Ticket(); rct2.isValid()) {
327 const auto validityRange = ticketLayout().text(3, 1, 36, 1).trimmed();
328 const auto idx = std::max(validityRange.lastIndexOf(QLatin1Char(' ')), validityRange.lastIndexOf(QLatin1Char('-')));
329 if (idx > 0) {
330 return QDateTime(QDate::fromString(validityRange.mid(idx + 1), QStringLiteral("dd.MM.yyyy")), {23, 59, 59});
331 }
332 return rct2.outboundArrivalTime();
333 }
334
335 return {};
336}
337
338Person Uic9183Parser::person() const
339{
340 // ERA FCB
341 if (const auto flex = findBlock<Uic9183Flex>(); flex.isValid()) {
342 Person p = FcbExtractor::person(flex.fcb());
343 if (!p.familyName().isEmpty() || !p.givenName().isEmpty()) {
344 return p;
345 }
346 }
347
348 // Deutsche Bahn vendor block
349 if (const auto b = findBlock<Vendor0080BLBlock>(); b.isValid()) {
350 // S028 contains family and given name separated by a '#', UTF-8 encoded
351 auto sblock = b.findSubBlock("028");
352 if (!sblock.isNull()) {
353 const auto endIt = sblock.content() + sblock.contentSize();
354 auto it = std::find(sblock.content(), endIt, '#');
355 if (it != endIt) {
356 Person p;
357 p.setGivenName(QString::fromUtf8(sblock.content(), std::distance(sblock.content(), it)));
358 ++it;
359 p.setFamilyName(QString::fromUtf8(it, std::distance(it, endIt)));
360 return p;
361 }
362 }
363 // S023 contains the full name, UTF-8 encoded
364 sblock = b.findSubBlock("023");
365 if (!sblock.isNull()) {
366 Person p;
367 p.setName(sblock.toString());
368 return p;
369 }
370 }
371 // CD vender block
372 if (const auto b = findBlock<Vendor1154UTBlock>(); b.isValid()) {
373 const auto subBlock = b.findSubBlock("KJ");
374 if (!subBlock.isNull()) {
375 Person p;
376 p.setName(subBlock.toString());
377 return p;
378 }
379 }
380
381 // RCT2 tickets
382 const auto rct2 = rct2Ticket();
383 if (rct2.isValid()) {
384 const auto name = rct2.passengerName();
385 if (!name.isEmpty()) {
386 Person p;
387 p.setName(name);
388 return p;
389 }
390 }
391
392 return {};
393}
394
395TrainStation Uic9183Parser::outboundDepartureStation() const
396{
397 TrainStation station;
398
399 // RTC2 ticket layout
400 if (const auto rtc2 = rct2Ticket(); rtc2.isValid()) {
401 station.setName(rtc2.outboundDepartureStation());
402 }
403
404 // DB vendor block
405 if (const auto b = findBlock<Vendor0080BLBlock>(); b.isValid()) {
406 if (const auto sblock = b.findSubBlock("015"); !sblock.isNull()) {
407 station.setName(sblock.toString());
408 }
409 // S035 contains the IBNR, possible with leading '80' country code and leading 0 stripped
410 if (const auto sblock = b.findSubBlock("035"); !sblock.isNull() && sblock.contentSize() <= 7) {
411 QString ibnr = QStringLiteral("ibnr:8000000");
412 const auto s = sblock.toString();
413 station.setIdentifier(ibnr.replace(ibnr.size() - s.size(), s.size(), s));
414 }
415 }
416
417 // ERA FCB
418 if (const auto flex = findBlock<Uic9183Flex>(); flex.hasTransportDocument()) {
419 FcbExtractor::readDepartureStation(flex.transportDocuments().at(0), station);
420 }
421
422 return station;
423}
424
425TrainStation Uic9183Parser::outboundArrivalStation() const
426{
427 TrainStation station;
428
429 // RTC2 ticket layout
430 if (const auto rtc2 = rct2Ticket(); rtc2.isValid()) {
431 station.setName(rtc2.outboundArrivalStation());
432 }
433
434 if (const auto b = findBlock<Vendor0080BLBlock>(); b.isValid()) {
435 if (const auto sblock = b.findSubBlock("016"); !sblock.isNull()) {
436 station.setName(sblock.toString());
437 }
438 // S036 contains the IBNR, possible with leading '80' country code and leading 0 stripped
439 if (const auto sblock = b.findSubBlock("036"); !sblock.isNull() && sblock.contentSize() <= 7) {
440 QString ibnr = QStringLiteral("ibnr:8000000");
441 const auto s = sblock.toString();
442 station.setIdentifier(ibnr.replace(ibnr.size() - s.size(), s.size(), s));
443 }
444 }
445
446 // ERA FCB
447 if (const auto flex = findBlock<Uic9183Flex>(); flex.hasTransportDocument()) {
448 FcbExtractor::readArrivalStation(flex.transportDocuments().at(0), station);
449 }
450
451 return station;
452}
453
454TrainStation Uic9183Parser::returnDepartureStation() const
455{
456 TrainStation station;
457
458 // RTC2 ticket layout
459 if (const auto rtc2 = rct2Ticket(); rtc2.isValid()) {
460 station.setName(rtc2.returnDepartureStation());
461 }
462
463 const auto outboundArrival = outboundArrivalStation();
464 // DB vendor block
465 if (const auto b = findBlock<Vendor0080BLBlock>(); b.isValid()) {
466 if (const auto sblock = b.findSubBlock("017"); !sblock.isNull()) {
467 station.setName(sblock.toString());
468 }
469 if (outboundArrival.name() == station.name()) {
470 station.setIdentifier(outboundArrival.identifier());
471 }
472 }
473
474 // ERA FCB
475 if (const auto flex = findBlock<Uic9183Flex>(); flex.hasTransportDocument()) {
476 const auto doc = flex.transportDocuments().at(0);
477 VariantVisitor([&station, outboundArrival](auto &&nrt) {
478 if (nrt.returnIncluded && nrt.returnDescriptionIsSet()) {
479 station.setName(nrt.returnDescription.fromStationNameUTF8);
480 station.setIdentifier(FcbUtil::fromStationIdentifier(nrt.stationCodeTable, nrt.returnDescription));
481 } else if (nrt.returnIncluded) {
482 if (outboundArrival.name() == station.name()) {
483 station.setIdentifier(outboundArrival.identifier());
484 }
485 }
486 }).visit<Fcb::v13::OpenTicketData, Fcb::v3::OpenTicketData>(doc);
487 FcbExtractor::fixStationCode(station);
488 }
489
490 return station;
491}
492
493TrainStation Uic9183Parser::returnArrivalStation() const
494{
495 TrainStation station;
496
497 // RTC2 ticket layout
498 if (const auto rtc2 = rct2Ticket(); rtc2.isValid()) {
499 station.setName(rtc2.returnArrivalStation());
500 }
501
502 const auto outboundDeparture = outboundDepartureStation();
503 // DB vendor block
504 if (const auto b = findBlock<Vendor0080BLBlock>(); b.isValid()) {
505 if (const auto sblock = b.findSubBlock("018"); !sblock.isNull()) {
506 station.setName(sblock.toString());
507 }
508 if (outboundDeparture.name() == station.name()) {
509 station.setIdentifier(outboundDeparture.identifier());
510 }
511 }
512
513 // ERA FCB
514 if (const auto flex = findBlock<Uic9183Flex>(); flex.hasTransportDocument()) {
515 const auto doc = flex.transportDocuments().at(0);
516 VariantVisitor([&station, outboundDeparture](auto &&nrt) {
517 if (nrt.returnIncluded && nrt.returnDescriptionIsSet()) {
518 station.setName(nrt.returnDescription.toStationNameUTF8);
519 station.setIdentifier(FcbUtil::toStationIdentifier(nrt.stationCodeTable, nrt.returnDescription));
520 } else if (nrt.returnIncluded) {
521 if (outboundDeparture.name() == station.name()) {
522 station.setIdentifier(outboundDeparture.identifier());
523 }
524 }
525 }).visit<Fcb::v13::OpenTicketData, Fcb::v3::OpenTicketData>(doc);
526 FcbExtractor::fixStationCode(station);
527 }
528
529 return station;
530}
531
532QString Uic9183Parser::seatingType() const
533{
534 if (const auto flex = findBlock<Uic9183Flex>(); flex.isValid()) {
535 if (auto c = FcbExtractor::seatingType(flex.fcb()); !c.isEmpty()) {
536 return c;
537 }
538 }
539
540 if (const auto b = findBlock<Vendor0080BLBlock>(); b.isValid()) {
541 // S014 contains the class, possibly with a leading 'S' for some reason
542 const auto sblock = b.findSubBlock("014");
543 if (!sblock.isNull()) {
544 const auto s = sblock.toString();
545 return s.startsWith(QLatin1Char('S')) ? s.right(1) : s;
546 }
547 }
548
549 if (const auto rct2 = rct2Ticket(); rct2.isValid()) {
550 return rct2.outboundClass();
551 }
552 return {};
553}
554
559
560QVariant Uic9183Parser::ticketLayoutVariant() const
561{
562 const auto layout = ticketLayout();
563 return layout.isValid() ? QVariant::fromValue(layout) : QVariant();
564}
565
567{
568 Rct2Ticket rct2(ticketLayout());
569 const auto u_head = findBlock<Uic9183Head>();
570 rct2.setContextDate(u_head.issuingDateTime());
571 return rct2;
572}
573
574QVariant Uic9183Parser::rct2TicketVariant() const
575{
576 const auto rct2 = rct2Ticket();
577 if (rct2.isValid()) {
578 return QVariant::fromValue(rct2);
579 }
580 return {};
581}
582
584{
585 return Uic9183Header(d->m_data);
586}
587
588QByteArray Uic9183Parser::rawData() const
589{
590 return d->m_data;
591}
592
594{
595 Uic9183Header h(data);
596 return h.isValid();
597}
598
599#include "moc_uic9183parser.cpp"
static QString toStationIdentifier(CodeTableTypeT stationCodeTable, const T &doc)
Arrival station identifier for a travel document, in the format needed for output with our JSON-LD fo...
Definition fcbutil.h:48
static QString fromStationIdentifier(CodeTableTypeT stationCodeTable, const T &doc)
Departure station identifier for a travel document, in the format needed for output with our JSON-LD ...
Definition fcbutil.h:34
A person.
Definition person.h:20
RCT2 ticket layout payload of an UIC 918.3 ticket token.
Definition rct2ticket.h:23
void setContextDate(const QDateTime &contextDt)
Date/time this ticket was first encountered, to recover possibly missing year numbers.
Train station.
Definition place.h:126
A data block from a UIC 918.3 ticket.
Represents a U_FLEX block holding different versions of an FCB payload.
Definition uic9183flex.h:22
U_HEAD block of a UIC 918.3 ticket container.
Definition uic9183head.h:21
Header of an UIC 918.3 ticket.
Parser for UIC 918.3 and 918.3* train tickets.
Q_INVOKABLE QVariant block(const QString &name) const
Same as the above, but for JS usage.
T findBlock() const
Returns the first block of type.
Uic9183Block findBlock(const char name[6]) const
Returns the first block with the given name.
QVariant ticketLayout
U_TLAY ticket layout block, if present, null otherwise.
QVariant rct2Ticket
RCT2 ticket layout block, if present, null otherwise.
void setContextDate(const QDateTime &)
Date/time this ticket was first encountered.
static bool maybeUic9183(const QByteArray &data)
Quickly checks if might be UIC 918.3 content.
Uic9183Block firstBlock() const
First data block in this ticket.
Uic9183Header header() const
Header found before the compressed payload.
Parser for a U_TLAY block in a UIC 918-3 ticket container, such as a ERA TLB ticket.
UIC 918.3 0080BL vendor data block.
UIC 918.3 0080VU vendor data block (DB local public transport extensions).
UIC 918.3 1154UT vendor data block.
Classes for reservation/travel data models, data extraction and data augmentation.
Definition berelement.h:17
char * data()
QByteArray fromRawData(const char *data, qsizetype size)
qsizetype size() const const
QDate fromString(QStringView string, QStringView format, QCalendar cal)
QDateTime fromString(QStringView string, QStringView format, QCalendar cal)
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
QJsonObject object() const const
QString fromUtf8(QByteArrayView str)
bool isEmpty() const const
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
qsizetype size() const const
QTimeZone utc()
QVariant fromValue(T &&value)
bool isValid() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Mar 28 2025 11:59:50 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.