/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2022 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 7 End-User License Agreement and JUCE Privacy Policy. End User License Agreement: www.juce.com/juce-7-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce::midi_ci { std::optional Parser::parse (Span message, Status* status) { const auto setStatus = [&] (Status s) { if (status != nullptr) *status = s; }; setStatus (Status::noError); Message::Generic generic; if (! detail::Marshalling::Reader { message } (generic)) { // Got a full sysex message, but it didn't contain a well-formed header. setStatus (Status::malformed); return {}; } if ((generic.header.version & std::byte { 0x70 }) != std::byte{}) { setStatus (Status::reservedVersion); return Message::Parsed { generic.header, std::monostate{} }; } const auto index = (uint8_t) generic.header.category; constexpr auto tables = detail::MessageTypeUtils::getTables(); const auto processFunction = tables.parsers[index]; return Message::Parsed { generic.header, processFunction (generic, status) }; } std::optional Parser::parse (const MUID ourMUID, Span message, Status* status) { const auto setStatus = [&] (Status s) { if (status != nullptr) *status = s; }; setStatus (Status::noError); if (const auto parsed = parse (message, status)) { if (parsed->header.destination != MUID::getBroadcast() && parsed->header.destination != ourMUID) setStatus (Status::mismatchedMUID); else if (parsed->header.source == ourMUID) setStatus (Status::collidingMUID); else if ((parsed->header.version & std::byte { 0x70 }) != std::byte{}) setStatus (Status::reservedVersion); return parsed; } return {}; } class DescriptionVisitor : public detail::MessageTypeUtils::MessageVisitor { public: DescriptionVisitor (const Message::Parsed* m, String* str) : msg (m), result (str) {} void visit (const std::monostate&) const override { *result = "!! Unrecognised !!"; } void visit (const Message::Discovery& body) const override { visitImpl (body); } void visit (const Message::DiscoveryResponse& body) const override { visitImpl (body); } void visit (const Message::InvalidateMUID& body) const override { visitImpl (body); } void visit (const Message::EndpointInquiry& body) const override { visitImpl (body); } void visit (const Message::EndpointInquiryResponse& body) const override { visitImpl (body); } void visit (const Message::ACK& body) const override { visitImpl (body); } void visit (const Message::NAK& body) const override { visitImpl (body); } void visit (const Message::ProfileInquiry& body) const override { visitImpl (body); } void visit (const Message::ProfileInquiryResponse& body) const override { visitImpl (body); } void visit (const Message::ProfileAdded& body) const override { visitImpl (body); } void visit (const Message::ProfileRemoved& body) const override { visitImpl (body); } void visit (const Message::ProfileDetails& body) const override { visitImpl (body); } void visit (const Message::ProfileDetailsResponse& body) const override { visitImpl (body); } void visit (const Message::ProfileOn& body) const override { visitImpl (body); } void visit (const Message::ProfileOff& body) const override { visitImpl (body); } void visit (const Message::ProfileEnabledReport& body) const override { visitImpl (body); } void visit (const Message::ProfileDisabledReport& body) const override { visitImpl (body); } void visit (const Message::ProfileSpecificData& body) const override { visitImpl (body); } void visit (const Message::PropertyExchangeCapabilities& body) const override { visitImpl (body); } void visit (const Message::PropertyExchangeCapabilitiesResponse& body) const override { visitImpl (body); } void visit (const Message::PropertyGetData& body) const override { visitImpl (body); } void visit (const Message::PropertyGetDataResponse& body) const override { visitImpl (body); } void visit (const Message::PropertySetData& body) const override { visitImpl (body); } void visit (const Message::PropertySetDataResponse& body) const override { visitImpl (body); } void visit (const Message::PropertySubscribe& body) const override { visitImpl (body); } void visit (const Message::PropertySubscribeResponse& body) const override { visitImpl (body); } void visit (const Message::PropertyNotify& body) const override { visitImpl (body); } void visit (const Message::ProcessInquiry& body) const override { visitImpl (body); } void visit (const Message::ProcessInquiryResponse& body) const override { visitImpl (body); } void visit (const Message::ProcessMidiMessageReport& body) const override { visitImpl (body); } void visit (const Message::ProcessMidiMessageReportResponse& body) const override { visitImpl (body); } void visit (const Message::ProcessEndMidiMessageReport& body) const override { visitImpl (body); } private: static const char* getDescription (const Message::Discovery&) { return "Discovery"; } static const char* getDescription (const Message::DiscoveryResponse&) { return "Discovery Response"; } static const char* getDescription (const Message::InvalidateMUID&) { return "Invalidate MUID"; } static const char* getDescription (const Message::EndpointInquiry&) { return "Endpoint"; } static const char* getDescription (const Message::EndpointInquiryResponse&) { return "Endpoint Response"; } static const char* getDescription (const Message::ACK&) { return "ACK"; } static const char* getDescription (const Message::NAK&) { return "NAK"; } static const char* getDescription (const Message::ProfileInquiry&) { return "Profile Inquiry"; } static const char* getDescription (const Message::ProfileInquiryResponse&) { return "Profile Inquiry Response"; } static const char* getDescription (const Message::ProfileAdded&) { return "Profile Added"; } static const char* getDescription (const Message::ProfileRemoved&) { return "Profile Removed"; } static const char* getDescription (const Message::ProfileDetails&) { return "Profile Details"; } static const char* getDescription (const Message::ProfileDetailsResponse&) { return "Profile Details Response"; } static const char* getDescription (const Message::ProfileOn&) { return "Profile On"; } static const char* getDescription (const Message::ProfileOff&) { return "Profile Off"; } static const char* getDescription (const Message::ProfileEnabledReport&) { return "Profile Enabled Report"; } static const char* getDescription (const Message::ProfileDisabledReport&) { return "Profile Disabled Report"; } static const char* getDescription (const Message::ProfileSpecificData&) { return "Profile Specific Data"; } static const char* getDescription (const Message::PropertyExchangeCapabilities&) { return "Property Exchange Capabilities"; } static const char* getDescription (const Message::PropertyExchangeCapabilitiesResponse&) { return "Property Exchange Capabilities Response"; } static const char* getDescription (const Message::PropertyGetData&) { return "Property Get Data"; } static const char* getDescription (const Message::PropertyGetDataResponse&) { return "Property Get Data Response"; } static const char* getDescription (const Message::PropertySetData&) { return "Property Set Data"; } static const char* getDescription (const Message::PropertySetDataResponse&) { return "Property Set Data Response"; } static const char* getDescription (const Message::PropertySubscribe&) { return "Property Subscribe"; } static const char* getDescription (const Message::PropertySubscribeResponse&) { return "Property Subscribe Response"; } static const char* getDescription (const Message::PropertyNotify&) { return "Property Notify"; } static const char* getDescription (const Message::ProcessInquiry&) { return "Process Inquiry"; } static const char* getDescription (const Message::ProcessInquiryResponse&) { return "Process Inquiry Response"; } static const char* getDescription (const Message::ProcessMidiMessageReport&) { return "Process Midi Message Report"; } static const char* getDescription (const Message::ProcessMidiMessageReportResponse&) { return "Process Midi Message Report Response"; } static const char* getDescription (const Message::ProcessEndMidiMessageReport&) { return "Process End Midi Message Report"; } template void visitImpl (const Body& body) const { const auto opts = ToVarOptions{}.withExplicitVersion ((int) msg->header.version) .withVersionIncluded (false); const auto json = ToVar::convert (body, opts); if (json.has_value()) *result = String (getDescription (body)) + ": " + JSON::toString (*json, JSON::FormatOptions{}.withSpacing (JSON::Spacing::none)); } const Message::Parsed* msg = nullptr; String* result = nullptr; }; String Parser::getMessageDescription (const Message::Parsed& message) { String result; detail::MessageTypeUtils::visit (message, DescriptionVisitor { &message, &result }); return result; } //============================================================================== //============================================================================== #if JUCE_UNIT_TESTS class ParserTests : public UnitTest { public: ParserTests() : UnitTest ("Parser", UnitTestCategories::midi) {} void runTest() override { auto random = getRandom(); beginTest ("Sending an empty message does nothing"); { const auto parsed = Parser::parse (MUID::makeRandom (random), {}); expect (parsed == std::nullopt); } beginTest ("Sending a garbage message does nothing"); { const std::vector bytes (128, std::byte { 0x70 }); const auto parsed = Parser::parse (MUID::makeRandom (random), bytes); expect (parsed == std::nullopt); } beginTest ("Sending a message with truncated body produces a malformed status"); { constexpr auto version1 = 0x01; const auto truncatedV1 = makeByteArray (0x7e, /* to function block */ 0x7f, /* midi CI */ 0x0d, /* discovery message */ 0x70, /* version */ version1, /* source MUID */ 0x01, /* ... */ 0x02, /* ... */ 0x03, /* ... */ 0x04, /* broadcast MUID */ 0x7f, /* ... */ 0x7f, /* ... */ 0x7f, /* ... */ 0x7f, /* manufacturer */ 0x10, /* ... */ 0x11, /* ... */ 0x12, /* family */ 0x20, /* ... */ 0x21, /* model */ 0x30, /* ... */ 0x31, /* revision */ 0x40, /* ... */ 0x41, /* ... */ 0x42, /* ... */ 0x43, /* CI category supported */ 0x7f, /* max sysex size */ 0x7f, /* ... */ 0x7f, /* ... */ 0x7f); /* Missing final byte for a version 1 message */ Parser::Status status{}; const auto parsedV1 = Parser::parse (MUID::makeRandom (random), truncatedV1, &status); expect (status == Parser::Status::malformed); expect (parsedV1 == Message::Parsed { Message::Header { ChannelInGroup::wholeBlock, std::byte { 0x70 }, std::byte { version1 }, MUID::makeUnchecked (0x80c101), MUID::getBroadcast() }, std::monostate{} }); constexpr auto version2 = 0x02; const auto truncatedV2 = makeByteArray (0x7e, /* to function block */ 0x7f, /* midi CI */ 0x0d, /* discovery message */ 0x70, /* version */ version2, /* source MUID */ 0x01, /* ... */ 0x02, /* ... */ 0x03, /* ... */ 0x04, /* broadcast MUID */ 0x7f, /* ... */ 0x7f, /* ... */ 0x7f, /* ... */ 0x7f, /* manufacturer */ 0x10, /* ... */ 0x11, /* ... */ 0x12, /* family */ 0x20, /* ... */ 0x21, /* model */ 0x30, /* ... */ 0x31, /* revision */ 0x40, /* ... */ 0x41, /* ... */ 0x42, /* ... */ 0x43, /* CI category supported */ 0x7f, /* max sysex size */ 0x7f, /* ... */ 0x7f, /* ... */ 0x7f, /* ... */ 0x7f); /* Missing final byte for a version 2 message */ const auto parsedV2 = Parser::parse (MUID::makeRandom (random), truncatedV2); expect (status == Parser::Status::malformed); expect (parsedV2 == Message::Parsed { Message::Header { ChannelInGroup::wholeBlock, std::byte { 0x70 }, std::byte { version2 }, MUID::makeUnchecked (0x80c101), MUID::getBroadcast() }, std::monostate{} }); } const auto getExpectedDiscoveryInput = [] (uint8_t version, uint8_t outputPathID) { return Message::Parsed { Message::Header { ChannelInGroup::wholeBlock, std::byte { 0x70 }, std::byte { version }, MUID::makeUnchecked (0x80c101), MUID::getBroadcast() }, Message::Discovery { { { std::byte { 0x10 }, std::byte { 0x11 }, std::byte { 0x12 } }, { std::byte { 0x20 }, std::byte { 0x21 } }, { std::byte { 0x30 }, std::byte { 0x31 } }, { std::byte { 0x40 }, std::byte { 0x41 }, std::byte { 0x42 }, std::byte { 0x43 } } }, std::byte { 0x7f }, 0xfffffff, std::byte { outputPathID } } }; }; beginTest ("Sending a V1 discovery message notifies the input listener"); { const auto initialMUID = MUID::makeRandom (random); constexpr uint8_t version = 0x01; const auto bytes = makeByteArray (0x7e, /* to function block */ 0x7f, /* midi CI */ 0x0d, /* discovery message */ 0x70, /* version */ version, /* source MUID */ 0x01, /* ... */ 0x02, /* ... */ 0x03, /* ... */ 0x04, /* broadcast MUID */ 0x7f, /* ... */ 0x7f, /* ... */ 0x7f, /* ... */ 0x7f, /* manufacturer */ 0x10, /* ... */ 0x11, /* ... */ 0x12, /* family */ 0x20, /* ... */ 0x21, /* model */ 0x30, /* ... */ 0x31, /* revision */ 0x40, /* ... */ 0x41, /* ... */ 0x42, /* ... */ 0x43, /* CI category supported */ 0x7f, /* max sysex size */ 0x7f, /* ... */ 0x7f, /* ... */ 0x7f, /* ... */ 0x7f); const auto parsed = Parser::parse (initialMUID, bytes); expect (parsed == getExpectedDiscoveryInput (version, 0)); } beginTest ("Sending a V2 discovery message notifies the input listener"); { constexpr uint8_t outputPathID = 5; const auto initialMUID = MUID::makeRandom (random); constexpr uint8_t version = 0x02; const auto bytes = makeByteArray (0x7e, /* to function block */ 0x7f, /* midi CI */ 0x0d, /* discovery message */ 0x70, /* version */ version, /* source MUID */ 0x01, /* ... */ 0x02, /* ... */ 0x03, /* ... */ 0x04, /* broadcast MUID */ 0x7f, /* ... */ 0x7f, /* ... */ 0x7f, /* ... */ 0x7f, /* manufacturer */ 0x10, /* ... */ 0x11, /* ... */ 0x12, /* family */ 0x20, /* ... */ 0x21, /* model */ 0x30, /* ... */ 0x31, /* revision */ 0x40, /* ... */ 0x41, /* ... */ 0x42, /* ... */ 0x43, /* CI category supported */ 0x7f, /* max sysex size */ 0x7f, /* ... */ 0x7f, /* ... */ 0x7f, /* ... */ 0x7f, /* output path ID */ outputPathID); const auto parsed = Parser::parse (initialMUID, bytes); expect (parsed == getExpectedDiscoveryInput (version, outputPathID)); } beginTest ("Sending a discovery message with a future version notifies the input listener and ignores trailing fields"); { constexpr uint8_t outputPathID = 10; const auto initialMUID = MUID::makeRandom (random); constexpr auto version = (uint8_t) detail::MessageMeta::implementationVersion + 1; const auto bytes = makeByteArray (0x7e, /* to function block */ 0x7f, /* midi CI */ 0x0d, /* discovery message */ 0x70, /* version */ version, /* source MUID */ 0x01, /* ... */ 0x02, /* ... */ 0x03, /* ... */ 0x04, /* broadcast MUID */ 0x7f, /* ... */ 0x7f, /* ... */ 0x7f, /* ... */ 0x7f, /* manufacturer */ 0x10, /* ... */ 0x11, /* ... */ 0x12, /* family */ 0x20, /* ... */ 0x21, /* model */ 0x30, /* ... */ 0x31, /* revision */ 0x40, /* ... */ 0x41, /* ... */ 0x42, /* ... */ 0x43, /* CI category supported */ 0x7f, /* max sysex size */ 0x7f, /* ... */ 0x7f, /* ... */ 0x7f, /* ... */ 0x7f, /* output path ID */ outputPathID, /* extra bytes */ 0x00, /* ... */ 0x00, /* ... */ 0x00, /* ... */ 0x00); const auto parsed = Parser::parse (initialMUID, bytes); expect (parsed == getExpectedDiscoveryInput (version, outputPathID)); } } private: template static std::array makeByteArray (Ts&&... ts) { jassert (((0 <= (int) ts && (int) ts <= std::numeric_limits::max()) && ...)); return { std::byte (ts)... }; } }; static ParserTests parserTests; #endif } // namespace juce::midi_ci