The codec is always set when reading the GPI header
[GPXSee.git] / src / data / gpiparser.cpp
blob47c2ea9195391e9cc70f56d53cd31acbbe97ecd2
1 #include <cstring>
2 #include <QDataStream>
3 #include <QtEndian>
4 #include <QUrl>
5 #include <QIODevice>
6 #include <QApplication>
7 #include <QBuffer>
8 #include <QImageReader>
9 #include <QCryptographicHash>
10 #include <QTemporaryDir>
11 #include "common/garmin.h"
12 #include "common/textcodec.h"
13 #include "common/color.h"
14 #include "common/util.h"
15 #include "address.h"
16 #include "gpiparser.h"
18 using namespace Garmin;
20 struct RecordHeader {
21 quint16 type;
22 quint16 flags;
23 quint32 size;
24 quint32 extra;
27 class TranslatedString {
28 public:
29 TranslatedString() {}
30 TranslatedString(const QString &lang, const QString &str)
31 : _lang(lang), _str(str) {}
33 const QString &str() const {return _str;}
34 const QString &lang() const {return _lang;}
36 private:
37 QString _lang;
38 QString _str;
41 #ifndef QT_NO_DEBUG
42 QDebug operator<<(QDebug dbg, const TranslatedString &ts)
44 dbg.nospace() << "TS(" << ts.lang() << ", " << ts.str() << ")";
45 return dbg.space();
47 #endif // QT_NO_DEBUG
49 class CryptDevice : public QIODevice
51 public:
52 CryptDevice(QIODevice *device, quint32 key, quint32 blockSize,
53 QObject *parent = 0);
54 bool isSequential() const {return true;}
56 protected:
57 qint64 readData(char *data, qint64 maxSize);
58 qint64 writeData(const char *, qint64) {return -1;}
60 private:
61 QIODevice *_device;
62 quint32 _key;
63 QByteArray _block;
64 int _available;
67 static void demangle(quint8 *data, quint32 size, quint32 key)
69 static const unsigned char shuf[] = {
70 0xb, 0xc, 0xa, 0x0,
71 0x8, 0xf, 0x2, 0x1,
72 0x6, 0x4, 0x9, 0x3,
73 0xd, 0x5, 0x7, 0xe
76 int hiCnt = 0, loCnt;
77 quint8 sum = shuf[((key >> 24) + (key >> 16) + (key >> 8) + key) & 0xf];
79 for (quint32 i = 0; i < size; i++) {
80 quint8 hiAdd = shuf[key >> (hiCnt << 2) & 0xf] + sum;
81 loCnt = (hiCnt > 6) ? 0 : hiCnt + 1;
82 quint8 loAdd = shuf[key >> (loCnt << 2) & 0xf] + sum;
83 quint8 hi = data[i] - (hiAdd << 4);
84 quint8 lo = data[i] - loAdd;
85 data[i] = (hi & 0xf0) | (lo & 0x0f);
86 hiCnt = (loCnt > 6) ? 0 : loCnt + 1;
90 CryptDevice::CryptDevice(QIODevice *device, quint32 key, quint32 blockSize,
91 QObject *parent) : QIODevice(parent), _device(device), _key(key), _available(0)
93 _block.resize(blockSize);
94 setOpenMode(_device->openMode());
97 qint64 CryptDevice::readData(char *data, qint64 maxSize)
99 qint64 rs, ts = 0;
100 int cs;
102 if (_available) {
103 cs = qMin(maxSize, (qint64)_available);
104 memcpy(data, _block.constData() + _block.size() - _available, cs);
105 _available -= cs;
106 maxSize -= cs;
107 ts = cs;
110 while (maxSize) {
111 if ((rs = _device->read(_block.data(), _block.size())) < 0)
112 return -1;
113 else if (!rs)
114 break;
115 _available = rs;
116 demangle((quint8*)_block.data(), _available, _key);
117 cs = qMin(maxSize, (qint64)_available);
118 memcpy(data + ts, _block.constData(), cs);
119 _available -= cs;
120 maxSize -= cs;
121 ts += cs;
124 return ts;
128 class DataStream : public QDataStream
130 public:
131 DataStream(QIODevice *d) : QDataStream(d) {}
133 void setCodepage(quint16 codepage) {_codec = TextCodec(codepage);}
135 quint16 readString(QString &str);
136 qint32 readInt24();
137 quint8 readRecordHeader(RecordHeader &hdr);
138 quint32 readTranslatedObjects(QList<TranslatedString> &objects);
140 quint32 skipRecord();
141 quint16 nextHeaderType();
143 private:
144 TextCodec _codec;
147 quint16 DataStream::readString(QString &str)
149 quint16 len;
150 *this >> len;
151 QByteArray ba;
152 ba.resize(len);
153 readRawData(ba.data(), len);
154 str = _codec.toString(ba);
156 return len + 2;
159 qint32 DataStream::readInt24()
161 unsigned char data[3];
162 quint32 val;
164 readRawData((char*)data, sizeof(data));
165 val = data[0] | ((quint32)data[1]) << 8 | ((quint32)data[2]) << 16;
166 return (val > 0x7FFFFF) ? (val & 0x7FFFFF) - 0x800000 : val;
169 quint16 DataStream::nextHeaderType()
171 quint16 type = 0;
173 if (device()->peek((char*)&type, sizeof(type)) < (qint64)sizeof(type)) {
174 setStatus(QDataStream::ReadCorruptData);
175 return 0xFFFF;
176 } else
177 return qFromLittleEndian(type);
180 quint8 DataStream::readRecordHeader(RecordHeader &hdr)
182 *this >> hdr.type >> hdr.flags >> hdr.size;
183 if (hdr.flags & 0xA)
184 *this >> hdr.extra;
185 return (hdr.flags & 0xA) ? 12 : 8;
188 quint32 DataStream::skipRecord()
190 RecordHeader rh;
191 quint8 rs = readRecordHeader(rh);
192 skipRawData(rh.size);
194 return rs + rh.size;
197 quint32 DataStream::readTranslatedObjects(QList<TranslatedString> &objects)
199 qint32 size = 0, ret;
200 char lang[3];
202 memset(lang, 0, sizeof(lang));
203 objects.clear();
205 *this >> size;
206 ret = size + 4;
207 while (status() == QDataStream::Ok && size > 0) {
208 QString str;
209 readRawData(lang, sizeof(lang) - 1);
210 size -= readString(str) + 2;
211 objects.append(TranslatedString(lang, str));
214 if (size < 0)
215 setStatus(QDataStream::ReadCorruptData);
217 return ret;
221 static quint32 readFileDataRecord(DataStream &stream)
223 RecordHeader rh;
224 quint32 ds, flags;
225 quint16 s2, s3;
226 quint8 rs;
227 QList<TranslatedString> obj;
229 rs = stream.readRecordHeader(rh);
230 stream >> flags >> s2 >> s3;
231 ds = 8;
232 // name
233 ds += stream.readTranslatedObjects(obj);
234 // copyright
235 ds += stream.readTranslatedObjects(obj);
236 // additional stuff depending on flags
237 // ...
239 if (ds > rh.size)
240 stream.setStatus(QDataStream::ReadCorruptData);
241 else if (ds < rh.size)
242 stream.skipRawData(rh.size - ds);
244 return rs + rh.size;
247 static quint32 readDescription(DataStream &stream, Waypoint &waypoint)
249 RecordHeader rh;
250 quint8 rs;
251 quint32 ds;
252 QList<TranslatedString> obj;
254 rs = stream.readRecordHeader(rh);
255 ds = stream.readTranslatedObjects(obj);
256 if (!obj.isEmpty())
257 waypoint.setDescription(obj.first().str());
259 if (ds != rh.size)
260 stream.setStatus(QDataStream::ReadCorruptData);
262 return rs + rh.size;
265 static quint32 readNotes(DataStream &stream, Waypoint &waypoint)
267 RecordHeader rh;
268 quint8 rs, flags;
269 quint32 ds = 1;
271 rs = stream.readRecordHeader(rh);
272 stream >> flags;
273 if (flags & 0x1) {
274 QList<TranslatedString> obj;
275 ds += stream.readTranslatedObjects(obj);
276 if (!obj.isEmpty())
277 waypoint.setComment(obj.first().str());
279 if (flags & 0x2) {
280 QString str;
281 ds += stream.readString(str);
282 if (!str.isEmpty())
283 waypoint.setComment(str);
286 if (ds != rh.size)
287 stream.setStatus(QDataStream::ReadCorruptData);
289 return rs + rh.size;
292 static quint32 readContact(DataStream &stream, Waypoint &waypoint)
294 RecordHeader rh;
295 quint8 rs;
296 quint16 flags;
297 quint32 ds = 2;
298 QString str;
299 QList<TranslatedString> obj;
301 rs = stream.readRecordHeader(rh);
302 stream >> flags;
304 if (flags & 0x1) {
305 ds += stream.readString(str);
306 waypoint.setPhone(str);
308 if (flags & 0x2) // phone2
309 ds += stream.readString(str);
310 if (flags & 0x4) // fax
311 ds += stream.readString(str);
312 if (flags & 0x8) {
313 ds += stream.readString(str);
314 waypoint.addLink(Link("mailto:" + str, str));
316 if (flags & 0x10) {
317 ds += stream.readString(str);
318 QUrl url(str);
319 waypoint.addLink(Link(url.scheme().isEmpty()
320 ? "http://" + str : str, str));
322 if (flags & 0x20) // unknown
323 ds += stream.readTranslatedObjects(obj);
325 if (ds != rh.size)
326 stream.setStatus(QDataStream::ReadCorruptData);
328 return rs + rh.size;
331 static quint32 readAddress(DataStream &stream, Waypoint &waypoint)
333 RecordHeader rh;
334 quint8 rs;
335 quint16 flags;
336 quint32 ds = 2;
337 QList<TranslatedString> obj;
338 QString str;
339 Address addr;
341 rs = stream.readRecordHeader(rh);
342 stream >> flags;
344 if (flags & 0x1) {
345 ds += stream.readTranslatedObjects(obj);
346 if (!obj.isEmpty())
347 addr.setCity(obj.first().str());
349 if (flags & 0x2) {
350 ds += stream.readTranslatedObjects(obj);
351 if (!obj.isEmpty())
352 addr.setCountry(obj.first().str());
354 if (flags & 0x4) {
355 ds += stream.readTranslatedObjects(obj);
356 if (!obj.isEmpty())
357 addr.setState(obj.first().str());
359 if (flags & 0x8) {
360 ds += stream.readString(str);
361 addr.setPostalCode(str);
363 if (flags & 0x10) {
364 ds += stream.readTranslatedObjects(obj);
365 if (!obj.isEmpty())
366 addr.setStreet(obj.first().str());
368 if (flags & 0x20) // unknown
369 ds += stream.readString(str);
371 if (addr.isValid())
372 waypoint.setAddress(addr.address());
374 if (ds != rh.size)
375 stream.setStatus(QDataStream::ReadCorruptData);
377 return rs + rh.size;
380 static quint32 readImageInfo(DataStream &stream, Waypoint &waypoint,
381 const QString &fileName, int &imgId)
383 RecordHeader rh;
384 quint8 rs, s1;
385 quint32 size;
387 rs = stream.readRecordHeader(rh);
388 stream >> s1 >> size;
390 QByteArray ba;
391 ba.resize(size);
392 stream.readRawData(ba.data(), ba.size());
394 if (Util::tempDir().isValid()) {
395 QBuffer buf(&ba);
396 QImageReader ir(&buf);
398 QByteArray id(fileName.toUtf8() + QByteArray::number(imgId++));
399 QFile imgFile(Util::tempDir().path() + "/" + QString("%0.%1").arg(
400 QCryptographicHash::hash(id, QCryptographicHash::Sha1).toHex(),
401 QString(ir.format())));
402 imgFile.open(QIODevice::WriteOnly);
403 imgFile.write(ba);
404 imgFile.close();
406 waypoint.addImage(imgFile.fileName());
409 if (size + 5 != rh.size)
410 stream.setStatus(QDataStream::ReadCorruptData);
412 return rs + rh.size;
415 static int speed(quint16 flags)
417 return (((flags >> 3) & 0x0F) * 10) + (((flags >> 2) & 1) * 5);
420 static QString units(quint16 flags)
422 return (flags & (1<<8)) ? "km/h" : "mi/h";
425 static bool portable(quint16 flags)
427 return (flags & 1);
430 static bool redLight(quint16 flags)
432 return (flags & (1<<9));
435 static QString cameraDesc(quint16 flags)
437 if (redLight(flags))
438 return "Red light camera";
439 else {
440 QString desc(QString::number(speed(flags)) + "&nbsp;" + units(flags));
441 return portable(flags) ? "<i>" + desc + "<i>" : desc;
445 static quint32 readCamera(DataStream &stream, QVector<Waypoint> &waypoints,
446 QList<Area> &polygons)
448 RecordHeader rh;
449 quint8 s7, rs;
450 quint16 flags;
451 qint32 top, right, bottom, left, lat, lon;
452 quint32 ds = 15;
455 rs = stream.readRecordHeader(rh);
456 top = stream.readInt24();
457 right = stream.readInt24();
458 bottom = stream.readInt24();
459 left = stream.readInt24();
460 stream >> flags >> s7;
462 if (s7) {
463 quint32 skip = s7 + 2 + s7/4;
464 stream.skipRawData(skip);
465 lat = stream.readInt24();
466 lon = stream.readInt24();
467 ds += skip + 6;
468 } else {
469 quint8 s8;
470 stream.skipRawData(9);
471 stream >> s8;
472 quint32 skip = 3 + s8 + s8/4;
473 stream.skipRawData(skip);
474 lat = stream.readInt24();
475 lon = stream.readInt24();
476 ds += skip + 16;
479 Area area(RectC(Coordinates(toWGS24(left), toWGS24(top)),
480 Coordinates(toWGS24(right), toWGS24(bottom))));
481 area.setDescription(cameraDesc(flags));
483 waypoints.append(Coordinates(toWGS24(lon), toWGS24(lat)));
484 polygons.append(area);
486 if (ds > rh.size)
487 stream.setStatus(QDataStream::ReadCorruptData);
488 else if (ds < rh.size)
489 stream.skipRawData(rh.size - ds);
491 return rs + rh.size;
494 static quint32 readIconId(DataStream &stream, quint16 &id)
496 RecordHeader rh;
497 quint8 rs;
499 rs = stream.readRecordHeader(rh);
500 stream >> id;
502 if (2 != rh.size)
503 stream.setStatus(QDataStream::ReadCorruptData);
505 return rs + rh.size;
508 static quint32 readPOI(DataStream &stream, QVector<Waypoint> &waypoints,
509 const QString &fileName, int &imgId, QVector<QPair<int, quint16> > &icons)
511 RecordHeader rh;
512 quint8 rs;
513 quint32 ds;
514 qint32 lat, lon;
515 quint16 s3, iconId = 0;
516 QList<TranslatedString> obj;
518 rs = stream.readRecordHeader(rh);
519 stream >> lat >> lon >> s3;
520 stream.skipRawData(s3);
521 ds = 10 + s3;
522 ds += stream.readTranslatedObjects(obj);
524 waypoints.append(Waypoint(Coordinates(toWGS32(lon), toWGS32(lat))));
525 if (!obj.isEmpty())
526 waypoints.last().setName(obj.first().str());
528 while (stream.status() == QDataStream::Ok && ds < rh.size) {
529 switch (stream.nextHeaderType()) {
530 case 4:
531 ds += readIconId(stream, iconId);
532 break;
533 case 10:
534 ds += readDescription(stream, waypoints.last());
535 break;
536 case 11:
537 ds += readAddress(stream, waypoints.last());
538 break;
539 case 12:
540 ds += readContact(stream, waypoints.last());
541 break;
542 case 13:
543 ds += readImageInfo(stream, waypoints.last(), fileName, imgId);
544 break;
545 case 14:
546 ds += readNotes(stream, waypoints.last());
547 break;
548 default:
549 ds += stream.skipRecord();
553 icons.append(QPair<int, quint16>(waypoints.size() - 1, iconId));
555 if (ds != rh.size)
556 stream.setStatus(QDataStream::ReadCorruptData);
558 return rs + rh.size;
561 static quint32 readSpatialIndex(DataStream &stream, QVector<Waypoint> &waypoints,
562 QList<Area> &polygons, const QString &fileName, int &imgId,
563 QVector<QPair<int, quint16> > &icons)
565 RecordHeader rh;
566 quint32 ds, s5;
567 qint32 top, right, bottom, left;
568 quint16 s6;
569 quint8 rs;
571 rs = stream.readRecordHeader(rh);
572 stream >> top >> right >> bottom >> left >> s5 >> s6;
573 stream.skipRawData(s6);
574 ds = 22 + s6;
575 if (rh.flags & 0x8) {
576 while (stream.status() == QDataStream::Ok && ds < rh.size) {
577 switch (stream.nextHeaderType()) {
578 case 2:
579 ds += readPOI(stream, waypoints, fileName, imgId, icons);
580 break;
581 case 8:
582 ds += readSpatialIndex(stream, waypoints, polygons,
583 fileName, imgId, icons);
584 break;
585 case 19:
586 ds += readCamera(stream, waypoints, polygons);
587 break;
588 default:
589 ds += stream.skipRecord();
594 if (ds != rh.size)
595 stream.setStatus(QDataStream::ReadCorruptData);
597 return rs + rh.size;
600 static quint32 readSymbol(DataStream &stream, QPixmap &pixmap)
602 RecordHeader rh;
603 quint8 rs, u8, bpp, transparent;
604 quint32 ds = 36, u32, imageSize, paletteSize, bgColor;
605 quint16 id, height, width, lineSize;
606 QByteArray data;
607 QVector<QRgb> palette;
608 QImage img;
610 rs = stream.readRecordHeader(rh);
611 stream >> id >> height >> width >> lineSize >> bpp >> u8 >> u8 >> u8
612 >> imageSize >> u32 >> paletteSize >> bgColor >> transparent >> u8 >> u8
613 >> u8 >> u32;
614 if (imageSize) {
615 data.resize(imageSize);
616 stream.readRawData(data.data(), data.size());
617 ds += data.size();
619 if (paletteSize) {
620 quint32 rgb;
621 palette.resize(paletteSize);
622 for (quint32 i = 0; i < paletteSize; i++) {
623 stream >> rgb;
624 palette[i] = (transparent && rgb == bgColor)
625 ? QRgb(0)
626 : Color::rgb((rgb & 0x000000FF), (rgb & 0x0000FF00) >> 8,
627 (rgb & 0x00FF0000) >> 16);
629 ds += paletteSize * 4;
632 if (data.size() >= lineSize * height) {
633 if (paletteSize) {
634 img = QImage((uchar*)data.data(), width, height, lineSize,
635 QImage::Format_Indexed8);
636 img.setColorTable(palette);
637 } else
638 img = QImage((uchar*)data.data(), width, height, lineSize,
639 QImage::Format_RGBX8888).rgbSwapped();
641 pixmap = QPixmap::fromImage(img);
643 /* There should be no more data left in the record, but broken GPI files
644 generated by pinns.co.uk tools exist in the wild so we read out
645 the record as a workaround for such files. */
646 if (ds > rh.size)
647 stream.setStatus(QDataStream::ReadCorruptData);
648 else if (ds < rh.size)
649 stream.skipRawData(rh.size - ds);
651 return rs + rh.size;
654 static void readPOIDatabase(DataStream &stream, QVector<Waypoint> &waypoints,
655 QList<Area> &polygons, const QString &fileName, int &imgId)
657 RecordHeader rh;
658 QList<TranslatedString> obj;
659 quint32 ds;
660 QVector<QPair<int, quint16> > il;
661 QVector<QPixmap> icons;
663 stream.readRecordHeader(rh);
664 ds = stream.readTranslatedObjects(obj);
665 ds += readSpatialIndex(stream, waypoints, polygons, fileName, imgId, il);
666 if (rh.flags & 0x8) {
667 while (stream.status() == QDataStream::Ok && ds < rh.size) {
668 switch (stream.nextHeaderType()) {
669 case 5:
670 icons.append(QPixmap());
671 ds += readSymbol(stream, icons.last());
672 break;
673 case 8:
674 ds += readSpatialIndex(stream, waypoints, polygons,
675 fileName, imgId, il);
676 break;
677 default:
678 ds += stream.skipRecord();
683 for (int i = 0; i < il.size(); i++) {
684 const QPair<int, quint16> &e = il.at(i);
685 if (e.second < icons.size())
686 waypoints[e.first].setStyle(icons.at(e.second));
689 if (ds != rh.size)
690 stream.setStatus(QDataStream::ReadCorruptData);
693 bool GPIParser::readData(DataStream &stream, QVector<Waypoint> &waypoints,
694 QList<Area> &polygons, const QString &fileName)
696 int imgId = 0;
698 while (stream.status() == QDataStream::Ok) {
699 switch (stream.nextHeaderType()) {
700 case 0x09:
701 readPOIDatabase(stream, waypoints, polygons, fileName,
702 imgId);
703 break;
704 case 0xffff: // EOF
705 stream.skipRecord();
706 if (stream.status() == QDataStream::Ok)
707 return true;
708 break;
709 default:
710 stream.skipRecord();
714 _errorString = "Invalid/corrupted GPI data";
716 return false;
719 bool GPIParser::readGPIHeader(DataStream &stream)
721 RecordHeader rh;
722 char m1[6], m2[2];
723 quint16 codepage = 0;
724 quint8 s2, s3;
725 quint32 ds;
727 stream.readRecordHeader(rh);
728 stream.readRawData(m1, sizeof(m1));
729 stream.readRawData(m2, sizeof(m2));
730 stream >> codepage >> s2 >> s3;
731 ds = sizeof(m1) + sizeof(m2) + 4;
733 stream.setCodepage(codepage);
735 if (s2 & 0x10)
736 ds += readFileDataRecord(stream);
738 if (stream.status() != QDataStream::Ok || ds != rh.size) {
739 _errorString = "Invalid GPI header";
740 return false;
741 } else
742 return true;
745 bool GPIParser::readFileHeader(DataStream &stream, quint32 &ebs)
747 RecordHeader rh;
748 quint32 ds, s7;
749 quint16 s10;
750 quint8 s5, s6, s8, s9;
751 char magic[6];
753 stream.readRecordHeader(rh);
754 stream.readRawData(magic, sizeof(magic));
755 if (memcmp(magic, "GRMREC", sizeof(magic))) {
756 _errorString = "Not a GPI file";
757 return false;
759 stream >> s5 >> s6 >> s7 >> s8 >> s9 >> s10;
760 stream.skipRawData(s10);
761 ds = sizeof(magic) + 10 + s10;
763 ebs = (s8 & 0x4) ? s9 * 8 + 8 : 0;
765 if (ds > rh.size)
766 stream.setStatus(QDataStream::ReadCorruptData);
767 else if (ds < rh.size)
768 stream.skipRawData(rh.size - ds);
770 if (stream.status() != QDataStream::Ok) {
771 _errorString = "Invalid file header";
772 return false;
773 } else
774 return true;
777 bool GPIParser::parse(QFile *file, QList<TrackData> &tracks,
778 QList<RouteData> &routes, QList<Area> &polygons, QVector<Waypoint> &waypoints)
780 Q_UNUSED(tracks);
781 Q_UNUSED(routes);
782 DataStream stream(file);
783 quint32 ebs;
785 stream.setByteOrder(QDataStream::LittleEndian);
787 if (!readFileHeader(stream, ebs) || !readGPIHeader(stream))
788 return false;
790 if (ebs) {
791 CryptDevice dev(stream.device(), 0xf870b5, ebs);
792 DataStream cryptStream(&dev);
793 cryptStream.setByteOrder(QDataStream::LittleEndian);
794 return readData(cryptStream, waypoints, polygons, file->fileName());
795 } else
796 return readData(stream, waypoints, polygons, file->fileName());