~aleteoryx/muditaos

2edcb7e330d6384eed94d768c2e85208e4c54333 — Adam Wulkiewicz 3 years ago 44986b9
[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
M module-services/service-cellular/CMakeLists.txt => module-services/service-cellular/CMakeLists.txt +1 -0
@@ 23,6 23,7 @@ set(SOURCES
    SignalStrength.cpp
    NetworkSettings.cpp
    PacketData.cpp
    Pdu.cpp
    QMBNManager.cpp
    RequestFactory.cpp
    CellularRequestHandler.cpp

A module-services/service-cellular/Pdu.cpp => module-services/service-cellular/Pdu.cpp +982 -0
@@ 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 <Utils.hpp>

#include <algorithm>
#include <array>
#include <cassert>
#include <cstdint>
#include <variant>

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<std::uint8_t, Text, Octets>;
    using ContentValue = DefaultValue;
    using HeaderName   = std::variant<std::uint8_t, Text>;
    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 <typename NameOrValue> inline bool isKnown(NameOrValue const &nameOrValue, KnownHeader const &knownHeader)
    {
        return std::visit(
            [&](auto const &val) {
                using Val = utils::remove_cref_t<decltype(val)>;
                if constexpr (std::is_same_v<Val, std::uint8_t>) {
                    return val == knownHeader.id;
                }
                if constexpr (std::is_same_v<Val, Text>) {
                    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 <typename Value> 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 <typename Value> 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 <typename Value> 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 <typename Value> 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 <typename Policy, typename Value> 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<DefaultPolicy>(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 <typename PageShiftPred, typename NamePred, typename ValuePred>
        static bool parseHeader(Octets &octets, PageShiftPred pageShiftPred, NamePred namePred, ValuePred valuePred)
        {
            HeaderName name = std::uint8_t(0);
            if (!parseGeneric<HeaderNamePolicy>(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<decltype(n)>;
                    if constexpr (std::is_same_v<n_t, std::uint8_t>) {
                        nameUint = n;
                    }
                },
                name);
            if (nameUint > 0) {
                if (nameUint < PageShift) {
                    pageShiftPred(static_cast<std::uint8_t>(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<IgnoreHeaderValuePolicy>(octets, foo);
            }

            HeaderValue value = std::uint8_t(0);
            if (!parseGeneric<DefaultPolicy>(octets, value)) {
                return false;
            }

            valuePred(std::move(value));
            return true;
        }

        template <std::size_t N>
        static bool parseHeaders(Octets &octets,
                                 std::array<KnownHeader, N> const &headerNames,
                                 std::array<HeaderValue, N> &headerValues)
        {
            std::array<bool, N> 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<MmsNotification> 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<HeaderValue, 1> pushValues;
            if (!Parser::parseHeaders(
                    headersOctets,
                    std::array<KnownHeader, 1>{{{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<HeaderValue, 3> mmsValues;
            if (!Parser::parseHeaders(
                    octets,
                    std::array<KnownHeader, 3>{{{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<Octets>(mmsNotificationFields.fromAddress)) {
            Octets octets      = std::get<Octets>(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<Text>(mmsNotificationFields.contentLocation)) {
            location = std::get<Text>(mmsNotificationFields.contentLocation);
        }

        return std::optional<MmsNotification>(MmsNotification(address.str(), location.str()));
    }

    std::optional<MmsNotification> parse(std::string const &message)
    {
        return parse(message, Connectionless);
    }

} // namespace pdu

A module-services/service-cellular/Pdu.hpp => module-services/service-cellular/Pdu.hpp +24 -0
@@ 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 <optional>
#include <string>

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<MmsNotification> parse(std::string const &message);

} // namespace pdu

M module-services/service-cellular/QMBNManager.cpp => module-services/service-cellular/QMBNManager.cpp +3 -3
@@ 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 <Utils.hpp>
#include "response.hpp"
#include <at/ATFactory.hpp>
#include <modem/mux/DLCChannel.h>


@@ 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<std::uint8_t>(byte));
    return writeNV(nvfile, utils::byteToHex<std::uint8_t>(byte));
}

at::Result::Code QMBNManager::writeNV(const std::string &nvfile, const std::string &hexvalue)

M module-services/service-cellular/ServiceCellular.cpp => module-services/service-cellular/ServiceCellular.cpp +22 -2
@@ 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<std::shared_ptr<sys::Message>> 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<std::string>(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<std::string> ServiceCellular::getNetworkInfo(void)

std::vector<std::string> ServiceCellular::getNetworkInfo()
{
    std::vector<std::string> data;
    auto channel = cmux->get(CellularMux::Channel::Commands);

M module-services/service-cellular/tests/CMakeLists.txt => module-services/service-cellular/tests/CMakeLists.txt +13 -2
@@ 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
)

A module-services/service-cellular/tests/unittest_Pdu.cpp => module-services/service-cellular/tests/unittest_Pdu.cpp +156 -0
@@ 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 <catch2/catch.hpp>
#include <service-cellular/Pdu.hpp>
#include <Utils.hpp>

TEST_CASE("Pdu parser test")
{
    SECTION("Valid MMS Notification Play")
    {
        const char *hexMsg =
            "0006226170706C69636174696F6E2F766E642E7761702E6D6D732D6D65737361676500AF848C82983132333435363738008D918A80"
            "8E0302BF5F8805810303F47F83687474703A2F2F6D6D73632E706C61792E706C2F6D6D732F776170656E632F313233343536373800"
            "8918802B34383938373635343332312F545950453D504C4D4E00";
        std::string byteMsg    = utils::hexToBytes<std::string>(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<std::string>(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<std::string>(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<std::string>(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<std::string>(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<std::string>(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<std::string>(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<std::string>(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<std::string>(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<std::string>(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<std::string>(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<std::string>(hexMsg);
        auto const mmsNotifOpt = pdu::parse(byteMsg);
        REQUIRE(bool(mmsNotifOpt) == false);
    }

    SECTION("Missing PDU Content")
    {
        const char *hexMsg     = "000608BEAF848DDBB481";
        std::string byteMsg    = utils::hexToBytes<std::string>(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<std::string>(hexMsg);
        auto const mmsNotifOpt = pdu::parse(byteMsg);
        REQUIRE(bool(mmsNotifOpt) == false);
    }
}

M module-utils/utility/Utils.cpp => module-utils/utility/Utils.cpp +0 -20
@@ 61,26 61,6 @@ namespace utils::filesystem

namespace utils
{
    std::vector<std::uint8_t> hexToBytes(const std::string &hex)
    {
        std::vector<std::uint8_t> 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<std::uint8_t> &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)

M module-utils/utility/Utils.hpp => module-utils/utility/Utils.hpp +54 -10
@@ 13,23 13,67 @@
#include <chrono>
#include <random>
#include <tuple>
#include <type_traits>
#include <functional>

namespace utils
{
    inline constexpr auto WHITESPACE = " \n\r\t\f\v";
    constexpr unsigned int secondsInMinute =
        std::chrono::duration_cast<std::chrono::seconds>(std::chrono::minutes(1)).count();
    constexpr unsigned int secondsInMinute = 60;

    std::string bytesToHex(const std::vector<std::uint8_t> &bytes);
    std::vector<std::uint8_t> hexToBytes(const std::string &hex);
    template <typename T> inline constexpr bool is_byte_v = std::is_integral_v<T> && sizeof(T) == sizeof(std::uint8_t);

    template <typename T> std::string numToHex(T c)
    template <typename T> using remove_cref_t = std::remove_const_t<std::remove_reference_t<T>>;

    // NOTE: With short string optimization storing one byte hex in std::string is probably fine

    template <typename T, std::enable_if_t<is_byte_v<T>, int> = 0> inline char halfByteToHex(T c)
    {
        return c < 10 ? '0' + c : 'a' + (c - 10);
    }

    template <typename T, std::enable_if_t<is_byte_v<T>, int> = 0> inline std::string byteToHex(T c)
    {
        return {halfByteToHex<std::uint8_t>((c & 0xF0) >> 4), halfByteToHex<std::uint8_t>(c & 0x0F)};
    }

    template <typename T = std::uint8_t, std::enable_if_t<is_byte_v<T>, 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 <typename Bytes                                               = std::vector<std::uint8_t>,
              std::enable_if_t<is_byte_v<typename Bytes::value_type>, 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<Value>(hex[offset]) << 4) | halfHexToByte<Value>(hex[offset + 1]);
        }
        if (fullBytesCount < bytesCount) {
            bytes.back() = halfHexToByte<Value>(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<unsigned long long>(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 <typename T> [[nodiscard]] std::string to_string(T t)
    template <typename T> [[nodiscard]] inline std::string to_string(T t)
    {
        return std::to_string(t);
    }

M module-utils/utility/tests/unittest_utils.cpp => module-utils/utility/tests/unittest_utils.cpp +52 -5
@@ 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<std::uint8_t> vb = {1, 2, 3, 4, 0xFF};
        auto ret                     = utils::bytesToHex(vb);
        REQUIRE((ret == "01020304ff"));
        REQUIRE((utils::endsWith("Abc", "Abc") == true));
    }
}


M pure_changelog.md => pure_changelog.md +1 -0
@@ 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]