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 }