From 2edcb7e330d6384eed94d768c2e85208e4c54333 Mon Sep 17 00:00:00 2001 From: Adam Wulkiewicz Date: Wed, 27 Jul 2022 21:05:30 +0200 Subject: [PATCH] [MOS-101] Parse MMS notification and show [MMS] in message thread The `[MMS]` notification is shown in message thread of the correct number - Add PDU WAP Push MMS Notification parser - Use it in ServiceCellular to parse the raw message - Refactor hexToBytes and bytesToHex utils - Add endsWith util - Add unit tests --- .../service-cellular/CMakeLists.txt | 1 + module-services/service-cellular/Pdu.cpp | 982 ++++++++++++++++++ module-services/service-cellular/Pdu.hpp | 24 + .../service-cellular/QMBNManager.cpp | 6 +- .../service-cellular/ServiceCellular.cpp | 24 +- .../service-cellular/tests/CMakeLists.txt | 15 +- .../service-cellular/tests/unittest_Pdu.cpp | 156 +++ module-utils/utility/Utils.cpp | 20 - module-utils/utility/Utils.hpp | 64 +- module-utils/utility/tests/unittest_utils.cpp | 57 +- pure_changelog.md | 1 + 11 files changed, 1308 insertions(+), 42 deletions(-) create mode 100644 module-services/service-cellular/Pdu.cpp create mode 100644 module-services/service-cellular/Pdu.hpp create mode 100644 module-services/service-cellular/tests/unittest_Pdu.cpp diff --git a/module-services/service-cellular/CMakeLists.txt b/module-services/service-cellular/CMakeLists.txt index 438a0f0d2e390e6e127a151bef9f97cd26e445db..068f4f68025d98b40a3e0eb6c62f658a10f4d8de 100644 --- a/module-services/service-cellular/CMakeLists.txt +++ b/module-services/service-cellular/CMakeLists.txt @@ -23,6 +23,7 @@ set(SOURCES SignalStrength.cpp NetworkSettings.cpp PacketData.cpp + Pdu.cpp QMBNManager.cpp RequestFactory.cpp CellularRequestHandler.cpp diff --git a/module-services/service-cellular/Pdu.cpp b/module-services/service-cellular/Pdu.cpp new file mode 100644 index 0000000000000000000000000000000000000000..cbf316c8490e1c280887537a268bd75b079ed5fe --- /dev/null +++ b/module-services/service-cellular/Pdu.cpp @@ -0,0 +1,982 @@ +// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved. +// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md + +#include "Pdu.hpp" + +#include + +#include +#include +#include +#include +#include + +namespace pdu::constants +{ + + // PDU Types + // http://www.openmobilealliance.org/release/Browser_Protocol_Stack/V2_1-20110315-A/OMA-WAP-TS-WSP-V1_0-20110315-A.pdf, + // p98 + enum Type : std::uint8_t + { + Reserved = 0x00, + Connect, + ConnectReply, + Redirect, + Reply, + Disconnect, + Push, + ConfirmedPush, + Suspend, + Resume, + Get = 0x40, + Options, + Head, + Delete, + Trace, + Post = 0x60, + Put, + DataFragment = 0x80 + }; + + // Well-known Header Field Names + // http://www.openmobilealliance.org/release/Browser_Protocol_Stack/V2_1-20110315-A/OMA-WAP-TS-WSP-V1_0-20110315-A.pdf, + // p103 + enum FieldName : std::uint8_t + { + /* v1.1 */ + Accept = 0x00, + AcceptCharset11, // deprecated + AcceptEncoding11, // deprecated + AcceptLanguage, + AcceptRanges, + Age, + Allow, + Authorization, + CacheControl11, // deprecated + Connection, + ContentBase11, // deprecated + ContentEncoding, + ContentLanguage, + ContentLength, + ContentLocation, + ContentMD5, + ContentRange11, // deprecated + ContentType, + Date, + Etag, + Expires, + From, + Host, + IfModifiedSince, + IfMatch, + IfNoneMatch, + IfRange, + IfUnmodifiedSince, + Location, + LastModified, + MaxForwards, + Pragma, + ProxyAuthenticate, + ProxyAuthorization, + Public, + Range, + Referer, + RetryAfter, + Server, + TransferEncoding, + Upgrade, + UserAgent, + Vary, + Via, + Warning, + WWWAuthenticate, + ContentDisposition11, // deprecated + /* v1.2 */ + XWapApplicationId, // X-Wap-Application-ID + XWapContentURI, + XWapInitiatorURI, + AcceptApplication, + BearerIndication, + PushFlag, + Profile, + ProfileDiff, + ProfileWarning12, // deprecated + /* v1.3 */ + Expect13, + TE, + Trailer, + AcceptCharset, + AcceptEncoding, + CacheControl13, // deprecated + ContentRange, + XWapTod, + ContentID, + SetCookie, + Cookie, + EncodingVersion, + /* v1.4 */ + ProfileWarning, + ContentDisposition, + XWAPSecurity, + CacheControl, + /* v1.5 */ + Expect, + XWapLocInvocation, + XWapLocDelivery + }; + + // Basic Well-Known Content Types + // http://www.openmobilealliance.org/release/Browser_Protocol_Stack/V2_1-20110315-A/OMA-WAP-TS-WSP-V1_0-20110315-A.pdf, + // p105 + // http://www.wapforum.org/wina/wsp-content-type.htm + enum ContentType : std::uint8_t + { + AnyAny = 0x00, // "*/*" + TextAny, // "text/*" + TextHtml, // "text/html" + TextPlain, // "text/plain" + TextXHdml, // "text/x-hdml" + TextXTtml, // "text/x-ttml" + TextXVCalendar, // "text/x-vCalendar" + TextXVCard, // "text/x-vCard" + TextVndWapWml, // "text/vnd.wap.wml" + TextVndWapWmlScript, // "text/vnd.wap.wmlscript" + TextVndWapWtaEvent, // "text/vnd.wap.wta-event" + MultipartAny, // "multipart/*" + MultipartMixed, // "multipart/mixed" + MultipartFormData, // "multipart/form-data" + MultipartByteranges, // "multipart/byteranges" + MultipartAlternative, // "multipart/alternative" + ApplicationAny, // "application/*" + ApplicationJavaVm, // "application/java-vm" + ApplicationXWwwFormUrlencoded, // "application/x-www-form-urlencoded" + ApplicationXHdmlc, // "application/x-hdmlc" + ApplicationVndWapWmlc, // "application/vnd.wap.wmlc" + ApplicationVndWapWmlscriptc, // "application/vnd.wap.wmlscriptc" + ApplicationVndWapWtaEventc, // "application/vnd.wap.wta-eventc" + ApplicationVndWapUaprof, // "application/vnd.wap.uaprof" + ApplicationVndWapWtlsCaCertificate, // "application/vnd.wap.wtls-ca-certificate" + ApplicationVndWapWtlsUserCertificate, // "application/vnd.wap.wtls-user-certificate" + ApplicationXX509CaCert, // "application/x-x509-ca-cert" + ApplicationXX509UserCert, // "application/x-x509-user-cert" + ImageAny, // "image/*" + ImageGif, // "image/gif" + ImageJpeg, // "image/jpeg" + ImageTiff, // "image/tiff" + ImagePng, // "image/png" + ImageVndWapWbmp, // "image/vnd.wap.wbmp" + ApplicationVndWapMultipartAny, // "application/vnd.wap.multipart.*" + ApplicationVndWapMultipartMixed, // "application/vnd.wap.multipart.mixed" + ApplicationVndWapMultipartFormData, // "application/vnd.wap.multipart.form-data" + ApplicationVndWapMultipartByteranges, // "application/vnd.wap.multipart.byteranges" + ApplicationVndWapMultipartAlternative, // "application/vnd.wap.multipart.alternative" + ApplicationXml, // "application/xml" + TextXml, // "text/xml" + ApplicationVndWapWbxml, // "application/vnd.wap.wbxml" + ApplicationXX968CrossCert, // "application/x-x968-cross-cert" + ApplicationXX968CaCert, // "application/x-x968-ca-cert" + ApplicationXX968UserCert, // "application/x-x968-user-cert" + TextVndWapSi, // "text/vnd.wap.si" + ApplicationVndWapSic, // "application/vnd.wap.sic" + TextVndWapSl, // "text/vnd.wap.sl" + ApplicationVndWapSlc, // "application/vnd.wap.slc" + TextVndWapCo, // "text/vnd.wap.co" + ApplicationVndWapCoc, // "application/vnd.wap.coc" + ApplicationVndWapRelated, // "application/vnd.wap.multipart.related" + ApplicationVndWapSia, // "application/vnd.wap.sia" + TextVndWapConnectivityXml, // "text/vnd.wap.connectivity-xml" + ApplicationVndWapConnectivityWbxml, // "application/vnd.wap.connectivity-wbxml" + ApplicationPkcs7Mime, // "application/pkcs7-mime" + ApplicationVndWapHashedCertificate, // "application/vnd.wap.hashed-certificate" + ApplicationVndWapSignedCertificate, // "application/vnd.wap.signed-certificate" + ApplicationVndWapCertResponse, // "application/vnd.wap.cert-response" + ApplicationXhtmlXml, // "application/xhtml+xml" + ApplicationWmlXml, // "application/wml+xml" + TextCss, // "text/css" + ApplicationVndWapMmsMessage, // "application/vnd.wap.mms-message" + ApplicationVndWapRolloverCertificate, // "application/vnd.wap.rollover-certificate" + ApplicationVndWapLoccWbxml, // "application/vnd.wap.locc+wbxml" + ApplicationVndWapLocXml, // "application/vnd.wap.loc+xml" + ApplicationVndSyncmlDmWbxml, // "application/vnd.syncml.dm+wbxml" + ApplicationVndSyncmlDmXml, // "application/vnd.syncml.dm+xml" + ApplicationVndSyncmlNotification, // "application/vnd.syncml.notification" + ApplicationVndWapXhtmlXml, // "application/vnd.wap.xhtml+xml" + ApplicationVndWvXspCir, // "application/vnd.wv.csp.cir" + ApplicationVndOmaDdXml, // "application/vnd.oma.dd+xml" + ApplicationVndOmaDrmMessage, // "application/vnd.oma.drm.message" + ApplicationVndOmaDrmContent, // "application/vnd.oma.drm.content" + ApplicationVndOmaDrmRightsXml, // "application/vnd.oma.drm.rights+xml" + ApplicationVndOmaDrmRightsWbxml, // "application/vnd.oma.drm.rights+wbxml" + }; + + enum PushApplicationId : std::uint8_t + { + XWapApplicationAny = 0x00, // x-wap-application:* + XWapApplicationPushSia, // x-wap-application:push.sia + XWapApplicationWmlUa, // x-wap-application:wml.ua + XWapApplicationWtaUa, // x-wap-application:wta.ua + XWapApplicationMmsUa, // x-wap-application:mms.ua + XWapApplicationPushSyncml, // x-wap-application:push.syncml + XWapApplicationLocUa, // x-wap-application:loc.ua + XWapApplicationSyncmlDm, // x-wap-application:syncml.dm + XWapApplicationDrmUa, // x-wap-application:drm.ua + XWapApplicationEmnUa, // x-wap-application:emn.ua + XWapApplicationWvUa, // x-wap-application:wv.ua + }; + + // MMS + // https://www.openmobilealliance.org/release/MMS/V1_3-20110913-A/OMA-TS-MMS_ENC-V1_3-20110913-A.pdf, p64 + enum MmsFieldName : std::uint8_t + { + MmsBcc = 0x01, + MmsCc, + XMmsContentLocation, + MmsContentType, + MmsDate, + XMmsDeliveryReport, + XMmsDeliveryTime, + XMmsExpiry, + MmsFrom, + XMmsMessageClass, + MmsMessageID, + XMmsMessageType, + XMmsMMSVersion, + XMmsMessageSize, + XMmsPriority, + XMmsReadReport, + XMmsReportAllowed, + XMmsResponseStatus, + XMmsResponseText, + XMmsSenderVisibility, + XMmsStatus, + MmsSubject, + MmsTo, + XMmsTransactionId, + XMmsRetrieveStatus, + XMmsRetrieveText, + XMmsReadStatus, + XMmsReplyCharging, + XMmsReplyChargingDeadline, + XMmsReplyChargingID, + XMmsReplyChargingSize, + XMmsPreviouslySentBy, + XMmsPreviouslySentDate, + XMmsStore, + XMmsMMState, + XMmsMMFlags, + XMmsStoreStatus, + XMmsStoreStatusText, + XMmsStored, + XMmsAttributes, + XMmsTotals, + XMmsMboxTotals, + XMmsQuotas, + XMmsMboxQuotas, + XMmsMessageCount, + MmsContent, + XMmsStart, + MmsAdditionalheaders, + XMmsDistributionIndicator, + XMmsElementDescriptor, + XMmsLimit, + XMmsRecommendedRetrievalMode, + XMmsRecommendedRetrievalModeText, + XMmsStatusText, + XMmsApplicID, + XMmsReplyApplicID, + XMmsAuxApplicInfo, + XMmsContentClass, + XMmsDRMContent, + XMmsAdaptationAllowed, + XMmsReplaceID, + XMmsCancelID, + XMmsCancelStatus + }; + + enum MmsMessageType : std::uint8_t + { + MmsMSendReq = 0x00, // m-send-req + MmsMSendConf, // m-send-conf + MmsMNotificationInd, // m-notification-ind + MmsMNotifyrespInd, // m-notifyresp-ind + MmsMRetrieveConf, // m-retrieve-conf + MmsMAcknowledgeInd, // m-acknowledge-ind + MmsMDeliveryInd, // m-delivery-ind + MmsMReadRecInd, // m-read-rec-ind + MmsMReadOrigInd, // m-read-orig-ind + MmsMForwardReq, // m-forward-req + MmsMForwardComf, // m-forward-conf + MmsMMboxStoreReq, // m-mbox-store-req + MmsMMboxStoreConf, // m-mbox-store-conf + MmsMMboxViewReq, // m-mbox-view-req + MmsMMboxViewConf, // m-mbox-view-conf + MmsMMboxUploadReq, // m-mbox-upload-req + MmsMMboxUploadConf, // m-mbox-upload-conf + MmsMMboxDeleteReq, // m-mbox-delete-req + MmsMMboxDeleteConf, // m-mbox-delete-conf + MmsMMboxDescr, // m-mbox-descr + MmsMDeleteReq, // m-delete-req + MmsMDeleteConf, // m-delete-conf + MmsMCancelReq, // m-cancel-req + MmsMCancelConf // m-cancel-conf + }; + +} // namespace pdu::constants + +namespace pdu +{ + + // A range of characters + struct CharRange + { + using Iterator = std::string::const_iterator; + + CharRange() : m_begin(), m_end() + {} + + CharRange(Iterator begin, Iterator end) : m_begin(begin), m_end(end) + { + assert(m_begin <= m_end); + } + + Iterator begin() const + { + return m_begin; + } + Iterator end() const + { + return m_end; + } + bool empty() const + { + return m_begin == m_end; + } + std::uint32_t size() const + { + return m_end - m_begin; + } + + std::string str() const + { + return std::string(m_begin, m_end); + } + + friend bool operator==(CharRange const &rng, std::string const &str) + { + return rng.size() == str.size() && std::equal(rng.m_begin, rng.m_end, str.begin()); + } + + friend bool operator==(std::string const &str, CharRange const &rng) + { + return rng == str; + } + + friend bool operator==(CharRange const &rng, const char *cstr) + { + return cstr[rng.size()] == '\0' && std::equal(rng.m_begin, rng.m_end, cstr); + } + + friend bool operator==(const char *cstr, CharRange const &rng) + { + return rng == cstr; + } + + protected: + Iterator m_begin; + Iterator m_end; + }; + + // A buffer representing input octets + struct Octets : CharRange + { + Octets() = default; + + Octets(Iterator begin, Iterator end) : CharRange(begin, end) + {} + + bool peek(std::uint8_t &result) const + { + if (m_begin != m_end) { + result = *m_begin; + return true; + } + return false; + } + + bool next(std::uint8_t &result) + { + if (m_begin != m_end) { + result = *m_begin++; + return true; + } + return false; + } + + void ignore() + { + if (m_begin != m_end) { + ++m_begin; + } + } + + void ignore(std::uint32_t n) + { + m_begin = advancedByN(n); + } + + Octets subOctets(std::uint32_t n) + { + Iterator it = advancedByN(n); + Octets result(m_begin, it); + m_begin = it; + return result; + } + + private: + Iterator advancedByN(std::uint32_t n) const + { + return n < size() ? m_begin + n : m_end; + } + }; + + // A range representing text, excluding last \0 character + struct Text : CharRange + { + Text() = default; + + Text(Iterator begin, Iterator end) : CharRange(begin, end) + {} + }; + + // http://www.openmobilealliance.org/release/Browser_Protocol_Stack/V2_1-20110315-A/OMA-WAP-TS-WSP-V1_0-20110315-A.pdf, + // p79 + using DefaultValue = std::variant; + using ContentValue = DefaultValue; + using HeaderName = std::variant; + using HeaderValue = DefaultValue; + + // Representing known header names and header values + // Intended to be used with string literals + // If passing an object is needed replace const char* + // with std::string or std::string_view + struct KnownHeader + { + std::uint8_t id; + const char *idCStr; + }; + + template inline bool isKnown(NameOrValue const &nameOrValue, KnownHeader const &knownHeader) + { + return std::visit( + [&](auto const &val) { + using Val = utils::remove_cref_t; + if constexpr (std::is_same_v) { + return val == knownHeader.id; + } + if constexpr (std::is_same_v) { + return val == knownHeader.idCStr; + } + return false; + }, + nameOrValue); + } + + struct Parser + { + static constexpr std::uint8_t Zero = 0; + static constexpr std::uint8_t EndOfString = 0; + static constexpr std::uint8_t UintvarEscape = 31; + static constexpr std::uint8_t TextMin = 32; + static constexpr std::uint8_t TextMax = 127; + static constexpr std::uint8_t TextEscape = 127; + static constexpr std::uint8_t PageShift = 127; + + static constexpr std::uint32_t StringMaxLength = 1024; + + // http://www.openmobilealliance.org/release/Browser_Protocol_Stack/V2_1-20110315-A/OMA-WAP-TS-WSP-V1_0-20110315-A.pdf, + // s63 + static bool parseUintvar(Octets &octets, std::uint32_t &val) + { + val = 0x00000000; + bool isEndFound = false; + for (int i = 0; i < 5; ++i) { + std::uint8_t octet = 0x00; + if (!octets.next(octet)) { + return false; + } + val = (val << 7) | octet; + if ((octet & 0x80) == 0x00) { + isEndFound = true; + break; + } + } + return isEndFound; + } + + // http://www.openmobilealliance.org/release/Browser_Protocol_Stack/V2_1-20110315-A/OMA-WAP-TS-WSP-V1_0-20110315-A.pdf, + // s63 + static bool parseUint(Octets &octets, std::uint32_t &val, std::uint32_t length) + { + if (length > 4) { + return false; + } + val = 0x00000000; + for (std::uint32_t i = 0; i < length; ++i) { + std::uint8_t octet = 0x00; + if (!octets.next(octet)) { + return false; + } + val = (val << 8) | octet; + } + return true; + } + + // http://www.openmobilealliance.org/release/Browser_Protocol_Stack/V2_1-20110315-A/OMA-WAP-TS-WSP-V1_0-20110315-A.pdf, + // p79 + static bool parseTextN(Octets &octets, Text &text, std::uint32_t length) + { + if (length > StringMaxLength) { + return false; + } + if (length == 0) { + text = Text(octets.begin(), octets.begin()); + return true; + } + // Avoid including EoS character + Octets::Iterator const begin = octets.begin(); + octets.ignore(length - 1); + Octets::Iterator const end = octets.begin(); + std::uint8_t octet = 0x00; + if (!octets.next(octet)) { + return false; + } + if (octet == EndOfString) { + text = Text(begin, end); + } + else { + text = Text(begin, octets.begin()); + } + return true; + } + + // http://www.openmobilealliance.org/release/Browser_Protocol_Stack/V2_1-20110315-A/OMA-WAP-TS-WSP-V1_0-20110315-A.pdf, + // p79 + static bool parseText(Octets &octets, Text &text) + { + Octets::Iterator begin = octets.begin(); + std::uint8_t octet = 0x00; + if (!octets.next(octet)) { + return false; + } + if (octet == TextEscape) { + begin = octets.begin(); + } + // Avoid including EoS character + Octets::Iterator end = begin; + while (octet != EndOfString) { + end = octets.begin(); + if (!octets.next(octet)) { + return false; + } + } + text = Text(begin, end); + return true; + } + + // http://www.openmobilealliance.org/release/Browser_Protocol_Stack/V2_1-20110315-A/OMA-WAP-TS-WSP-V1_0-20110315-A.pdf, + // p79 + // Value may be: + // - decoded short integer + // - text + // - data as parsable octets range + struct DefaultPolicy + { + static bool data(Octets &octets, std::uint8_t lengthOctet, DefaultValue &value) + { + octets.ignore(); + std::uint32_t length = lengthOctet; + if (lengthOctet == UintvarEscape) { + if (!parseUintvar(octets, length)) { + return false; + } + } + if (octets.size() < length) { + return false; + } + value = octets.subOctets(length); + return true; + } + + template static bool text(Octets &octets, std::uint8_t charOctet, Value &value) + { + // Do not ignore the first character, it's part of the text + Text text(octets.begin(), octets.begin()); + if (!parseText(octets, text)) { + return false; + } + value = text; + return true; + } + + static bool shortInteger(Octets &octets, std::uint8_t uintOctet, DefaultValue &value) + { + octets.ignore(); + value = std::uint8_t(uintOctet & 0x7F); + return true; + } + }; + + // http://www.openmobilealliance.org/release/Browser_Protocol_Stack/V2_1-20110315-A/OMA-WAP-TS-WSP-V1_0-20110315-A.pdf, + // p81 + // Name may be: + // - not decoded short integer well-known field name [128, 255] + // - text token starting from octet [32, 126] + // - shortcut page shift delimiter [0, 31] + // - page shift delimter 127 (then header value is one octet page id and + // shouldn't be parsed with parseHeaderValue) + struct HeaderNamePolicy + { + static bool data(Octets &octets, std::uint8_t lengthOctet, HeaderName &value) + { + octets.ignore(); + value = lengthOctet; + return true; + } + + static bool text(Octets &octets, std::uint8_t charOctet, HeaderName &value) + { + if (charOctet == PageShift) { + octets.ignore(); + value = charOctet; + return true; + } + else { + return DefaultPolicy::text(octets, charOctet, value); + } + } + + static bool shortInteger(Octets &octets, std::uint8_t uintOctet, HeaderName &value) + { + octets.ignore(); + value = uintOctet; + return true; + } + }; + + // http://www.openmobilealliance.org/release/Browser_Protocol_Stack/V2_1-20110315-A/OMA-WAP-TS-WSP-V1_0-20110315-A.pdf, + // p81 + struct IgnoreHeaderValuePolicy + { + template static bool data(Octets &octets, std::uint8_t lengthOctet, Value &value) + { + octets.ignore(); + std::uint32_t length = lengthOctet; + if (lengthOctet == UintvarEscape) { + if (!parseUintvar(octets, length)) { + return false; + } + } + if (octets.size() < length) { + return false; + } + octets.ignore(length); + return true; + } + + template static bool text(Octets &octets, std::uint8_t charOctet, Value &value) + { + octets.ignore(); + while (charOctet != EndOfString) { + if (!octets.next(charOctet)) { + return false; + } + } + return true; + } + + template static bool shortInteger(Octets &octets, std::uint8_t uintOctet, Value &value) + { + octets.ignore(); + return true; + } + }; + + // http://www.openmobilealliance.org/release/Browser_Protocol_Stack/V2_1-20110315-A/OMA-WAP-TS-WSP-V1_0-20110315-A.pdf, + // p79 + template static bool parseGeneric(Octets &octets, Value &value) + { + std::uint8_t octet = 0x00; + if (!octets.peek(octet)) { + return false; + } + + if (octet < TextMin) { + return Policy::data(octets, octet, value); + } + else if (octet <= TextMax) { + return Policy::text(octets, octet, value); + } + else { // octet > TextMax + return Policy::shortInteger(octets, octet, value); + } + } + + // Value may be: + // - decoded short integer well-known field name + // - long integer well-known field name + // - text + // - integer/text type mashed together with optional parameter + static bool parseContentType(Octets &octets, ContentValue &value) + { + return parseGeneric(octets, value); + } + + // http://www.openmobilealliance.org/release/Browser_Protocol_Stack/V2_1-20110315-A/OMA-WAP-TS-WSP-V1_0-20110315-A.pdf, + // p81 + template + static bool parseHeader(Octets &octets, PageShiftPred pageShiftPred, NamePred namePred, ValuePred valuePred) + { + HeaderName name = std::uint8_t(0); + if (!parseGeneric(octets, name)) { + return false; + } + + // Distinguish between page shift and short integer + std::uint32_t nameUint = 0; + std::visit( + [&](auto const &n) { + using n_t = utils::remove_cref_t; + if constexpr (std::is_same_v) { + nameUint = n; + } + }, + name); + if (nameUint > 0) { + if (nameUint < PageShift) { + pageShiftPred(static_cast(nameUint)); + return true; + } + else if (nameUint == PageShift) { + std::uint8_t octet = 0; + if (!octets.next(octet)) { + return false; + } + pageShiftPred(octet); + return true; + } + else if (nameUint > TextMax && nameUint <= 0xFF) { + name = std::uint8_t(nameUint & 0x7F); // decode short integer + } + else if (nameUint > 0xFF) { + return false; // This should not happen + } + } + + if (!namePred(std::move(name))) { + int foo; + return parseGeneric(octets, foo); + } + + HeaderValue value = std::uint8_t(0); + if (!parseGeneric(octets, value)) { + return false; + } + + valuePred(std::move(value)); + return true; + } + + template + static bool parseHeaders(Octets &octets, + std::array const &headerNames, + std::array &headerValues) + { + std::array headersFound; + headersFound.fill(false); + std::size_t foundCount = 0; + + while (!octets.empty()) { + std::size_t nameId = N; + HeaderValue headerValue = std::uint8_t(0); + bool pageShift = false; + + auto pageShiftPred = [&](std::uint8_t page) { pageShift = true; }; + auto namePred = [&](auto &&name) { + for (std::size_t i = 0; i < N; ++i) { + if (headersFound[i]) { + // Header already found + continue; + } + else if (isKnown(name, headerNames[i])) { + headersFound[i] = true; + ++foundCount; + nameId = i; + return true; + } + } + return false; + }; + auto valuePred = [&](auto &&value) { + if (nameId < N) { + headerValues[nameId] = std::move(value); + } + }; + + if (!Parser::parseHeader(octets, pageShiftPred, namePred, valuePred)) { + return false; + } + + // NOTE: Page shifts are currently not supported + if (pageShift) { + return false; + } + + if (foundCount == N) { + break; + } + } + + return true; + } + }; + + // http://www.openmobilealliance.org/release/Browser_Protocol_Stack/V2_1-20110315-A/OMA-WAP-TS-WSP-V1_0-20110315-A.pdf, + // p64 + struct CommonFields + { + std::uint8_t id = 0x00; // set in connectionless mode + std::uint8_t type = 0x00; + }; + + // http://www.openmobilealliance.org/release/Browser_Protocol_Stack/V2_1-20110315-A/OMA-WAP-TS-WSP-V1_0-20110315-A.pdf, + // p70 + struct PushFields + { + std::uint32_t headersLen = 0; + ContentValue contentType = std::uint8_t{0}; + HeaderValue applicationId = std::uint8_t{0}; + }; + + // https://www.openmobilealliance.org/release/MMS/V1_3-20110913-A/OMA-TS-MMS_ENC-V1_3-20110913-A.pdf, p17 + struct MmsCommonFields + { + HeaderValue messageType = std::uint8_t{0}; + }; + + // https://www.openmobilealliance.org/release/MMS/V1_3-20110913-A/OMA-TS-MMS_ENC-V1_3-20110913-A.pdf, p21, p55, p53 + struct MmsNotificationFields + { + HeaderValue fromAddress = std::uint8_t{0}; + HeaderValue contentLocation = std::uint8_t{0}; + }; + + enum ConnectionMode + { + Connectionless, + Connectionmode + }; + + std::optional parse(std::string const &message, ConnectionMode connectionMode) + { + Octets octets(message.begin(), message.end()); + + // http://www.openmobilealliance.org/release/Browser_Protocol_Stack/V2_1-20110315-A/OMA-WAP-TS-WSP-V1_0-20110315-A.pdf, + // p64 + CommonFields commonFields; + if (connectionMode == Connectionless && !octets.next(commonFields.id)) { + return std::nullopt; + } + if (!octets.next(commonFields.type)) { + return std::nullopt; + } + + if (commonFields.type != constants::Push) { + return std::nullopt; // unsupported PDU type + } + + PushFields pushFields; + if (!Parser::parseUintvar(octets, pushFields.headersLen)) { + return std::nullopt; + } + Octets headersOctets = octets.subOctets(pushFields.headersLen); + if (!Parser::parseContentType(headersOctets, pushFields.contentType)) { + return std::nullopt; + } + + if (!isKnown(pushFields.contentType, + {constants::ApplicationVndWapMmsMessage, "application/vnd.wap.mms-message"})) { + return std::nullopt; // unsupported Push type + } + + // Push headers + { + std::array pushValues; + if (!Parser::parseHeaders( + headersOctets, + std::array{{{constants::XWapApplicationId, "X-Wap-Application-ID"}}}, + pushValues)) { + return std::nullopt; + } + pushFields.applicationId = std::move(pushValues[0]); + } + + if (!isKnown(pushFields.applicationId, {constants::XWapApplicationMmsUa, "x-wap-application:mms.ua"})) { + return std::nullopt; // unsupported WAP Push app + } + + // MMS headers + // https://www.openmobilealliance.org/release/MMS/V1_3-20110913-A/OMA-TS-MMS_ENC-V1_3-20110913-A.pdf, p21 + MmsCommonFields mmsCommonFields; + MmsNotificationFields mmsNotificationFields; + { + std::array mmsValues; + if (!Parser::parseHeaders( + octets, + std::array{{{constants::XMmsMessageType, "X-Mms-Message-Type"}, + {constants::MmsFrom, "From"}, + {constants::XMmsContentLocation, "X-Mms-Content-Location"}}}, + mmsValues)) { + return std::nullopt; + } + mmsCommonFields.messageType = std::move(mmsValues[0]); + mmsNotificationFields.fromAddress = std::move(mmsValues[1]); + mmsNotificationFields.contentLocation = std::move(mmsValues[2]); + } + + // https://www.openmobilealliance.org/release/MMS/V1_3-20110913-A/OMA-TS-MMS_ENC-V1_3-20110913-A.pdf, p56 + if (!isKnown(mmsCommonFields.messageType, {constants::MmsMNotificationInd, "m-notification-ind"})) { + return std::nullopt; // unsuported MMS type + } + + // https://www.openmobilealliance.org/release/MMS/V1_3-20110913-A/OMA-TS-MMS_ENC-V1_3-20110913-A.pdf, p55 + Text address; + if (std::holds_alternative(mmsNotificationFields.fromAddress)) { + Octets octets = std::get(mmsNotificationFields.fromAddress); + std::uint8_t token = 0; + if (!octets.next(token)) { + return std::nullopt; + } + constexpr std::uint8_t AddressPresentToken = 128; + if (token == AddressPresentToken) { + if (!Parser::parseTextN(octets, address, octets.size())) { + return std::nullopt; + } + } + } + + // https://www.openmobilealliance.org/release/MMS/V1_3-20110913-A/OMA-TS-MMS_ENC-V1_3-20110913-A.pdf, p53 + Text location; + if (std::holds_alternative(mmsNotificationFields.contentLocation)) { + location = std::get(mmsNotificationFields.contentLocation); + } + + return std::optional(MmsNotification(address.str(), location.str())); + } + + std::optional parse(std::string const &message) + { + return parse(message, Connectionless); + } + +} // namespace pdu diff --git a/module-services/service-cellular/Pdu.hpp b/module-services/service-cellular/Pdu.hpp new file mode 100644 index 0000000000000000000000000000000000000000..dff3adc0350bc59ee98660360601637c2f0ec832 --- /dev/null +++ b/module-services/service-cellular/Pdu.hpp @@ -0,0 +1,24 @@ +// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved. +// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md + +#pragma once + +#include +#include + +namespace pdu +{ + + struct MmsNotification + { + MmsNotification(std::string &&fromAddress, std::string &&contentLocation) + : fromAddress(std::move(fromAddress)), contentLocation(std::move(contentLocation)) + {} + + std::string fromAddress; + std::string contentLocation; + }; + + std::optional parse(std::string const &message); + +} // namespace pdu diff --git a/module-services/service-cellular/QMBNManager.cpp b/module-services/service-cellular/QMBNManager.cpp index b65cb52b2a64f5f1aedb23e7f4a379b4149952dd..2d4ce5621d05b0f3cfe100b8f76645aeb2431e94 100644 --- a/module-services/service-cellular/QMBNManager.cpp +++ b/module-services/service-cellular/QMBNManager.cpp @@ -1,8 +1,8 @@ -// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved. +// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved. // For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md #include "QMBNManager.hpp" -#include "Utils.hpp" +#include #include "response.hpp" #include #include @@ -77,7 +77,7 @@ at::Result::Code QMBNManager::setAutoSelect(at::response::qmbncfg::MBNAutoSelect at::Result::Code QMBNManager::writeNVByte(const std::string &nvfile, std::uint8_t byte) { - return writeNV(nvfile, utils::numToHex(byte)); + return writeNV(nvfile, utils::byteToHex(byte)); } at::Result::Code QMBNManager::writeNV(const std::string &nvfile, const std::string &hexvalue) diff --git a/module-services/service-cellular/ServiceCellular.cpp b/module-services/service-cellular/ServiceCellular.cpp index c7bb386348adb5c81c1b171431a96cc31f469e76..23c9543f98986c41ee9680251abf5f996066582a 100644 --- a/module-services/service-cellular/ServiceCellular.cpp +++ b/module-services/service-cellular/ServiceCellular.cpp @@ -6,6 +6,7 @@ #include "CellularUrcHandler.hpp" #include "service-cellular/CellularMessage.hpp" #include "service-cellular/CellularServiceAPI.hpp" +#include "service-cellular/Pdu.hpp" #include "service-cellular/ServiceCellular.hpp" #include "service-cellular/SignalStrength.hpp" #include "service-cellular/State.hpp" @@ -949,6 +950,11 @@ std::optional> ServiceCellular::identifyNotificati return urcHandler.getResponse(); } +std::string numberFromAddress(std::string const &address) +{ + return utils::endsWith(address, "/TYPE=PLMN") ? address.substr(0, address.size() - 10) : std::string(); +} + auto ServiceCellular::receiveSMS(std::string messageNumber) -> bool { auto retVal = true; @@ -1075,7 +1081,20 @@ auto ServiceCellular::receiveSMS(std::string messageNumber) -> bool if (messageParsed) { messageParsed = false; - const auto decodedMessage = UCS2(messageRawBody).toUTF8(); + UTF8 decodedMessage; + + const std::string decodedStr = utils::hexToBytes(messageRawBody); + const auto mmsNotificationOpt = pdu::parse(decodedStr); + if (mmsNotificationOpt) { + std::string number = numberFromAddress(mmsNotificationOpt->fromAddress); + // NOTE: number may be empty + decodedMessage = UTF8("[MMS]"); + receivedNumber = UTF8(number); + } + + if (decodedMessage.empty()) { + decodedMessage = UCS2(messageRawBody).toUTF8(); + } const auto record = createSMSRecord(decodedMessage, receivedNumber, messageDate); @@ -1141,7 +1160,8 @@ bool ServiceCellular::getIMSI(std::string &destination, bool fullNumber) LOG_ERROR("ServiceCellular::getIMSI failed."); return false; } -std::vector ServiceCellular::getNetworkInfo(void) + +std::vector ServiceCellular::getNetworkInfo() { std::vector data; auto channel = cmux->get(CellularMux::Channel::Commands); diff --git a/module-services/service-cellular/tests/CMakeLists.txt b/module-services/service-cellular/tests/CMakeLists.txt index 16fc7de8efacdfd769db38f385c8f4086cec3e51..0b744c193c76d3e756065f6b341487b96fabf8da 100644 --- a/module-services/service-cellular/tests/CMakeLists.txt +++ b/module-services/service-cellular/tests/CMakeLists.txt @@ -59,7 +59,7 @@ add_catch2_executable( add_gtest_executable( NAME - connection-manager + cellular-connection-manager SRCS unittest_connection-manager.cpp LIBS @@ -75,11 +75,22 @@ add_catch2_executable( LIBS module-cellular ) + add_catch2_executable( NAME - DTMFCode + cellular-DTMFCode SRCS unittest_DTMFCode.cpp LIBS module-cellular ) + +add_catch2_executable( + NAME + cellular-Pdu + SRCS + unittest_Pdu.cpp + LIBS + module-cellular + module-utils +) diff --git a/module-services/service-cellular/tests/unittest_Pdu.cpp b/module-services/service-cellular/tests/unittest_Pdu.cpp new file mode 100644 index 0000000000000000000000000000000000000000..d89edae67c7245340dd45d748f60dd504a0ab02a --- /dev/null +++ b/module-services/service-cellular/tests/unittest_Pdu.cpp @@ -0,0 +1,156 @@ +// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved. +// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md + +#include +#include +#include + +TEST_CASE("Pdu parser test") +{ + SECTION("Valid MMS Notification Play") + { + const char *hexMsg = + "0006226170706C69636174696F6E2F766E642E7761702E6D6D732D6D65737361676500AF848C82983132333435363738008D918A80" + "8E0302BF5F8805810303F47F83687474703A2F2F6D6D73632E706C61792E706C2F6D6D732F776170656E632F313233343536373800" + "8918802B34383938373635343332312F545950453D504C4D4E00"; + std::string byteMsg = utils::hexToBytes(hexMsg); + auto const mmsNotifOpt = pdu::parse(byteMsg); + REQUIRE(mmsNotifOpt->fromAddress == "+48987654321/TYPE=PLMN"); + REQUIRE(mmsNotifOpt->contentLocation == "http://mmsc.play.pl/mms/wapenc/12345678"); + } + + SECTION("Valid MMS Notification T-Mobile") + { + const char *hexMsg = + "C50603BEAF848C829831383261316237323931643835663230644070322E6D6D73632E742D6D6F62696C652E706C008D918A80" + "8E0285C288048102A8BE83687474703A2F2F31302E3234322E3234332E34303A393830302F6D6D313F69643D32323038313531" + "3832613162373238313932366630356262008918802B34383530353035303530352F545950453D504C4D4E009600"; + std::string byteMsg = utils::hexToBytes(hexMsg); + auto const mmsNotifOpt = pdu::parse(byteMsg); + REQUIRE(mmsNotifOpt->fromAddress == "+48505050505/TYPE=PLMN"); + REQUIRE(mmsNotifOpt->contentLocation == "http://10.242.243.40:9800/mm1?id=220815182a1b7281926f05bb"); + } + + SECTION("Incomplete MMS Notification T-Mobile") + { + // Incomplete From MMS header data value + const char *hexMsg = + "C50603BEAF848C829831383261316237323931643835663230644070322E6D6D73632E742D6D6F62696C652E706C008D918A80" + "8E0285C288048102A8BE83687474703A2F2F31302E3234322E3234332E34303A393830302F6D6D313F69643D32323038313531" + "3832613162373238313932366630356262008918802B34383530"; + std::string byteMsg = utils::hexToBytes(hexMsg); + auto const mmsNotifOpt = pdu::parse(byteMsg); + REQUIRE(bool(mmsNotifOpt) == false); + } + + SECTION("Valid MMS Notification T-Mobile 2") + { + const char *hexMsg = + "600603BEAF848C82983138326135373163376432353265383033644070312E6D6D73632E742D6D6F62696C652E706C008D918A" + "808E0302211C88058103093A7F83687474703A2F2F31302E3230352E34332E34303A393830302F6D6D313F69643D3232303831" + "36313832613537316335343632643638386266008918802B34383238373238373238372F545950453D504C4D4E009600"; + std::string byteMsg = utils::hexToBytes(hexMsg); + auto const mmsNotifOpt = pdu::parse(byteMsg); + REQUIRE(mmsNotifOpt->fromAddress == "+48287287287/TYPE=PLMN"); + REQUIRE(mmsNotifOpt->contentLocation == "http://10.205.43.40:9800/mm1?id=220816182a571c5462d688bf"); + } + + SECTION("Valid MMS Notification Plus") + { + const char *hexMsg = + "000607BE8DE9AF84B4808C8298303130322D67616B32346E326E643679393230008D908918802B34383738323738323738322F" + "545950453D504C4D4E008A808E0302211C88058103093A7E83687474703A2F2F6D6D733372783A383030322F303130322D6761" + "6B32346E326E64367939323000"; + std::string byteMsg = utils::hexToBytes(hexMsg); + auto const mmsNotifOpt = pdu::parse(byteMsg); + REQUIRE(mmsNotifOpt->fromAddress == "+48782782782/TYPE=PLMN"); + REQUIRE(mmsNotifOpt->contentLocation == "http://mms3rx:8002/0102-gak24n2nd6y920"); + } + + SECTION("Valid MMS Notification Orange") + { + const char *hexMsg = + "000607BEAF848DDBB4818C8298416263646566676869008D928918802B34383132333435363738392F545950453D504C4D4E008681" + "8A808E0285AE88048102A8BF83687474703A2F2F67656F6D323A383030322F6D6D73632F633F41626364656667686900"; + std::string byteMsg = utils::hexToBytes(hexMsg); + auto const mmsNotifOpt = pdu::parse(byteMsg); + REQUIRE(mmsNotifOpt->fromAddress == "+48123456789/TYPE=PLMN"); + REQUIRE(mmsNotifOpt->contentLocation == "http://geom2:8002/mmsc/c?Abcdefghi"); + } + + SECTION("Valid MMS Notification without From field") + { + const char *hexMsg = "000607BEAF848DDBB4818C8298416263646566676869008D9286818A808E0285AE88048102A8BF83687474703" + "A2F2F67656F6D323A383030322F6D6D73632F633F41626364656667686900"; + std::string byteMsg = utils::hexToBytes(hexMsg); + auto const mmsNotifOpt = pdu::parse(byteMsg); + REQUIRE(mmsNotifOpt->fromAddress == ""); + REQUIRE(mmsNotifOpt->contentLocation == "http://geom2:8002/mmsc/c?Abcdefghi"); + } + + SECTION("PDU Type != Push") + { + const char *hexMsg = + "000507BEAF848DDBB4818C8298416263646566676869008D928918802B34383132333435363738392F545950453D504C4D4E008681" + "8A808E0285AE88048102A8BF83687474703A2F2F67656F6D323A383030322F6D6D73632F633F41626364656667686900"; + std::string byteMsg = utils::hexToBytes(hexMsg); + auto const mmsNotifOpt = pdu::parse(byteMsg); + REQUIRE(bool(mmsNotifOpt) == false); + } + + SECTION("X-Wap-Application-ID != x-wap-application:mms.ua") + { + const char *hexMsg = + "000607BEAF838DDBB4818C8298416263646566676869008D928918802B34383132333435363738392F545950453D504C4D4E008681" + "8A808E0285AE88048102A8BF83687474703A2F2F67656F6D323A383030322F6D6D73632F633F41626364656667686900"; + std::string byteMsg = utils::hexToBytes(hexMsg); + auto const mmsNotifOpt = pdu::parse(byteMsg); + REQUIRE(bool(mmsNotifOpt) == false); + } + + SECTION("Missing X-Wap-Application-ID") + { + const char *hexMsg = + "000605BE8DDBB4818C8298416263646566676869008D928918802B34383132333435363738392F545950453D504C4D4E0086818A80" + "8E0285AE88048102A8BF83687474703A2F2F67656F6D323A383030322F6D6D73632F633F41626364656667686900"; + std::string byteMsg = utils::hexToBytes(hexMsg); + auto const mmsNotifOpt = pdu::parse(byteMsg); + REQUIRE(bool(mmsNotifOpt) == false); + } + + SECTION("X-Mms-Message-Type != m-notification-ind") + { + const char *hexMsg = + "000607BEAF848DDBB4818C8198416263646566676869008D928918802B34383132333435363738392F545950453D504C4D4E008681" + "8A808E0285AE88048102A8BF83687474703A2F2F67656F6D323A383030322F6D6D73632F633F41626364656667686900"; + std::string byteMsg = utils::hexToBytes(hexMsg); + auto const mmsNotifOpt = pdu::parse(byteMsg); + REQUIRE(bool(mmsNotifOpt) == false); + } + + SECTION("Missing X-Mms-Message-Type") + { + const char *hexMsg = + "000607BEAF848DDBB4818C8198416263646566676869008D928918802B34383132333435363738392F545950453D504C4D4E008681" + "8A808E0285AE88048102A8BF83687474703A2F2F67656F6D323A383030322F6D6D73632F633F41626364656667686900"; + std::string byteMsg = utils::hexToBytes(hexMsg); + auto const mmsNotifOpt = pdu::parse(byteMsg); + REQUIRE(bool(mmsNotifOpt) == false); + } + + SECTION("Missing PDU Content") + { + const char *hexMsg = "000608BEAF848DDBB481"; + std::string byteMsg = utils::hexToBytes(hexMsg); + auto const mmsNotifOpt = pdu::parse(byteMsg); + REQUIRE(bool(mmsNotifOpt) == false); + } + + SECTION("Invalid PDU header length") + { + const char *hexMsg = "000607BEAF848DDBB481"; + std::string byteMsg = utils::hexToBytes(hexMsg); + auto const mmsNotifOpt = pdu::parse(byteMsg); + REQUIRE(bool(mmsNotifOpt) == false); + } +} diff --git a/module-utils/utility/Utils.cpp b/module-utils/utility/Utils.cpp index 9c72a9b827dea72e792435f43459ebeae0869359..5c1f29c29d2d2574495e92df4045ee3486e5871e 100644 --- a/module-utils/utility/Utils.cpp +++ b/module-utils/utility/Utils.cpp @@ -61,26 +61,6 @@ namespace utils::filesystem namespace utils { - std::vector hexToBytes(const std::string &hex) - { - std::vector bytes; - - for (unsigned int i = 0; i < hex.length(); i += 2) { - std::string byteString = hex.substr(i, 2); - std::uint8_t byte = std::stoull(byteString.c_str(), nullptr, 16); - bytes.push_back(byte); - } - return bytes; - } - std::string bytesToHex(const std::vector &bytes) - { - std::stringstream s; - s.fill('0'); - for (auto const &b : bytes) - s << std::setw(2) << std::hex << (unsigned short)b; - return s.str(); - } - std::string generateRandomId(std::size_t length) noexcept { if (!length) diff --git a/module-utils/utility/Utils.hpp b/module-utils/utility/Utils.hpp index 8274f7c80337f4e27303a53361e29ff65558412e..af65cd254fe40094d8f734de2365e1a32fbde6f1 100644 --- a/module-utils/utility/Utils.hpp +++ b/module-utils/utility/Utils.hpp @@ -13,23 +13,67 @@ #include #include #include +#include #include namespace utils { inline constexpr auto WHITESPACE = " \n\r\t\f\v"; - constexpr unsigned int secondsInMinute = - std::chrono::duration_cast(std::chrono::minutes(1)).count(); + constexpr unsigned int secondsInMinute = 60; - std::string bytesToHex(const std::vector &bytes); - std::vector hexToBytes(const std::string &hex); + template inline constexpr bool is_byte_v = std::is_integral_v && sizeof(T) == sizeof(std::uint8_t); - template std::string numToHex(T c) + template using remove_cref_t = std::remove_const_t>; + + // NOTE: With short string optimization storing one byte hex in std::string is probably fine + + template , int> = 0> inline char halfByteToHex(T c) + { + return c < 10 ? '0' + c : 'a' + (c - 10); + } + + template , int> = 0> inline std::string byteToHex(T c) + { + return {halfByteToHex((c & 0xF0) >> 4), halfByteToHex(c & 0x0F)}; + } + + template , int> = 0> inline T halfHexToByte(char c) + { + if (c >= '0' && c <= '9') { + return c - '0'; + } + else if (c >= 'A' && c <= 'F') { + return c - 'A' + 10; + } + else if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } + throw std::invalid_argument("Unexpected hex digit"); + return 0; + } + + template , + std::enable_if_t, int> = 0> + inline Bytes hexToBytes(const std::string &hex) + { + using Value = typename Bytes::value_type; + const std::size_t bytesCount = (hex.size() + 1) / 2; + const std::size_t fullBytesCount = hex.size() / 2; + Bytes bytes(bytesCount, Value(0)); + for (std::size_t i = 0; i < fullBytesCount; ++i) { + const std::size_t offset = i * 2; + bytes[i] = (halfHexToByte(hex[offset]) << 4) | halfHexToByte(hex[offset + 1]); + } + if (fullBytesCount < bytesCount) { + bytes.back() = halfHexToByte(hex.back()); + } + return bytes; + } + + inline bool endsWith(std::string const &str, std::string const &suffix) { - std::stringstream s; - s.fill('0'); - s << std::setw(sizeof(T) * 2) << std::hex << static_cast(c); - return s.str(); + return str.size() >= suffix.size() && + std::equal(str.begin() + (str.size() - suffix.size()), str.end(), suffix.begin()); } static inline std::string removeNewLines(const std::string &s) @@ -78,7 +122,7 @@ namespace utils return base; } - template [[nodiscard]] std::string to_string(T t) + template [[nodiscard]] inline std::string to_string(T t) { return std::to_string(t); } diff --git a/module-utils/utility/tests/unittest_utils.cpp b/module-utils/utility/tests/unittest_utils.cpp index e200da81397a6b55a0d6c7059bc5ae8df39b867e..d5b3e51b32331e2fd6a5fc230b2237a37e8fdb67 100644 --- a/module-utils/utility/tests/unittest_utils.cpp +++ b/module-utils/utility/tests/unittest_utils.cpp @@ -496,19 +496,66 @@ TEST_CASE("Hex to bytes") REQUIRE(b[3] == 0xEF); } + SECTION("Odd number of digits") + { + auto b = utils::hexToBytes("AbcDe"); + REQUIRE(b.size() == 3); + REQUIRE(b[0] == 0xAB); + REQUIRE(b[1] == 0xCD); + REQUIRE(b[2] == 0xE); + } + SECTION("Out of hex") { REQUIRE_THROWS_AS(utils::hexToBytes("deAdbEZZ"), std::invalid_argument); } } -TEST_CASE("Bytes to hex") +TEST_CASE("Byte to hex") +{ + SECTION("One digit") + { + auto ret = utils::byteToHex(std::uint8_t(0xC)); + REQUIRE((ret == "0c")); + } + + SECTION("Two digits") + { + auto ret = utils::byteToHex(std::uint8_t(0x3F)); + REQUIRE((ret == "3f")); + } +} + +TEST_CASE("Ends with") { - SECTION("Vector of bytes") + SECTION("Empty string") + { + REQUIRE((utils::endsWith("", "abc") == false)); + } + + SECTION("Empty suffix") + { + REQUIRE((utils::endsWith("abc", "") == true)); + } + + SECTION("Both empty") + { + REQUIRE((utils::endsWith("", "") == true)); + } + + SECTION("No") + { + REQUIRE((utils::endsWith("Abcde", "def") == false)); + } + + SECTION("Yes") + { + REQUIRE((utils::endsWith("Abcde", "de") == true)); + } + + SECTION("Equal") { - std::vector vb = {1, 2, 3, 4, 0xFF}; - auto ret = utils::bytesToHex(vb); - REQUIRE((ret == "01020304ff")); + REQUIRE((utils::endsWith("Abc", "Abc") == true)); } } diff --git a/pure_changelog.md b/pure_changelog.md index 436139b34c78f8425fc9200ef5ec956c05fc1ca2..355dcd2efba07ccc236045ae28febbe4e0e714e1 100644 --- a/pure_changelog.md +++ b/pure_changelog.md @@ -13,6 +13,7 @@ * Fixed turning on loudspeaker before outgoing call is answered * Fixed PLAY label translation in German * Fixed USB connection/disconnection detection +* Added basic MMS handling ## [1.3.0 2022-08-04]