File indexing completed on 2024-05-19 05:57:13

0001 /*
0002     SPDX-FileCopyrightText: 2021 Volker Krause <vkrause@kde.org>
0003     SPDX-FileCopyrightText: 2019 Jonah BrĂ¼chert <jbb@kaidan.im>
0004     SPDX-License-Identifier: GPL-3.0-or-later
0005 */
0006 
0007 #include "QrCodeContent.h"
0008 #include "mecardparser.h"
0009 
0010 #include <QRegularExpression>
0011 #include <QUrlQuery>
0012 
0013 QrCodeContent::QrCodeContent() = default;
0014 
0015 QrCodeContent::QrCodeContent(const QByteArray &content, Prison::Format::BarcodeFormat format)
0016     : m_content(content)
0017     , m_format(format)
0018 {
0019 }
0020 
0021 QrCodeContent::QrCodeContent(const QString &content, Prison::Format::BarcodeFormat format)
0022     : m_content(content)
0023     , m_format(format)
0024 {
0025 }
0026 
0027 QrCodeContent::~QrCodeContent() = default;
0028 
0029 static bool isUrl(const QString &text)
0030 {
0031     QRegularExpression exp(QStringLiteral("(?:https?|ftp)://\\S+"));
0032 
0033     return exp.match(text).hasMatch();
0034 }
0035 
0036 static bool isVCard(const QString &text)
0037 {
0038     return (text.startsWith(QLatin1String("BEGIN:VCARD")) && text.trimmed().endsWith(QLatin1String("END:VCARD")));
0039 }
0040 
0041 static bool isOtpToken(const QString &text)
0042 {
0043     if (text.startsWith(QLatin1String("otpauth"))) {
0044         QUrl uri(text);
0045         if (uri.isValid() && (uri.host() == QLatin1String("totp") || uri.host() == QLatin1String("hotp"))) {
0046             QUrlQuery query(uri.query());
0047             return query.hasQueryItem(QStringLiteral("secret"));
0048         }
0049     }
0050 
0051     return false;
0052 }
0053 
0054 static constexpr const char base45Table[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
0055 
0056 static bool isBase45(const QString &text)
0057 {
0058     return std::all_of(text.begin(), text.end(), [](QChar c) {
0059         return c.row() == 0 && std::find(std::begin(base45Table), std::end(base45Table), c.cell()) != std::end(base45Table);
0060     });
0061 }
0062 
0063 static bool isHealtCertificate(const QString &text)
0064 {
0065     return
0066         // EU DGC, NL COVID-19 CoronaCheck
0067         (text.size() > 400 && (text.startsWith(QLatin1String("HC1:")) || text.startsWith(QLatin1String("NL2:"))) && isBase45(text)) ||
0068         // SMART Health Cards (SHC)
0069         (text.size() > 1000 && text.startsWith(QLatin1String("shc:/")) && std::all_of(text.begin() + 10, text.end(), [](QChar c) {
0070             return c.row() == 0 && c.cell() >= '0' && c.cell() <= '9';
0071         }))
0072     ;
0073 }
0074 
0075 static bool isTransportTicket(const QByteArray &data)
0076 {
0077     // UIC 918.3 train ticket containers
0078     if (data.size() > 100 && (data.startsWith("#UT") || data.startsWith("OTI"))) {
0079         return true;
0080     }
0081 
0082     // VDV eTicket
0083     if (data.size() >= 352 && data.startsWith((char)0x9E) && data.contains("VDV")) {
0084         return true;
0085     }
0086 
0087     return false;
0088 }
0089 
0090 static bool isTransportTicket(const QString &text)
0091 {
0092     // IATA BCBP
0093     if (text.size() >= 47 && text[0] == QLatin1Char('M') && text[1].digitValue() >= 1 && text[1].digitValue() <= 4
0094         && std::all_of(text.begin(), text.end(), [](QChar c) { return c.row() == 0; })
0095         && std::all_of(text.begin() + 30, text.begin() + 36, [](QChar c) { return c.isLetter() && c.isUpper(); })) {
0096         return true;
0097     }
0098 
0099     return false;
0100 }
0101 
0102 static bool isWifiSetting(const QString &text)
0103 {
0104     MeCardParser p;
0105     return p.parse(text) && p.header().compare(QLatin1String("wifi"), Qt::CaseInsensitive) == 0;
0106 }
0107 
0108 // https://en.wikipedia.org/wiki/Global_Trade_Item_Number
0109 // https://en.wikipedia.org/wiki/International_Standard_Book_Number
0110 // https://en.wikipedia.org/wiki/International_Article_Number
0111 // https://en.wikipedia.org/wiki/List_of_GS1_country_codes
0112 static bool isGlobalTradeItemNumber(const QString &text)
0113 {
0114     if (text.size() != 13 || std::any_of(text.begin(), text.end(), [](auto c) { return c.row() != 0 || !c.isDigit(); })) {
0115         return false;
0116     }
0117 
0118     int checkSum = 0;
0119     for (int i = 0; i < text.size() - 1; ++i) {
0120         checkSum += text[i].digitValue() * (i % 2 == 1 ? 3 : 1);
0121     }
0122     return text.back().digitValue() == ((10 - (checkSum % 10)) % 10);
0123 }
0124 
0125 QrCodeContent::ContentType QrCodeContent::contentType() const
0126 {
0127     if (m_content.type() == QVariant::ByteArray) {
0128         const auto data = m_content.toByteArray();
0129 
0130         // Indian vaccination certificates
0131         if (data.startsWith(QByteArray::fromHex("504B0304")) && data.contains("certificate.json")) {
0132             return HealthCertificate;
0133         }
0134 
0135         if (isTransportTicket(data)) {
0136             return ContentType::TransportTicket;
0137         }
0138 
0139         return ContentType::Binary;
0140     }
0141 
0142     const auto text = m_content.toString();
0143     if (isUrl(text))
0144         return ContentType::Url;
0145     else if (isVCard(text))
0146         return ContentType::VCard;
0147     else if (isOtpToken(text))
0148         return ContentType::OtpToken;
0149     else if (isHealtCertificate(text))
0150         return ContentType::HealthCertificate;
0151     else if (m_format == Prison::Format::EAN13 && isGlobalTradeItemNumber(text)) {
0152         if (text.startsWith(QLatin1String("978"))) {
0153             return ContentType::ISBN;
0154         }
0155         if (text.startsWith(QLatin1String("950")) || text.startsWith(QLatin1String("979"))) { // ISSN / ISMN
0156             return ContentType::Text;
0157         }
0158         return ContentType::EAN;
0159     } else if (isTransportTicket(text)) {
0160         return ContentType::TransportTicket;
0161     } else if (isWifiSetting(text)) {
0162         return ContentType::WifiSetting;
0163     }
0164 
0165     return ContentType::Text;
0166 }
0167 
0168 bool QrCodeContent::isPlainText() const
0169 {
0170     return m_content.type() == QVariant::String;
0171 }
0172 
0173 QString QrCodeContent::text() const
0174 {
0175     if (m_content.type() == QVariant::String) {
0176         return m_content.toString();
0177     }
0178     return {};
0179 }
0180 
0181 QByteArray QrCodeContent::binaryContent() const
0182 {
0183     if (m_content.type() == QVariant::ByteArray) {
0184         return m_content.toByteArray();
0185     }
0186     return {};
0187 }